Files
agila_open_tool/tools/v/vlib/net/http/h2_server.v
T

565 lines
17 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 server-side HTTP/2 connection driver on top
// of the existing framing (h2_frame.v) and HPACK (h2_hpack.v) layers. It is
// intentionally serial: SETTINGS_MAX_CONCURRENT_STREAMS is advertised as 1,
// so the peer sends one request at a time. Concurrent stream handling and
// background writers are a planned follow-up.
// h2_server_max_concurrent_streams is the value advertised in our SETTINGS;
// the peer may not open more streams than this at once.
const h2_server_max_concurrent_streams = u32(1)
// h2_server_default_window is the default initial flow-control window
// (RFC 7540 Section 6.9.2).
const h2_server_default_window = u32(65535)
// h2_server_max_request_body caps the in-memory request body the server will
// accept before responding with a stream error. Bodies larger than this are
// rejected with RST_STREAM(REFUSED_STREAM).
const h2_server_max_request_body = 8 * 1024 * 1024
// H2ServerStream holds the state of one in-flight server-side stream.
struct H2ServerStream {
mut:
id u32
hpack_block []u8 // assembled HEADERS + CONTINUATION fragments
headers []H2HeaderField
body []u8
end_headers bool
end_stream bool
headers_done bool // set once we've decoded the HPACK block
send_window i64
}
// H2ServerConn drives one server-side HTTP/2 connection over a transport.
struct H2ServerConn {
mut:
transport H2Transport
encoder H2HpackEncoder
decoder H2HpackDecoder
peer H2PeerSettings
rbuf []u8
send_window i64 = i64(h2_server_default_window)
streams map[u32]&H2ServerStream
last_stream_id u32
awaiting_cont u32 // non-zero when mid-CONTINUATION on this stream
closing bool
idle_conns &TlsIdleConnTracker = unsafe { nil }
idle_handle int
}
// serve_h2_conn drives a single HTTP/2 server-side connection until the
// transport closes or a protocol error forces a GOAWAY. `handler` is invoked
// once per fully-received request stream.
fn serve_h2_conn(mut transport H2Transport, mut handler Handler) ! {
serve_h2_conn_with_idle_tracker(mut transport, mut handler, unsafe { nil }, 0)!
}
fn serve_h2_conn_with_idle_tracker(mut transport H2Transport, mut handler Handler, idle_conns &TlsIdleConnTracker, idle_handle int) ! {
mut c := &H2ServerConn{
transport: transport
idle_conns: idle_conns
idle_handle: idle_handle
}
c.serve(mut handler) or {
// Best-effort GOAWAY before bailing. Skip if one was already sent by
// the error path (e.g. apply_settings sends FLOW_CONTROL_ERROR).
if !c.closing {
c.send_goaway(.protocol_error, err.msg()) or {}
}
return err
}
}
fn (mut c H2ServerConn) serve(mut handler Handler) ! {
// Register the connection with the idle tracker once for its whole
// lifetime, instead of around every frame read. The reader thread spends
// nearly all of its time blocked in a frame read, so per-frame mark/unmark
// only added shared-lock contention (and an O(n) list scan) on the hot
// path. On shutdown, close_idle still interrupts the reader by shutting the
// fd down; an h2 request in flight when the server stops is interrupted —
// including response writes, which may be truncated mid-DATA-frame. This
// is acceptable at shutdown and is not relied on by any caller (the
// graceful "wait for active request" guarantee is HTTP/1.1-only).
tracked := c.should_track_idle_read()
if tracked && !c.idle_conns.mark_idle(c.idle_handle) {
// The server is already shutting down; do not start serving.
return
}
defer {
if tracked {
c.idle_conns.unmark_idle(c.idle_handle)
}
}
c.read_client_preface()!
c.send_initial_settings()!
for !c.closing {
frame := c.read_frame() or {
// Treat a clean transport close as end of session.
return
}
c.dispatch_frame(frame, mut handler)!
}
}
fn (mut c H2ServerConn) should_track_idle_read() bool {
return c.idle_handle > 0 && c.idle_conns != unsafe { nil }
}
fn (mut c H2ServerConn) read_client_preface() ! {
c.fill_at_least(h2_client_preface.len)!
got := c.rbuf[..h2_client_preface.len].bytestr()
if got != h2_client_preface {
return error('h2 server: invalid connection preface')
}
c.rbuf = c.rbuf[h2_client_preface.len..].clone()
}
fn (mut c H2ServerConn) send_initial_settings() ! {
c.send_frame(H2SettingsFrame{
settings: [
H2Setting{h2_settings_enable_push, 0},
H2Setting{h2_settings_max_concurrent_streams, h2_server_max_concurrent_streams},
H2Setting{h2_settings_initial_window_size, h2_server_default_window},
H2Setting{h2_settings_max_frame_size, h2_default_max_frame_size},
]
})!
}
fn (mut c H2ServerConn) dispatch_frame(frame H2Frame, mut handler Handler) ! {
// RFC 7540 §6.10: once a HEADERS or PUSH_PROMISE without END_HEADERS is
// seen, the next frame must be a CONTINUATION on the same stream.
if c.awaiting_cont != 0 {
if frame is H2ContinuationFrame {
if frame.stream_id != c.awaiting_cont {
return error('h2 server: CONTINUATION on the wrong stream')
}
} else {
return error('h2 server: expected CONTINUATION after HEADERS without END_HEADERS')
}
}
match frame {
H2SettingsFrame, H2PingFrame, H2WindowUpdateFrame {
c.handle_control_frame(frame)!
}
H2GoawayFrame {
c.closing = true
}
H2RstStreamFrame {
c.streams.delete(frame.stream_id)
}
H2PriorityFrame {
// Priority is advisory; ignore.
}
H2HeadersFrame {
c.on_headers(frame, mut handler)!
}
H2ContinuationFrame {
c.on_continuation(frame, mut handler)!
}
H2DataFrame {
c.on_data(frame, mut handler)!
}
H2PushPromiseFrame {
// Clients should not push. Treat as a protocol error.
return error('h2 server: PUSH_PROMISE from client')
}
H2UnknownFrame {
// RFC 7540 §4.1: ignore unknown frame types.
}
}
}
// handle_control_frame services SETTINGS, PING, and WINDOW_UPDATE frames that
// may arrive at any point in the session, including while a response write is
// blocked in send_body waiting for flow-control credit. Both dispatch_frame and
// pump_for_window delegate to this function so the logic — and any validation
// errors — exist in exactly one place.
fn (mut c H2ServerConn) handle_control_frame(frame H2Frame) ! {
match frame {
H2SettingsFrame {
if !frame.ack {
c.apply_settings(frame.settings)!
c.send_frame(H2SettingsFrame{
ack: true
})!
}
}
H2PingFrame {
if !frame.ack {
c.send_frame(H2PingFrame{
ack: true
data: frame.data
})!
}
}
H2WindowUpdateFrame {
if frame.stream_id == 0 {
c.send_window += i64(frame.window_size_increment)
} else if mut s := c.streams[frame.stream_id] {
s.send_window += i64(frame.window_size_increment)
}
}
else {}
}
}
fn (mut c H2ServerConn) 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 §6.5.3: values above 2^31-1 are a FLOW_CONTROL_ERROR,
// not a PROTOCOL_ERROR; send the correct code before unwinding.
if s.value > u32(0x7fff_ffff) {
c.send_goaway(.flow_control_error,
'SETTINGS_INITIAL_WINDOW_SIZE ${s.value} exceeds 2^31-1') or {}
return error('h2 server: SETTINGS_INITIAL_WINDOW_SIZE ${s.value} exceeds 2^31-1 (FLOW_CONTROL_ERROR)')
}
// RFC 7540 §6.9.2: a change to the initial window size adjusts
// the send window of every active stream by the delta.
delta := i64(s.value) - i64(c.peer.initial_window_size)
c.peer.initial_window_size = s.value
for _, mut st in c.streams {
st.send_window += delta
}
}
h2_settings_max_frame_size {
// RFC 7540 §6.5.2: valid range is 2^14 (16384)..2^24-1 inclusive.
if s.value < h2_default_max_frame_size || s.value > h2_max_max_frame_size {
return error('h2 server: SETTINGS_MAX_FRAME_SIZE ${s.value} out of range [16384, 16777215]')
}
c.peer.max_frame_size = s.value
}
h2_settings_max_header_list_size {
c.peer.max_header_list_size = s.value
}
else {}
}
}
}
fn (mut c H2ServerConn) on_headers(frame H2HeadersFrame, mut handler Handler) ! {
// Stream ids from the client must be odd and strictly increasing.
if frame.stream_id & 1 == 0 || frame.stream_id <= c.last_stream_id {
return error('h2 server: invalid client stream id ${frame.stream_id}')
}
c.last_stream_id = frame.stream_id
mut s := &H2ServerStream{
id: frame.stream_id
hpack_block: frame.fragment.clone()
end_headers: frame.end_headers
end_stream: frame.end_stream
send_window: i64(c.peer.initial_window_size)
}
c.streams[frame.stream_id] = s
if !frame.end_headers {
c.awaiting_cont = frame.stream_id
return
}
c.finalize_headers(mut s, mut handler)!
}
fn (mut c H2ServerConn) on_continuation(frame H2ContinuationFrame, mut handler Handler) ! {
mut s := c.streams[frame.stream_id] or {
return error('h2 server: CONTINUATION for unknown stream ${frame.stream_id}')
}
s.hpack_block << frame.fragment
if frame.end_headers {
s.end_headers = true
c.awaiting_cont = 0
c.finalize_headers(mut s, mut handler)!
}
}
fn (mut c H2ServerConn) finalize_headers(mut s H2ServerStream, mut handler Handler) ! {
s.headers = c.decoder.decode(s.hpack_block) or {
c.send_rst_stream(s.id, .compression_error)!
c.streams.delete(s.id)
return
}
s.headers_done = true
if s.end_stream {
c.run_request(mut s, mut handler)!
}
}
fn (mut c H2ServerConn) on_data(frame H2DataFrame, mut handler Handler) ! {
mut s := c.streams[frame.stream_id] or {
// DATA for an unknown stream (likely already RST'd); just drop and
// keep flow control consistent.
if frame.data.len > 0 {
c.send_window_update(0, u32(frame.data.len))!
}
return
}
if !s.headers_done {
return error('h2 server: DATA before END_HEADERS')
}
if frame.data.len > 0 {
if s.body.len + frame.data.len > h2_server_max_request_body {
c.send_rst_stream(s.id, .refused_stream)!
c.streams.delete(s.id)
return
}
s.body << frame.data
// Replenish the connection window; per-stream we replenish on
// completion since we hold the body in memory.
c.send_window_update(0, u32(frame.data.len))!
c.send_window_update(s.id, u32(frame.data.len))!
}
if frame.end_stream {
s.end_stream = true
c.run_request(mut s, mut handler)!
}
}
fn (mut c H2ServerConn) run_request(mut s H2ServerStream, mut handler Handler) ! {
req := c.build_request(s) or {
c.send_rst_stream(s.id, .protocol_error)!
c.streams.delete(s.id)
return
}
resp := handler.handle(req)
c.send_response(s.id, resp)!
c.streams.delete(s.id)
}
fn (mut c H2ServerConn) build_request(s &H2ServerStream) !Request {
mut req := Request{
version: .v2_0
header: new_header()
}
mut method := ''
mut path := ''
mut authority := ''
mut scheme := 'https'
for f in s.headers {
match f.name {
':method' {
method = f.value
}
':path' {
path = f.value
}
':authority' {
authority = f.value
}
':scheme' {
scheme = f.value
}
else {
if f.name.starts_with(':') {
return error('h2 server: unknown pseudo-header ${f.name}')
}
req.header.add_custom(f.name, f.value) or {}
}
}
}
if method == '' || path == '' {
return error('h2 server: missing :method or :path')
}
req.method = method_from_str(method)
if authority != '' && !req.header.contains(.host) {
req.header.add(.host, authority)
}
// Match the HTTP/1.1 path: req.url is the request-target (the :path
// pseudo-header), so handlers see the same shape on both transports.
_ = scheme // :scheme is parsed and discarded; handlers infer it from Host
req.url = path
req.data = s.body.bytestr()
req.host = authority
return req
}
fn (mut c H2ServerConn) send_response(stream_id u32, resp Response) ! {
status := if resp.status_code == 0 { 200 } else { resp.status_code }
mut fields := [H2HeaderField{':status', status.str()}]
for key in resp.header.keys() {
lkey := key.to_lower()
// Drop hop-by-hop headers; HTTP/2 forbids them (RFC 7540 §8.1.2.2).
if lkey in ['connection', 'keep-alive', 'transfer-encoding', 'upgrade', 'proxy-connection'] {
continue
}
for val in resp.header.custom_values(key) {
fields << H2HeaderField{lkey, val}
}
}
body := resp.body.bytes()
has_body := body.len > 0
block := c.encoder.encode(fields)
c.send_header_block(stream_id, block, !has_body)!
if has_body {
c.send_body(stream_id, body)!
}
}
fn (mut c H2ServerConn) 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
}
}
fn (mut c H2ServerConn) send_body(stream_id u32, body []u8) ! {
max := int(c.peer.max_frame_size)
mut off := 0
for off < body.len {
// Respect both the connection and per-stream send windows
// (RFC 7540 Section 6.9). When either is exhausted, read frames until
// the peer grows a window with WINDOW_UPDATE.
for c.send_window <= 0 || c.stream_send_window(stream_id) <= 0 {
c.pump_for_window(stream_id)!
}
avail := if c.send_window < c.stream_send_window(stream_id) {
c.send_window
} else {
c.stream_send_window(stream_id)
}
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)
if mut s := c.streams[stream_id] {
s.send_window -= i64(chunk)
}
off = next
}
}
// stream_send_window returns the current per-stream send window, or 0 if the
// stream is gone.
fn (c &H2ServerConn) stream_send_window(stream_id u32) i64 {
if s := c.streams[stream_id] {
return s.send_window
}
return 0
}
// pump_for_window reads one frame while a response is blocked on flow control,
// servicing control frames (SETTINGS / PING / WINDOW_UPDATE) via
// handle_control_frame and aborting on RST_STREAM for the active stream.
fn (mut c H2ServerConn) pump_for_window(stream_id u32) ! {
frame := c.read_frame()!
c.handle_control_frame(frame)!
if frame is H2RstStreamFrame {
if frame.stream_id == stream_id {
return error('h2 server: stream reset by peer while writing response')
}
}
// With SETTINGS_MAX_CONCURRENT_STREAMS=1 no other stream frames are
// expected mid-response; ignore anything else defensively.
}
fn (mut c H2ServerConn) send_window_update(stream_id u32, inc u32) ! {
if inc == 0 {
return
}
c.send_frame(H2WindowUpdateFrame{
stream_id: stream_id
window_size_increment: inc
})!
}
fn (mut c H2ServerConn) send_rst_stream(stream_id u32, code H2ErrorCode) ! {
c.send_frame(H2RstStreamFrame{
stream_id: stream_id
error_code: u32(code)
})!
}
fn (mut c H2ServerConn) send_goaway(code H2ErrorCode, msg string) ! {
c.send_frame(H2GoawayFrame{
last_stream_id: c.last_stream_id
error_code: u32(code)
debug_data: msg.bytes()
})!
c.closing = true
}
fn (mut c H2ServerConn) 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 server: 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
}
fn (mut c H2ServerConn) 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 server: connection closed by peer')
}
c.rbuf << tmp[..got]
}
}
fn (mut c H2ServerConn) send_frame(f H2Frame) ! {
c.write_all(f.encode())!
}
fn (mut c H2ServerConn) write_all(data []u8) ! {
mut sent := 0
for sent < data.len {
n := c.transport.write(data[sent..])!
if n <= 0 {
return error('h2 server: transport write returned ${n}')
}
sent += n
}
}