Files

500 lines
15 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 implements a minimal HTTP/2 client connection (RFC 7540) on top of
// the framing and HPACK layers. It is intentionally synchronous and handles a
// single request at a time: it sends one request, then reads frames until that
// stream completes, servicing connection-level frames (SETTINGS, PING,
// WINDOW_UPDATE, GOAWAY) inline. Stream multiplexing and a background reader
// are a follow-up; this is the smallest useful, testable client.
// h2_client_preface is the fixed sequence a client sends to start an HTTP/2
// connection, immediately followed by a SETTINGS frame (RFC 7540 Section 3.5).
pub const h2_client_preface = 'PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n'
// h2_default_initial_window is the initial flow-control window for both the
// connection and new streams (RFC 7540 Section 6.9.2).
pub const h2_default_initial_window = u32(65535)
// h2_conn_read_chunk is the size of each transport read.
const h2_conn_read_chunk = 16 * 1024
// H2Transport is the byte transport an H2Conn runs over: typically an
// ALPN-negotiated `h2` TLS connection, but any reader/writer works (which makes
// the connection testable without a socket). Its method signatures match
// net.ssl.SSLConn, so an SSLConn satisfies it directly.
pub interface H2Transport {
mut:
read(mut buf []u8) !int
write(buf []u8) !int
}
// H2PeerSettings holds the peer's SETTINGS, with HTTP/2 defaults.
struct H2PeerSettings {
mut:
header_table_size u32 = 4096
enable_push bool = true
max_concurrent_streams u32 = max_u32
initial_window_size u32 = 65535
max_frame_size u32 = 16384
max_header_list_size u32 = max_u32
}
// H2DataFn is called for each DATA frame received on the response stream, with
// the chunk's bytes, the cumulative body bytes received (including this chunk),
// the body length from Content-Length if known (else 0), and the response
// status code.
pub type H2DataFn = fn (chunk []u8, body_so_far u64, body_expected u64, status int) !
// H2ClientRequest describes a single HTTP/2 request. Header names in `headers`
// must be lowercase (RFC 7540 Section 8.1.2); the pseudo-headers are filled in
// from the other fields.
pub struct H2ClientRequest {
pub:
method string = 'GET'
scheme string = 'https'
authority string
path string = '/'
headers []H2HeaderField
body []u8
// Optional response chunk callback, called after each DATA frame's payload
// is received. The arguments are the chunk bytes (not yet copied into the
// response body), the cumulative body bytes received so far (including this
// chunk), the body length from Content-Length (0 when not present), and the
// response status code.
on_data H2DataFn = unsafe { nil }
// stop_copying_limit, when >= 0, caps the cumulative body bytes copied into
// the response body; further DATA chunks are dropped but the callback keeps
// firing and the stream is drained to completion.
stop_copying_limit i64 = -1
// stop_receiving_limit, when >= 0, causes the response read loop to break
// once that many body bytes have been received. The callback fires for the
// final chunk; no further callbacks fire after that.
stop_receiving_limit i64 = -1
}
// H2ClientResponse is the result of an HTTP/2 request.
pub struct H2ClientResponse {
pub mut:
status int
headers []H2HeaderField
body []u8
}
// H2Conn is a client-side HTTP/2 connection.
pub struct H2Conn {
mut:
transport H2Transport
encoder H2HpackEncoder
decoder H2HpackDecoder
peer H2PeerSettings
rbuf []u8 // buffered bytes read from the transport, not yet consumed
pending []H2Frame // stream frames read early (while sending), to replay
next_stream_id u32 = 1 // clients use odd stream ids
cur_stream_id u32 // the stream currently being driven by do()
send_window i64 = 65535 // connection-level send flow-control window
stream_send_window i64 // send window for cur_stream_id
handshaked bool
goaway bool
// aborted is set when this connection terminated a stream early
// (RST_STREAM sent without draining the remaining DATA). Subsequent
// requests on the same connection must fail rather than risk being starved
// by leftover DATA frames the peer had already sent for the cancelled
// stream.
aborted bool
}
// new_h2_conn creates a client connection over `transport`. The HTTP/2
// connection preface is sent lazily on the first request.
pub fn new_h2_conn(transport H2Transport) &H2Conn {
return &H2Conn{
transport: transport
}
}
// do sends `req` and returns the response, after the request's stream closes.
pub fn (mut c H2Conn) do(req H2ClientRequest) !H2ClientResponse {
c.handshake()!
if c.goaway {
return error('h2: connection is shutting down (GOAWAY)')
}
if c.aborted {
return error('h2: connection is no longer usable after an early stream termination')
}
stream_id := c.next_stream_id
c.next_stream_id += 2
c.cur_stream_id = stream_id
c.stream_send_window = i64(c.peer.initial_window_size)
mut fields := [
H2HeaderField{':method', req.method},
H2HeaderField{':scheme', req.scheme},
H2HeaderField{':authority', req.authority},
H2HeaderField{':path', req.path},
]
for h in req.headers {
fields << h
}
block := c.encoder.encode(fields)
has_body := req.body.len > 0
c.send_header_block(stream_id, block, !has_body)!
if has_body {
c.send_body(stream_id, req.body)!
}
return c.read_response(stream_id, req)!
}
fn (mut c H2Conn) handshake() ! {
if c.handshaked {
return
}
c.write_all(h2_client_preface.bytes())!
// Advertise our SETTINGS: refuse server push, and use the protocol defaults
// otherwise. (Our decoder uses the default 4096-byte HPACK table.)
c.send_frame(H2SettingsFrame{
settings: [
H2Setting{h2_settings_enable_push, 0},
H2Setting{h2_settings_initial_window_size, h2_default_initial_window},
H2Setting{h2_settings_max_frame_size, h2_default_max_frame_size},
]
})!
c.handshaked = true
}
// read_response reads frames until `stream_id` is closed, returning its
// response and servicing connection-level frames along the way. The streaming
// options on `req` (on_data callback and the two stop limits) are honored
// while reading DATA frames, matching the HTTP/1.1 streaming semantics.
fn (mut c H2Conn) read_response(stream_id u32, req H2ClientRequest) !H2ClientResponse {
mut resp := H2ClientResponse{}
mut got_headers := false
mut body_so_far := u64(0)
mut body_expected := u64(0)
for {
frame := c.next_frame()!
if c.handle_conn_frame(frame)! {
continue
}
match frame {
H2HeadersFrame {
if frame.stream_id != stream_id {
continue
}
fragment := c.collect_header_block(frame.fragment, frame.end_headers, stream_id)!
for f in c.decoder.decode(fragment)! {
if f.name == ':status' {
resp.status = f.value.int()
} else if !f.name.starts_with(':') {
resp.headers << f
if f.name == 'content-length' {
body_expected = f.value.u64()
}
}
}
got_headers = true
if frame.end_stream {
break
}
}
H2DataFrame {
if frame.stream_id != stream_id {
continue
}
if frame.data.len > 0 {
body_so_far += u64(frame.data.len)
// Append the chunk to the response body unless the copy
// limit has been reached; the callback still fires.
if req.stop_copying_limit < 0
|| i64(body_so_far) - i64(frame.data.len) < req.stop_copying_limit {
if req.stop_copying_limit >= 0 && i64(body_so_far) > req.stop_copying_limit {
remaining := req.stop_copying_limit - (i64(body_so_far) - i64(frame.data.len))
if remaining > 0 {
resp.body << frame.data[..int(remaining)]
}
} else {
resp.body << frame.data
}
}
if req.on_data != unsafe { nil } {
req.on_data(frame.data, body_so_far, body_expected, resp.status)!
}
// Replenish flow control so the peer keeps sending.
c.send_window_update(0, u32(frame.data.len))!
c.send_window_update(stream_id, u32(frame.data.len))!
}
if frame.end_stream {
break
}
if req.stop_receiving_limit >= 0 && i64(body_so_far) >= req.stop_receiving_limit {
// Cancel the stream (RFC 7540 Section 8.1.4 / 5.4.2) so the
// peer stops sending more DATA, and mark the connection
// unusable: in-flight DATA frames that the peer has already
// sent for this stream would otherwise consume the
// connection-level receive window and block subsequent
// requests on the same H2Conn.
c.send_frame(H2RstStreamFrame{
stream_id: stream_id
error_code: u32(H2ErrorCode.cancel)
})!
c.aborted = true
break
}
}
H2RstStreamFrame {
if frame.stream_id == stream_id {
return error('h2: stream reset by peer (${h2_error_code_name(frame.error_code)})')
}
}
else {
// PRIORITY / PUSH_PROMISE / stray CONTINUATION / unknown: ignore.
}
}
}
if !got_headers {
return error('h2: stream closed without a response')
}
return resp
}
// collect_header_block returns the full HPACK block for a HEADERS frame,
// reading and concatenating CONTINUATION frames until END_HEADERS.
fn (mut c H2Conn) collect_header_block(first []u8, end_headers bool, stream_id u32) ![]u8 {
if end_headers {
return first
}
mut fragment := first.clone()
for {
frame := c.read_frame()!
if frame is H2ContinuationFrame {
if frame.stream_id != stream_id {
return error('h2: CONTINUATION on the wrong stream')
}
fragment << frame.fragment
if frame.end_headers {
break
}
} else {
return error('h2: expected a CONTINUATION frame')
}
}
return fragment
}
// handle_conn_frame services a connection-level frame, returning true if the
// frame was fully handled here (so the caller should skip it).
fn (mut c H2Conn) handle_conn_frame(frame H2Frame) !bool {
match frame {
H2SettingsFrame {
if !frame.ack {
c.apply_settings(frame.settings)
c.send_frame(H2SettingsFrame{
ack: true
})!
}
return true
}
H2PingFrame {
if !frame.ack {
c.send_frame(H2PingFrame{
ack: true
data: frame.data
})!
}
return true
}
H2WindowUpdateFrame {
if frame.stream_id == 0 {
c.send_window += i64(frame.window_size_increment)
} else if frame.stream_id == c.cur_stream_id {
c.stream_send_window += i64(frame.window_size_increment)
}
return true
}
H2GoawayFrame {
c.goaway = true
return error('h2: GOAWAY received (${h2_error_code_name(frame.error_code)})')
}
else {
return false
}
}
}
fn (mut c H2Conn) apply_settings(settings []H2Setting) {
for s in settings {
match s.id {
h2_settings_header_table_size {
c.peer.header_table_size = s.value
}
h2_settings_enable_push {
c.peer.enable_push = s.value != 0
}
h2_settings_max_concurrent_streams {
c.peer.max_concurrent_streams = s.value
}
h2_settings_initial_window_size {
// RFC 7540 Section 6.9.2: a change to the initial window size
// retroactively adjusts the active stream's send window by the
// delta.
delta := i64(s.value) - i64(c.peer.initial_window_size)
c.peer.initial_window_size = s.value
c.stream_send_window += delta
}
h2_settings_max_frame_size {
c.peer.max_frame_size = s.value
}
h2_settings_max_header_list_size {
c.peer.max_header_list_size = s.value
}
else {} // unknown settings are ignored (RFC 7540 Section 6.5.2)
}
}
}
// send_header_block sends an HPACK header block as a HEADERS frame, splitting
// it across CONTINUATION frames when it exceeds the peer's max frame size
// (RFC 7540 Section 4.3). END_STREAM, when set, goes on the HEADERS frame.
fn (mut c H2Conn) send_header_block(stream_id u32, block []u8, end_stream bool) ! {
max := int(c.peer.max_frame_size)
if block.len <= max {
c.send_frame(H2HeadersFrame{
stream_id: stream_id
fragment: block
end_headers: true
end_stream: end_stream
})!
return
}
c.send_frame(H2HeadersFrame{
stream_id: stream_id
fragment: block[..max]
end_headers: false
end_stream: end_stream
})!
mut off := max
for off < block.len {
mut next := off + max
if next > block.len {
next = block.len
}
c.send_frame(H2ContinuationFrame{
stream_id: stream_id
fragment: block[off..next]
end_headers: next == block.len
})!
off = next
}
}
// send_body writes the request body as DATA frames, chunked to the peer's max
// frame size and bounded by both the connection and the per-stream send
// flow-control windows (RFC 7540 Section 6.9).
fn (mut c H2Conn) send_body(stream_id u32, body []u8) ! {
max := int(c.peer.max_frame_size)
mut off := 0
for off < body.len {
for c.send_window <= 0 || c.stream_send_window <= 0 {
// Wait for the peer to grow a window. Connection-level frames are
// handled; any stream frames are stashed for read_response.
frame := c.next_frame()!
if !c.handle_conn_frame(frame)! {
c.pending << frame
}
}
avail := if c.send_window < c.stream_send_window {
c.send_window
} else {
c.stream_send_window
}
mut chunk := body.len - off
if chunk > max {
chunk = max
}
if i64(chunk) > avail {
chunk = int(avail)
}
next := off + chunk
c.send_frame(H2DataFrame{
stream_id: stream_id
data: body[off..next]
end_stream: next == body.len
})!
c.send_window -= i64(chunk)
c.stream_send_window -= i64(chunk)
off = next
}
}
fn (mut c H2Conn) send_window_update(stream_id u32, inc u32) ! {
if inc == 0 {
return
}
c.send_frame(H2WindowUpdateFrame{
stream_id: stream_id
window_size_increment: inc
})!
}
// next_frame returns the next frame, replaying any stashed stream frames first.
fn (mut c H2Conn) next_frame() !H2Frame {
if c.pending.len > 0 {
f := c.pending[0]
c.pending.delete(0)
return f
}
return c.read_frame()!
}
// read_frame reads and decodes one frame from the transport, enforcing our
// advertised max frame size.
fn (mut c H2Conn) read_frame() !H2Frame {
c.fill_at_least(h2_frame_header_len)!
header := h2_parse_frame_header(c.rbuf)!
if header.length > h2_default_max_frame_size {
return error('h2: frame larger than SETTINGS_MAX_FRAME_SIZE (${header.length})')
}
total := h2_frame_header_len + int(header.length)
c.fill_at_least(total)!
frame := h2_parse_frame(header, c.rbuf[h2_frame_header_len..total])!
c.rbuf = c.rbuf[total..].clone()
return frame
}
// fill_at_least reads from the transport until rbuf holds at least n bytes.
fn (mut c H2Conn) fill_at_least(n int) ! {
for c.rbuf.len < n {
mut tmp := []u8{len: h2_conn_read_chunk}
got := c.transport.read(mut tmp)!
if got <= 0 {
return error('h2: connection closed by peer')
}
c.rbuf << tmp[..got]
}
}
fn (mut c H2Conn) send_frame(f H2Frame) ! {
c.write_all(f.encode())!
}
fn (mut c H2Conn) write_all(data []u8) ! {
mut sent := 0
for sent < data.len {
n := c.transport.write(data[sent..])!
if n <= 0 {
return error('h2: transport write returned ${n}')
}
sent += n
}
}
// h2_error_code_name renders an HTTP/2 error code, falling back to hex for
// codes outside the defined range.
fn h2_error_code_name(code u32) string {
if code <= u32(H2ErrorCode.http_1_1_required) {
return unsafe { H2ErrorCode(code) }.str()
}
return 'unknown(0x${code.hex()})'
}