// 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 { // Constrains our *encoder* (the server's response-header encoder // whose output the client decodes with its advertised table size). c.peer.header_table_size = s.value c.encoder.dyn_table.set_max_size(int(s.value)) c.encoder.pending_max_table_size = int(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: validate ALL stream windows before mutating any // so that a delta overflow on stream N does not leave streams 1..N-1 // partially updated. Send FLOW_CONTROL_ERROR (not PROTOCOL_ERROR). delta := i64(s.value) - i64(c.peer.initial_window_size) for _, st in c.streams { if st.send_window + delta > i64(0x7fff_ffff) { c.send_goaway(.flow_control_error, 'SETTINGS_INITIAL_WINDOW_SIZE delta overflows stream ${st.id} send window') or {} return error('h2 server: SETTINGS_INITIAL_WINDOW_SIZE delta overflows stream ${st.id} send window (RFC 7540 §6.9.2 FLOW_CONTROL_ERROR)') } } 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}') } if s.hpack_block.len + frame.fragment.len > h2_max_recv_header_block { return error('h2 server: request header block exceeds ${h2_max_recv_header_block} bytes') } 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. Credit flow_size (full wire bytes // including padding) per RFC 7540 §6.9.1. if frame.flow_size > 0 { c.send_window_update(0, u32(frame.flow_size))! } 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 { // Credit the connection window before resetting so the peer is // not penalised for bytes it legitimately sent within its window. c.send_window_update(0, u32(frame.flow_size)) or {} c.send_rst_stream(s.id, .refused_stream)! c.streams.delete(s.id) return } s.body << frame.data } // Replenish flow control using flow_size (full wire payload including padding), // per RFC 7540 §6.9.1. Credit unconditionally when flow_size>0: a padding-only // DATA frame (data.len==0, flow_size>0) still consumes the peer's send window // and must be credited back. (Formerly inside the data.len>0 block — padding-only // frames leaked window silently.) if frame.flow_size > 0 { c.send_window_update(0, u32(frame.flow_size))! c.send_window_update(s.id, u32(frame.flow_size))! } 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)! // Check against our own advertised receive limit (sent in send_initial_settings). // H2ServerConn always advertises h2_default_max_frame_size and never renegotiates // it, so the constant is correct here. c.peer.max_frame_size is the client's // receive limit (our outbound cap) and must not be used for inbound checking. 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 } }