| 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 | // --- HTTP/2 (RFC 7540 / 7541) file map ----------------------------------------- |
| 7 | // The h2 implementation is split by layer; h2_conn.v (this file) is the lead. |
| 8 | // h2_frame.v binary framing layer (RFC 7540 §4, §6): parse/encode |
| 9 | // h2_hpack.v HPACK header compression (RFC 7541) |
| 10 | // h2_hpack_static.v HPACK static table data (RFC 7541 App. A) |
| 11 | // h2_hpack_huffman*.v HPACK Huffman code + table (generated) |
| 12 | // h2_error.v H2ErrorCode for RST_STREAM / GOAWAY |
| 13 | // h2_conn.v synchronous single-stream client connection (this file) |
| 14 | // h2_mux_conn.v multiplexed client: one connection, many concurrent streams |
| 15 | // h2_server.v server-side connection driver |
| 16 | // h2_client.v glue: net.http Request/Response <-> the h2 layer |
| 17 | // vschannel_h2_windows.c.v HTTP/2 over the Windows SChannel TLS transport |
| 18 | // Each *.v has a sibling *_test.v with hermetic in-memory-transport tests. |
| 19 | |
| 20 | // This file implements a minimal HTTP/2 client connection (RFC 7540) on top of |
| 21 | // the framing and HPACK layers. It is intentionally synchronous and handles a |
| 22 | // single request at a time: it sends one request, then reads frames until that |
| 23 | // stream completes, servicing connection-level frames (SETTINGS, PING, |
| 24 | // WINDOW_UPDATE, GOAWAY) inline. Concurrent multiplexing over one connection is |
| 25 | // provided separately by h2_mux_conn.v; this remains the smallest synchronous client. |
| 26 | |
| 27 | // h2_client_preface is the fixed sequence a client sends to start an HTTP/2 |
| 28 | // connection, immediately followed by a SETTINGS frame (RFC 7540 Section 3.5). |
| 29 | pub const h2_client_preface = 'PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n' |
| 30 | |
| 31 | // h2_default_initial_window is the initial flow-control window for both the |
| 32 | // connection and new streams (RFC 7540 Section 6.9.2). |
| 33 | pub const h2_default_initial_window = u32(65535) |
| 34 | |
| 35 | // h2_conn_read_chunk is the size of each transport read. |
| 36 | const h2_conn_read_chunk = 16 * 1024 |
| 37 | |
| 38 | // H2Transport is the byte transport an H2Conn runs over: typically an |
| 39 | // ALPN-negotiated `h2` TLS connection, but any reader/writer works (which makes |
| 40 | // the connection testable without a socket). Its method signatures match |
| 41 | // net.ssl.SSLConn, so an SSLConn satisfies it directly. |
| 42 | pub interface H2Transport { |
| 43 | mut: |
| 44 | read(mut buf []u8) !int |
| 45 | write(buf []u8) !int |
| 46 | } |
| 47 | |
| 48 | // H2PeerSettings holds the peer's SETTINGS, with HTTP/2 defaults. |
| 49 | struct H2PeerSettings { |
| 50 | mut: |
| 51 | header_table_size u32 = 4096 |
| 52 | enable_push bool = true |
| 53 | max_concurrent_streams u32 = max_u32 |
| 54 | initial_window_size u32 = 65535 |
| 55 | max_frame_size u32 = 16384 |
| 56 | max_header_list_size u32 = max_u32 |
| 57 | } |
| 58 | |
| 59 | // H2DataFn is called for each DATA frame received on the response stream, with |
| 60 | // the chunk's bytes, the cumulative body bytes received (including this chunk), |
| 61 | // the body length from Content-Length if known (else 0), and the response |
| 62 | // status code. |
| 63 | pub type H2DataFn = fn (chunk []u8, body_so_far u64, body_expected u64, status int) ! |
| 64 | |
| 65 | // H2ClientRequest describes a single HTTP/2 request. Header names in `headers` |
| 66 | // must be lowercase (RFC 7540 Section 8.1.2); the pseudo-headers are filled in |
| 67 | // from the other fields. |
| 68 | pub struct H2ClientRequest { |
| 69 | pub: |
| 70 | method string = 'GET' |
| 71 | scheme string = 'https' |
| 72 | authority string |
| 73 | path string = '/' |
| 74 | headers []H2HeaderField |
| 75 | body []u8 |
| 76 | // Optional response chunk callback, called after each DATA frame's payload |
| 77 | // is received. The arguments are the chunk bytes (not yet copied into the |
| 78 | // response body), the cumulative body bytes received so far (including this |
| 79 | // chunk), the body length from Content-Length (0 when not present), and the |
| 80 | // response status code. |
| 81 | on_data H2DataFn = unsafe { nil } |
| 82 | // stop_copying_limit, when >= 0, caps the cumulative body bytes copied into |
| 83 | // the response body; further DATA chunks are dropped but the callback keeps |
| 84 | // firing and the stream is drained to completion. |
| 85 | stop_copying_limit i64 = -1 |
| 86 | // stop_receiving_limit, when >= 0, causes the response read loop to break |
| 87 | // once that many body bytes have been received. The callback fires for the |
| 88 | // final chunk; no further callbacks fire after that. |
| 89 | stop_receiving_limit i64 = -1 |
| 90 | } |
| 91 | |
| 92 | // H2ClientResponse is the result of an HTTP/2 request. |
| 93 | pub struct H2ClientResponse { |
| 94 | pub mut: |
| 95 | status int |
| 96 | headers []H2HeaderField |
| 97 | body []u8 |
| 98 | } |
| 99 | |
| 100 | // H2Conn is a client-side HTTP/2 connection. |
| 101 | pub struct H2Conn { |
| 102 | mut: |
| 103 | transport H2Transport |
| 104 | encoder H2HpackEncoder |
| 105 | decoder H2HpackDecoder |
| 106 | peer H2PeerSettings |
| 107 | rbuf []u8 // buffered bytes read from the transport, not yet consumed |
| 108 | pending []H2Frame // stream frames read early (while sending), to replay |
| 109 | next_stream_id u32 = 1 // clients use odd stream ids |
| 110 | cur_stream_id u32 // the stream currently being driven by do() |
| 111 | send_window i64 = 65535 // connection-level send flow-control window |
| 112 | stream_send_window i64 // send window for cur_stream_id |
| 113 | handshaked bool |
| 114 | goaway bool |
| 115 | // aborted is set when this connection terminated a stream early |
| 116 | // (RST_STREAM sent without draining the remaining DATA). Subsequent |
| 117 | // requests on the same connection must fail rather than risk being starved |
| 118 | // by leftover DATA frames the peer had already sent for the cancelled |
| 119 | // stream. |
| 120 | aborted bool |
| 121 | } |
| 122 | |
| 123 | // new_h2_conn creates a client connection over `transport`. The HTTP/2 |
| 124 | // connection preface is sent lazily on the first request. |
| 125 | pub fn new_h2_conn(transport H2Transport) &H2Conn { |
| 126 | return &H2Conn{ |
| 127 | transport: transport |
| 128 | } |
| 129 | } |
| 130 | |
| 131 | // do sends `req` and returns the response, after the request's stream closes. |
| 132 | pub fn (mut c H2Conn) do(req H2ClientRequest) !H2ClientResponse { |
| 133 | c.handshake()! |
| 134 | if c.goaway { |
| 135 | return error('h2: connection is shutting down (GOAWAY)') |
| 136 | } |
| 137 | if c.aborted { |
| 138 | return error('h2: connection is no longer usable after an early stream termination') |
| 139 | } |
| 140 | stream_id := c.next_stream_id |
| 141 | c.next_stream_id += 2 |
| 142 | c.cur_stream_id = stream_id |
| 143 | c.stream_send_window = i64(c.peer.initial_window_size) |
| 144 | |
| 145 | mut fields := [ |
| 146 | H2HeaderField{':method', req.method}, |
| 147 | H2HeaderField{':scheme', req.scheme}, |
| 148 | H2HeaderField{':authority', req.authority}, |
| 149 | H2HeaderField{':path', req.path}, |
| 150 | ] |
| 151 | for h in req.headers { |
| 152 | fields << h |
| 153 | } |
| 154 | // RFC 9113 §6.5.2: honor the peer's advisory SETTINGS_MAX_HEADER_LIST_SIZE — |
| 155 | // refuse an over-limit request locally instead of having the server reject it |
| 156 | // after the round trip. |
| 157 | if c.peer.max_header_list_size != max_u32 { |
| 158 | size := h2_header_list_size(fields) |
| 159 | if size > u64(c.peer.max_header_list_size) { |
| 160 | return error('h2: request header list (${size} bytes) exceeds peer SETTINGS_MAX_HEADER_LIST_SIZE (${c.peer.max_header_list_size})') |
| 161 | } |
| 162 | } |
| 163 | block := c.encoder.encode(fields) |
| 164 | has_body := req.body.len > 0 |
| 165 | c.send_header_block(stream_id, block, !has_body)! |
| 166 | if has_body { |
| 167 | c.send_body(stream_id, req.body)! |
| 168 | } |
| 169 | return c.read_response(stream_id, req)! |
| 170 | } |
| 171 | |
| 172 | fn (mut c H2Conn) handshake() ! { |
| 173 | if c.handshaked { |
| 174 | return |
| 175 | } |
| 176 | c.write_all(h2_client_preface.bytes())! |
| 177 | // Advertise our SETTINGS: refuse server push, and use the protocol defaults |
| 178 | // otherwise. (Our decoder uses the default 4096-byte HPACK table.) |
| 179 | c.send_frame(H2SettingsFrame{ |
| 180 | settings: [ |
| 181 | H2Setting{h2_settings_enable_push, 0}, |
| 182 | H2Setting{h2_settings_initial_window_size, h2_default_initial_window}, |
| 183 | H2Setting{h2_settings_max_frame_size, h2_default_max_frame_size}, |
| 184 | ] |
| 185 | })! |
| 186 | c.handshaked = true |
| 187 | } |
| 188 | |
| 189 | // read_response reads frames until `stream_id` is closed, returning its |
| 190 | // response and servicing connection-level frames along the way. The streaming |
| 191 | // options on `req` (on_data callback and the two stop limits) are honored |
| 192 | // while reading DATA frames, matching the HTTP/1.1 streaming semantics. |
| 193 | fn (mut c H2Conn) read_response(stream_id u32, req H2ClientRequest) !H2ClientResponse { |
| 194 | mut resp := H2ClientResponse{} |
| 195 | mut got_headers := false |
| 196 | mut body_so_far := u64(0) |
| 197 | mut body_expected := u64(0) |
| 198 | mut has_content_length := false |
| 199 | for { |
| 200 | frame := c.next_frame()! |
| 201 | if c.handle_conn_frame(frame)! { |
| 202 | continue |
| 203 | } |
| 204 | match frame { |
| 205 | H2HeadersFrame { |
| 206 | if frame.stream_id != stream_id { |
| 207 | continue |
| 208 | } |
| 209 | fragment := c.collect_header_block(frame.fragment, frame.end_headers, stream_id)! |
| 210 | // Decode once — HPACK table state is advanced here. |
| 211 | decoded := c.decoder.decode(fragment)! |
| 212 | if got_headers { |
| 213 | // A second HEADERS block carries trailers, which MUST end the |
| 214 | // stream (RFC 9113 §8.1). Without END_STREAM read_response would |
| 215 | // loop forever waiting for a stream end that never comes; the mux |
| 216 | // path resets such a stream, so the synchronous client fails too. |
| 217 | if !frame.end_stream { |
| 218 | return error('h2: trailers HEADERS frame must carry END_STREAM') |
| 219 | } |
| 220 | for f in decoded { |
| 221 | // RFC 9113 §8.1: trailers MUST NOT contain pseudo-header fields; |
| 222 | // the §8.2 field-name rules apply. A malformed field makes the |
| 223 | // response malformed — fail the request (mirrors the mux reset). |
| 224 | if f.name.starts_with(':') { |
| 225 | return error('h2: malformed trailers: pseudo-header ${f.name}') |
| 226 | } |
| 227 | reason := h2_response_field_error(f.name) |
| 228 | if reason != '' { |
| 229 | return error('h2: malformed trailers: ${reason}') |
| 230 | } |
| 231 | resp.headers << f |
| 232 | } |
| 233 | break |
| 234 | } |
| 235 | // First (interim or final) response HEADERS. Find :status. A |
| 236 | // response MUST carry a valid :status (RFC 9113 §8.3.1), and 101 is |
| 237 | // forbidden in HTTP/2 (§8.1.1). |
| 238 | mut status := 0 |
| 239 | mut status_seen := false |
| 240 | for f in decoded { |
| 241 | if f.name == ':status' { |
| 242 | if f.value.len == 3 && all_digits(f.value) { |
| 243 | status = f.value.int() |
| 244 | } else { |
| 245 | return error('h2: malformed :status value: ${f.value}') |
| 246 | } |
| 247 | status_seen = true |
| 248 | break |
| 249 | } |
| 250 | } |
| 251 | if !status_seen || status < 100 || status > 599 || status == 101 { |
| 252 | // Missing / out-of-range / 101 status: fail rather than latch a |
| 253 | // bogus status or (for 101 or a sub-200 interim) loop forever |
| 254 | // waiting for a "final" HEADERS. Mirrors the mux path, which |
| 255 | // resets the stream with PROTOCOL_ERROR. |
| 256 | return error('h2: response with a missing or invalid :status: ${status}') |
| 257 | } |
| 258 | if status >= 100 && status < 200 { |
| 259 | // 1xx informational: discard and continue waiting for the |
| 260 | // final HEADERS block. Do not set got_headers here. |
| 261 | // A 1xx is not a final response and must not end the stream |
| 262 | // (RFC 9113 §8.1); END_STREAM here is malformed. Fail rather |
| 263 | // than loop forever waiting for a final response the stream can |
| 264 | // no longer send. (The mux path rejects this as a stream-level |
| 265 | // PROTOCOL_ERROR; the synchronous client has no other stream to |
| 266 | // keep alive, so a connection-level error is appropriate here.) |
| 267 | if frame.end_stream { |
| 268 | return error('h2: server set END_STREAM on a 1xx informational response') |
| 269 | } |
| 270 | continue |
| 271 | } |
| 272 | // Final response (status >= 200): populate, skipping pseudo-headers. |
| 273 | resp.status = status |
| 274 | mut seen_regular := false |
| 275 | mut seen_status := false |
| 276 | for f in decoded { |
| 277 | if f.name.starts_with(':') { |
| 278 | // RFC 9113 §8.3: in a response only :status is valid; pseudo- |
| 279 | // headers MUST precede regular fields and MUST NOT be duplicated. |
| 280 | // An undefined pseudo, :status after a regular field, or a |
| 281 | // second :status is malformed. |
| 282 | if f.name != ':status' || seen_regular || seen_status { |
| 283 | return error('h2: malformed response: invalid pseudo-header ${f.name}') |
| 284 | } |
| 285 | seen_status = true |
| 286 | continue |
| 287 | } |
| 288 | seen_regular = true |
| 289 | // RFC 9113 §8.2: reject uppercase/empty names and connection-specific |
| 290 | // fields rather than delivering a malformed response to the caller. |
| 291 | reason := h2_response_field_error(f.name) |
| 292 | if reason != '' { |
| 293 | return error('h2: malformed response: ${reason}') |
| 294 | } |
| 295 | resp.headers << f |
| 296 | if f.name == 'content-length' { |
| 297 | if all_digits(f.value) { |
| 298 | body_expected = f.value.u64() |
| 299 | has_content_length = true |
| 300 | } else { |
| 301 | return error('h2: malformed Content-Length: ${f.value}') |
| 302 | } |
| 303 | } |
| 304 | } |
| 305 | got_headers = true |
| 306 | if frame.end_stream { |
| 307 | break |
| 308 | } |
| 309 | } |
| 310 | H2DataFrame { |
| 311 | if frame.stream_id != stream_id { |
| 312 | continue |
| 313 | } |
| 314 | if !got_headers { |
| 315 | // DATA before the response HEADERS is a protocol error |
| 316 | // (RFC 9113 §8.1); the mux path rejects it. Fail rather than |
| 317 | // deliver body bytes for a response that has no status yet. |
| 318 | return error('h2: DATA frame before response HEADERS') |
| 319 | } |
| 320 | if frame.data.len > 0 { |
| 321 | body_so_far += u64(frame.data.len) |
| 322 | // Append the chunk to the response body unless the copy |
| 323 | // limit has been reached; the callback still fires. |
| 324 | if req.stop_copying_limit < 0 |
| 325 | || i64(body_so_far) - i64(frame.data.len) < req.stop_copying_limit { |
| 326 | if req.stop_copying_limit >= 0 && i64(body_so_far) > req.stop_copying_limit { |
| 327 | remaining := req.stop_copying_limit - (i64(body_so_far) - i64(frame.data.len)) |
| 328 | if remaining > 0 { |
| 329 | resp.body << frame.data[..int(remaining)] |
| 330 | } |
| 331 | } else { |
| 332 | resp.body << frame.data |
| 333 | } |
| 334 | } |
| 335 | if req.on_data != unsafe { nil } { |
| 336 | req.on_data(frame.data, body_so_far, body_expected, resp.status)! |
| 337 | } |
| 338 | } |
| 339 | // Replenish flow control using flow_size (full wire payload including |
| 340 | // padding), per RFC 7540 §6.9.1. Credit unconditionally when flow_size>0: |
| 341 | // a padding-only DATA frame (data.len==0, flow_size>0) still consumes the |
| 342 | // peer's send window and must be credited back. (Formerly inside the |
| 343 | // data.len>0 block — padding-only frames leaked window silently.) |
| 344 | if frame.flow_size > 0 { |
| 345 | c.send_window_update(0, u32(frame.flow_size))! |
| 346 | c.send_window_update(stream_id, u32(frame.flow_size))! |
| 347 | } |
| 348 | if frame.end_stream { |
| 349 | break |
| 350 | } |
| 351 | if req.stop_receiving_limit >= 0 && i64(body_so_far) >= req.stop_receiving_limit { |
| 352 | // Cancel the stream (RFC 7540 Section 8.1.4 / 5.4.2) so the |
| 353 | // peer stops sending more DATA, and mark the connection |
| 354 | // unusable: in-flight DATA frames that the peer has already |
| 355 | // sent for this stream would otherwise consume the |
| 356 | // connection-level receive window and block subsequent |
| 357 | // requests on the same H2Conn. |
| 358 | c.send_frame(H2RstStreamFrame{ |
| 359 | stream_id: stream_id |
| 360 | error_code: u32(H2ErrorCode.cancel) |
| 361 | })! |
| 362 | c.aborted = true |
| 363 | break |
| 364 | } |
| 365 | } |
| 366 | H2RstStreamFrame { |
| 367 | if frame.stream_id == stream_id { |
| 368 | return error('h2: stream reset by peer (${h2_error_code_name(frame.error_code)})') |
| 369 | } |
| 370 | } |
| 371 | H2PushPromiseFrame { |
| 372 | // We sent SETTINGS_ENABLE_PUSH=0 in the preface; receiving |
| 373 | // PUSH_PROMISE is a connection error (RFC 7540 §8.2 PROTOCOL_ERROR). |
| 374 | // Ignoring it without decoding the embedded HPACK block would also |
| 375 | // desync the dynamic table. |
| 376 | return error('h2: unexpected PUSH_PROMISE (server push was disabled)') |
| 377 | } |
| 378 | else { |
| 379 | // PRIORITY / stray CONTINUATION / unknown: ignore. |
| 380 | } |
| 381 | } |
| 382 | } |
| 383 | if !got_headers { |
| 384 | return error('h2: stream closed without a response') |
| 385 | } |
| 386 | // Verify body completeness when Content-Length was advertised (RFC 7230 §3.3.2). |
| 387 | // Skip HEAD responses and 204/304 which carry no body by definition. |
| 388 | // Skip early-cancelled streams where we sent RST_STREAM ourselves. |
| 389 | body_allowed := req.method != 'HEAD' && resp.status != 204 && resp.status != 304 |
| 390 | if has_content_length && body_allowed && !c.aborted && body_so_far != body_expected { |
| 391 | return error('h2: response body ${body_so_far} bytes does not match Content-Length ${body_expected}') |
| 392 | } |
| 393 | return resp |
| 394 | } |
| 395 | |
| 396 | // collect_header_block returns the full HPACK block for a HEADERS frame, |
| 397 | // reading and concatenating CONTINUATION frames until END_HEADERS. |
| 398 | fn (mut c H2Conn) collect_header_block(first []u8, end_headers bool, stream_id u32) ![]u8 { |
| 399 | if end_headers { |
| 400 | return first |
| 401 | } |
| 402 | mut fragment := first.clone() |
| 403 | for { |
| 404 | frame := c.read_frame()! |
| 405 | if frame is H2ContinuationFrame { |
| 406 | if frame.stream_id != stream_id { |
| 407 | return error('h2: CONTINUATION on the wrong stream') |
| 408 | } |
| 409 | fragment << frame.fragment |
| 410 | if fragment.len > h2_max_recv_header_block { |
| 411 | return error('h2: response header block exceeds ${h2_max_recv_header_block} bytes') |
| 412 | } |
| 413 | if frame.end_headers { |
| 414 | break |
| 415 | } |
| 416 | } else { |
| 417 | return error('h2: expected a CONTINUATION frame') |
| 418 | } |
| 419 | } |
| 420 | return fragment |
| 421 | } |
| 422 | |
| 423 | // handle_conn_frame services a connection-level frame, returning true if the |
| 424 | // frame was fully handled here (so the caller should skip it). |
| 425 | fn (mut c H2Conn) handle_conn_frame(frame H2Frame) !bool { |
| 426 | match frame { |
| 427 | H2SettingsFrame { |
| 428 | if !frame.ack { |
| 429 | c.apply_settings(frame.settings)! |
| 430 | c.send_frame(H2SettingsFrame{ |
| 431 | ack: true |
| 432 | })! |
| 433 | } |
| 434 | return true |
| 435 | } |
| 436 | H2PingFrame { |
| 437 | if !frame.ack { |
| 438 | c.send_frame(H2PingFrame{ |
| 439 | ack: true |
| 440 | data: frame.data |
| 441 | })! |
| 442 | } |
| 443 | return true |
| 444 | } |
| 445 | H2WindowUpdateFrame { |
| 446 | inc := frame.window_size_increment |
| 447 | if frame.stream_id == 0 { |
| 448 | if inc == 0 { |
| 449 | // RFC 7540 §6.9: connection-level zero increment is a connection |
| 450 | // error (PROTOCOL_ERROR). |
| 451 | return error('h2: connection WINDOW_UPDATE with zero increment (RFC 7540 §6.9 PROTOCOL_ERROR)') |
| 452 | } |
| 453 | new_window := c.send_window + i64(inc) |
| 454 | if new_window > i64(0x7fff_ffff) { |
| 455 | // RFC 7540 §6.9.1: exceeding 2^31-1 is a FLOW_CONTROL_ERROR. |
| 456 | return error('h2: connection flow-control window exceeded 2^31-1 (RFC 7540 §6.9.1)') |
| 457 | } |
| 458 | c.send_window = new_window |
| 459 | } else if frame.stream_id == c.cur_stream_id { |
| 460 | if inc == 0 { |
| 461 | // RFC 7540 §6.9: stream-level zero increment is a stream error |
| 462 | // (RST_STREAM). H2Conn has no multi-stream tracking so we treat |
| 463 | // it as connection-fatal to avoid accepting a broken stream state. |
| 464 | return error('h2: stream WINDOW_UPDATE with zero increment (RFC 7540 §6.9 PROTOCOL_ERROR)') |
| 465 | } |
| 466 | new_window := c.stream_send_window + i64(inc) |
| 467 | if new_window > i64(0x7fff_ffff) { |
| 468 | return error('h2: stream flow-control window exceeded 2^31-1 (RFC 7540 §6.9.1)') |
| 469 | } |
| 470 | c.stream_send_window = new_window |
| 471 | } |
| 472 | // Zero-increment on an untracked stream: silently ignore — H2Conn |
| 473 | // cannot send RST_STREAM for a stream it does not own. |
| 474 | return true |
| 475 | } |
| 476 | H2GoawayFrame { |
| 477 | c.goaway = true |
| 478 | return error('h2: GOAWAY received (${h2_error_code_name(frame.error_code)})') |
| 479 | } |
| 480 | else { |
| 481 | return false |
| 482 | } |
| 483 | } |
| 484 | } |
| 485 | |
| 486 | fn (mut c H2Conn) apply_settings(settings []H2Setting) ! { |
| 487 | for s in settings { |
| 488 | match s.id { |
| 489 | h2_settings_header_table_size { |
| 490 | c.peer.header_table_size = s.value |
| 491 | // RFC 7541 §6.3: SETTINGS_HEADER_TABLE_SIZE from the server |
| 492 | // constrains our ENCODER's dynamic table (outgoing request headers). |
| 493 | // Evict entries exceeding the new limit immediately so we never |
| 494 | // reference indices the peer has already evicted; the pending flag |
| 495 | // causes encode() to emit the required Dynamic Table Size Update |
| 496 | // prefix at the start of the next header block. |
| 497 | c.encoder.dyn_table.set_max_size(int(s.value)) |
| 498 | c.encoder.pending_max_table_size = int(s.value) |
| 499 | } |
| 500 | h2_settings_enable_push { |
| 501 | c.peer.enable_push = s.value != 0 |
| 502 | } |
| 503 | h2_settings_max_concurrent_streams { |
| 504 | c.peer.max_concurrent_streams = s.value |
| 505 | } |
| 506 | h2_settings_initial_window_size { |
| 507 | if s.value > u32(0x7fff_ffff) { |
| 508 | // RFC 7540 §6.5.3: values above 2^31-1 are a FLOW_CONTROL_ERROR. |
| 509 | return error('h2: peer SETTINGS_INITIAL_WINDOW_SIZE ${s.value} exceeds 2^31-1') |
| 510 | } |
| 511 | // RFC 7540 §6.9.2: a change to the initial window size |
| 512 | // retroactively adjusts the active stream's send window by the delta. |
| 513 | // Validate before mutating so a rejected SETTINGS leaves state consistent. |
| 514 | delta := i64(s.value) - i64(c.peer.initial_window_size) |
| 515 | new_window := c.stream_send_window + delta |
| 516 | if new_window > i64(0x7fff_ffff) { |
| 517 | // RFC 7540 §6.9.2: if applying the delta pushes the stream |
| 518 | // window above 2^31-1 it is a FLOW_CONTROL_ERROR. |
| 519 | return error('h2: SETTINGS_INITIAL_WINDOW_SIZE delta overflows stream send window') |
| 520 | } |
| 521 | c.peer.initial_window_size = s.value |
| 522 | c.stream_send_window = new_window |
| 523 | } |
| 524 | h2_settings_max_frame_size { |
| 525 | if s.value < h2_default_max_frame_size || s.value > u32(0x00ff_ffff) { |
| 526 | // RFC 7540 §6.5.2: valid range is 2^14..2^24-1. Values outside |
| 527 | // this range are a connection SETTINGS_ERROR; a zero value would |
| 528 | // also make our HEADERS/DATA chunk step zero and hang senders. |
| 529 | return error('h2: peer SETTINGS_MAX_FRAME_SIZE ${s.value} out of range [16384, 16777215]') |
| 530 | } |
| 531 | c.peer.max_frame_size = s.value |
| 532 | } |
| 533 | h2_settings_max_header_list_size { |
| 534 | c.peer.max_header_list_size = s.value |
| 535 | } |
| 536 | else {} // unknown settings are ignored (RFC 7540 §6.5.2) |
| 537 | } |
| 538 | } |
| 539 | } |
| 540 | |
| 541 | // send_header_block sends an HPACK header block as a HEADERS frame, splitting |
| 542 | // it across CONTINUATION frames when it exceeds the peer's max frame size |
| 543 | // (RFC 7540 Section 4.3). END_STREAM, when set, goes on the HEADERS frame. |
| 544 | fn (mut c H2Conn) send_header_block(stream_id u32, block []u8, end_stream bool) ! { |
| 545 | max := int(c.peer.max_frame_size) |
| 546 | if block.len <= max { |
| 547 | c.send_frame(H2HeadersFrame{ |
| 548 | stream_id: stream_id |
| 549 | fragment: block |
| 550 | end_headers: true |
| 551 | end_stream: end_stream |
| 552 | })! |
| 553 | return |
| 554 | } |
| 555 | c.send_frame(H2HeadersFrame{ |
| 556 | stream_id: stream_id |
| 557 | fragment: block[..max] |
| 558 | end_headers: false |
| 559 | end_stream: end_stream |
| 560 | })! |
| 561 | mut off := max |
| 562 | for off < block.len { |
| 563 | mut next := off + max |
| 564 | if next > block.len { |
| 565 | next = block.len |
| 566 | } |
| 567 | c.send_frame(H2ContinuationFrame{ |
| 568 | stream_id: stream_id |
| 569 | fragment: block[off..next] |
| 570 | end_headers: next == block.len |
| 571 | })! |
| 572 | off = next |
| 573 | } |
| 574 | } |
| 575 | |
| 576 | // send_body writes the request body as DATA frames, chunked to the peer's max |
| 577 | // frame size and bounded by both the connection and the per-stream send |
| 578 | // flow-control windows (RFC 7540 Section 6.9). |
| 579 | fn (mut c H2Conn) send_body(stream_id u32, body []u8) ! { |
| 580 | max := int(c.peer.max_frame_size) |
| 581 | mut off := 0 |
| 582 | for off < body.len { |
| 583 | for c.send_window <= 0 || c.stream_send_window <= 0 { |
| 584 | // Wait for the peer to grow a window. Connection-level frames are |
| 585 | // handled; any stream frames are stashed for read_response. |
| 586 | frame := c.next_frame()! |
| 587 | if !c.handle_conn_frame(frame)! { |
| 588 | c.pending << frame |
| 589 | } |
| 590 | } |
| 591 | avail := if c.send_window < c.stream_send_window { |
| 592 | c.send_window |
| 593 | } else { |
| 594 | c.stream_send_window |
| 595 | } |
| 596 | mut chunk := body.len - off |
| 597 | if chunk > max { |
| 598 | chunk = max |
| 599 | } |
| 600 | if i64(chunk) > avail { |
| 601 | chunk = int(avail) |
| 602 | } |
| 603 | next := off + chunk |
| 604 | c.send_frame(H2DataFrame{ |
| 605 | stream_id: stream_id |
| 606 | data: body[off..next] |
| 607 | end_stream: next == body.len |
| 608 | })! |
| 609 | c.send_window -= i64(chunk) |
| 610 | c.stream_send_window -= i64(chunk) |
| 611 | off = next |
| 612 | } |
| 613 | } |
| 614 | |
| 615 | fn (mut c H2Conn) send_window_update(stream_id u32, inc u32) ! { |
| 616 | if inc == 0 { |
| 617 | return |
| 618 | } |
| 619 | c.send_frame(H2WindowUpdateFrame{ |
| 620 | stream_id: stream_id |
| 621 | window_size_increment: inc |
| 622 | })! |
| 623 | } |
| 624 | |
| 625 | // next_frame returns the next frame, replaying any stashed stream frames first. |
| 626 | fn (mut c H2Conn) next_frame() !H2Frame { |
| 627 | if c.pending.len > 0 { |
| 628 | f := c.pending[0] |
| 629 | c.pending.delete(0) |
| 630 | return f |
| 631 | } |
| 632 | return c.read_frame()! |
| 633 | } |
| 634 | |
| 635 | // read_frame reads and decodes one frame from the transport, enforcing the |
| 636 | // receive limit we advertised to the peer in our own SETTINGS. This is |
| 637 | // h2_default_max_frame_size, which H2Conn always sends and never renegotiates. |
| 638 | // (c.peer.max_frame_size is the peer's receive limit — our outbound cap — |
| 639 | // and must not be used here.) |
| 640 | fn (mut c H2Conn) read_frame() !H2Frame { |
| 641 | c.fill_at_least(h2_frame_header_len)! |
| 642 | header := h2_parse_frame_header(c.rbuf)! |
| 643 | if header.length > h2_default_max_frame_size { |
| 644 | return error('h2: frame larger than SETTINGS_MAX_FRAME_SIZE (${header.length})') |
| 645 | } |
| 646 | total := h2_frame_header_len + int(header.length) |
| 647 | c.fill_at_least(total)! |
| 648 | frame := h2_parse_frame(header, c.rbuf[h2_frame_header_len..total])! |
| 649 | c.rbuf = c.rbuf[total..].clone() |
| 650 | return frame |
| 651 | } |
| 652 | |
| 653 | // fill_at_least reads from the transport until rbuf holds at least n bytes. |
| 654 | fn (mut c H2Conn) fill_at_least(n int) ! { |
| 655 | for c.rbuf.len < n { |
| 656 | mut tmp := []u8{len: h2_conn_read_chunk} |
| 657 | got := c.transport.read(mut tmp)! |
| 658 | if got <= 0 { |
| 659 | return error('h2: connection closed by peer') |
| 660 | } |
| 661 | c.rbuf << tmp[..got] |
| 662 | } |
| 663 | } |
| 664 | |
| 665 | fn (mut c H2Conn) send_frame(f H2Frame) ! { |
| 666 | c.write_all(f.encode())! |
| 667 | } |
| 668 | |
| 669 | fn (mut c H2Conn) write_all(data []u8) ! { |
| 670 | mut sent := 0 |
| 671 | for sent < data.len { |
| 672 | n := c.transport.write(data[sent..])! |
| 673 | if n <= 0 { |
| 674 | return error('h2: transport write returned ${n}') |
| 675 | } |
| 676 | sent += n |
| 677 | } |
| 678 | } |
| 679 | |
| 680 | // h2_error_code_name renders an HTTP/2 error code, falling back to hex for |
| 681 | // codes outside the defined range. |
| 682 | fn h2_error_code_name(code u32) string { |
| 683 | if code <= u32(H2ErrorCode.http_1_1_required) { |
| 684 | return unsafe { H2ErrorCode(code) }.str() |
| 685 | } |
| 686 | return 'unknown(0x${code.hex()})' |
| 687 | } |
| 688 | |