Skip to main content

nodejs/stdlib/
http2.rs

1//! Node `http2` module: a REAL, minimal HTTP/2 server over TLS+ALPN.
2//!
3//! This is a genuine HTTP/2 server (RFC 7540 framing + RFC 7541 HPACK via the
4//! `hpack` crate) layered on the same blocking-`rustls` model as `tls`/`https`.
5//! A real h2 client (`curl --http2`, `nghttp`, Node's `http2` client) can perform
6//! a GET and receive a response body from a server built with
7//! `http2.createSecureServer`.
8//!
9//! ── Threading model (identical discipline to `net`/`tls`) ────────────────────
10//! Background threads NEVER touch the JS heap (a main-thread `thread_local`). One
11//! thread per TCP connection owns the `rustls::StreamOwned`, performs the TLS
12//! handshake (negotiating ALPN `h2`), then runs the full HTTP/2 framing loop:
13//! reading/parsing frames and writing response frames. HPACK `Encoder`/`Decoder`
14//! (with their connection-scoped dynamic tables) live on that thread. Every
15//! JS-visible effect — building the `Http2Stream`/`Http2Session` objects, emitting
16//! `stream`/`session`/`request`, running listeners — happens on the main thread
17//! via posted `IoTask` closures. The stream objects talk back to their connection
18//! thread through an `mpsc` channel of `H2Cmd`s (`respond`/`write`/`end` enqueue a
19//! command; the owner thread encodes+writes the frame), so reads and writes stay
20//! on the single thread that owns the stateful TLS + HPACK cipher/table state.
21//!
22//! ── What IS implemented (server) ─────────────────────────────────────────────
23//!   * TLS handshake with ALPN offering only `h2`; non-`h2` clients are closed.
24//!   * Client connection preface check (`PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n`).
25//!   * SETTINGS: we send our (empty, valid) SETTINGS; we ACK the client's SETTINGS
26//!     and ignore incoming SETTINGS ACKs.
27//!   * HEADERS (type 1) inbound: PADDED + PRIORITY flags stripped, header block
28//!     HPACK-decoded into pseudo-headers (`:method`/`:path`/`:scheme`/`:authority`)
29//!     + regular headers. Fires `stream` (and compat `request`).
30//!   * DATA (type 0) inbound: emitted on the stream as `data`/`end` (request body).
31//!   * `Http2Stream.respond(headers)` → HPACK-encoded HEADERS with `:status`.
32//!   * `Http2Stream.write(data)` / `.end([data])` → DATA frames (END_STREAM on end),
33//!     chunked to <= 16384 (the default SETTINGS_MAX_FRAME_SIZE) per frame.
34//!   * PING (type 6): answered with a PING ACK echoing the 8-byte payload.
35//!   * WINDOW_UPDATE (type 8) / PRIORITY (type 2) / RST_STREAM (type 3): accepted
36//!     and ignored (large-enough default windows; see caveats).
37//!   * GOAWAY (type 7): sent on connection close; inbound GOAWAY ends the loop.
38//!
39//! ── Honest limitations (NOT implemented — deliberately, not faked) ────────────
40//!   * `http2.connect` (CLIENT) is NOT implemented — it throws a clear error.
41//!   * `http2.createServer` (cleartext h2c / prior-knowledge) is NOT implemented.
42//!   * CONTINUATION frames (type 9): a HEADERS frame WITHOUT END_HEADERS is not
43//!     reassembled — that stream is skipped. Real clients pack a single small GET
44//!     header block into one HEADERS frame, so this rarely triggers, but a very
45//!     large request header block would be dropped rather than mis-parsed.
46//!   * Flow control is minimal: we advertise/assume the default windows and IGNORE
47//!     inbound WINDOW_UPDATE for our own send side. A response body larger than the
48//!     peer's stream/connection window (65535 bytes by default) can STALL. Small
49//!     responses (the common case, and the test target) are unaffected.
50//!   * No server push, no trailers, no PRIORITY tree, no per-stream RST bookkeeping,
51//!     no ALTSVC/ORIGIN. These are absent, never stubbed to look present.
52//!
53//! ── Handler-fault isolation (why the server used to close right after HEADERS) ─
54//! The event loop treats an `Err` returned from a posted `IoTask` as FATAL
55//! (`host::drive_event_loop` runs `task()?`), which terminates the whole process
56//! and closes every live socket. `events::emit` propagates a listener's error
57//! (`invoke(&f, …)?`). So an exception thrown by the user's `stream`/`request`
58//! handler used to bubble out of the connection's `on_headers` IoTask and kill the
59//! server the instant the first request arrived — the client sees the TCP close as
60//! a "broken pipe" with no response. To prevent one faulty handler from taking
61//! down the server, the connection IoTasks (`on_headers`/`on_data`/`on_session`)
62//! CATCH handler errors, print them to stderr (like Node's uncaught-exception
63//! output, so the cause is visible), and return `Ok` — the loop and other
64//! connections survive. Set `HTTP2_DEBUG=1` to trace every frame in/out on stderr.
65//!
66//! ── Verification status ──────────────────────────────────────────────────────
67//! The framing (9-octet header layout, big-endian 24-bit length, frame type/flag
68//! values, HPACK usage) is written to RFC 7540/7541. It has NOT been compiled or
69//! run in this session (the parent owns the shared build), so the byte-level
70//! correctness is verified-by-construction against the RFCs, not by a live curl.
71//! See the final report for the exact `curl --http2` command to confirm it.
72
73use crate::host::{invoke, with_host, IoTask, JsObj};
74use fusevm::Value;
75use hpack::{Decoder, Encoder};
76use indexmap::IndexMap;
77use rustls::pki_types::{CertificateDer, PrivateKeyDer};
78use rustls::{ServerConfig, ServerConnection, StreamOwned};
79use std::collections::HashMap;
80use std::io::{Read, Write};
81use std::net::{TcpListener, TcpStream};
82use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
83use std::sync::mpsc::Sender;
84use std::sync::Arc;
85
86/// `http2` module functions routed through `stdlib::call`.
87pub const METHODS: &[&str] = &[
88    "createSecureServer",
89    "createServer",
90    "connect",
91    "getDefaultSettings",
92    "getPackedSettings",
93    "getUnpackedSettings",
94];
95
96/// Instance method names for the `@@native` tags this module owns (exposed to
97/// `stdlib::instance_has_method` so a method *read* yields a bound method).
98pub const SERVER_METHODS: &[&str] = &["listen", "close", "address", "setTimeout"];
99pub const STREAM_METHODS: &[&str] = &[
100    "respond",
101    "write",
102    "end",
103    "close",
104    "setEncoding",
105    "setTimeout",
106    "pause",
107    "resume",
108    // HTTP/1-compat surface (Http2ServerResponse), best-effort:
109    "writeHead",
110    "setHeader",
111    "getHeader",
112    "removeHeader",
113];
114pub const SESSION_METHODS: &[&str] = &[
115    "settings",
116    "ping",
117    "goaway",
118    "close",
119    "destroy",
120    "ref",
121    "unref",
122    "setTimeout",
123];
124
125// ── HTTP/2 constants (RFC 7540 §6, §11) ──────────────────────────────────────
126
127const PREFACE: &[u8] = b"PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n";
128
129// Frame types.
130const FT_DATA: u8 = 0x0;
131const FT_HEADERS: u8 = 0x1;
132const FT_PRIORITY: u8 = 0x2;
133const FT_RST_STREAM: u8 = 0x3;
134const FT_SETTINGS: u8 = 0x4;
135const FT_PING: u8 = 0x6;
136const FT_GOAWAY: u8 = 0x7;
137const FT_WINDOW_UPDATE: u8 = 0x8;
138const FT_CONTINUATION: u8 = 0x9;
139
140// Frame flags.
141const FL_END_STREAM: u8 = 0x1;
142const FL_ACK: u8 = 0x1; // SETTINGS/PING re-use bit 0 as ACK
143const FL_END_HEADERS: u8 = 0x4;
144const FL_PADDED: u8 = 0x8;
145const FL_PRIORITY: u8 = 0x20;
146
147/// Default SETTINGS_MAX_FRAME_SIZE — the largest DATA payload we emit per frame.
148const MAX_FRAME_SIZE: usize = 16384;
149
150// ── process-global id sources (ids minted on background threads) ─────────────
151
152static NEXT_SERVER_ID: AtomicU64 = AtomicU64::new(1);
153static NEXT_STREAM_KEY: AtomicU64 = AtomicU64::new(1);
154static NEXT_SESSION_KEY: AtomicU64 = AtomicU64::new(1);
155
156fn next_server_id() -> u64 {
157    NEXT_SERVER_ID.fetch_add(1, Ordering::Relaxed)
158}
159fn next_stream_key() -> u64 {
160    NEXT_STREAM_KEY.fetch_add(1, Ordering::Relaxed)
161}
162fn next_session_key() -> u64 {
163    NEXT_SESSION_KEY.fetch_add(1, Ordering::Relaxed)
164}
165
166// ── commands from the main thread to a connection's owner thread ─────────────
167
168/// A frame the owner thread should encode + write, produced by JS method calls.
169enum H2Cmd {
170    /// Send a HEADERS frame carrying the given (already ordered, `:status`-first)
171    /// header list; `end` sets END_STREAM (a header-only response).
172    Respond {
173        stream_id: u32,
174        headers: Vec<(String, String)>,
175        end: bool,
176    },
177    /// Send DATA (chunked to MAX_FRAME_SIZE); `end` sets END_STREAM on the last.
178    Data {
179        stream_id: u32,
180        data: Vec<u8>,
181        end: bool,
182    },
183    /// RST_STREAM with NO_ERROR.
184    Close { stream_id: u32 },
185    /// GOAWAY(NO_ERROR) then terminate the owner loop.
186    Goaway,
187}
188
189// ── main-thread state ────────────────────────────────────────────────────────
190
191struct H2ServerRec {
192    emitter: Value,
193    stop: Arc<AtomicBool>,
194}
195
196struct H2StreamRec {
197    emitter: Value,
198    tx: Sender<H2Cmd>,
199    stream_id: u32,
200    /// True once a HEADERS response frame has been sent (respond/writeHead).
201    responded: bool,
202}
203
204struct H2SessionRec {
205    #[allow(dead_code)]
206    emitter: Value,
207    tx: Sender<H2Cmd>,
208}
209
210#[derive(Default)]
211struct H2State {
212    servers: HashMap<u64, H2ServerRec>,
213    streams: HashMap<u64, H2StreamRec>,
214    sessions: HashMap<u64, H2SessionRec>,
215}
216
217thread_local! {
218    static H2: std::cell::RefCell<H2State> = std::cell::RefCell::new(H2State::default());
219    /// `ServerConfig`s built by `createSecureServer` before `listen` assigns an id
220    /// (mirrors `tls::PENDING_CONFIGS`).
221    static PENDING_CONFIGS: std::cell::RefCell<Vec<(Value, Arc<ServerConfig>)>> =
222        const { std::cell::RefCell::new(Vec::new()) };
223}
224
225// ── shared object / prop helpers (same shape as net/tls) ─────────────────────
226
227fn get_prop(recv: &Value, key: &str) -> Option<Value> {
228    with_host(|h| match h.get(recv) {
229        Some(JsObj::Object(p)) => p.get(key).cloned(),
230        _ => None,
231    })
232}
233
234fn set_prop(recv: &Value, key: &str, val: Value) {
235    with_host(|h| {
236        if let Some(JsObj::Object(p)) = h.get_mut(recv) {
237            p.insert(key.to_string(), val);
238        }
239    });
240}
241
242fn u64_prop(recv: &Value, key: &str) -> Option<u64> {
243    get_prop(recv, key).map(|v| with_host(|h| h.to_number(&v)) as u64)
244}
245
246/// Delegate the EventEmitter surface to `events`; `None` for a non-emitter method.
247fn emitter_dispatch(recv: &Value, method: &str, args: &[Value]) -> Option<Result<Value, String>> {
248    match method {
249        "on"
250        | "addListener"
251        | "prependListener"
252        | "once"
253        | "prependOnceListener"
254        | "emit"
255        | "removeListener"
256        | "off"
257        | "removeAllListeners"
258        | "listenerCount"
259        | "eventNames"
260        | "setMaxListeners"
261        | "getMaxListeners"
262        | "listeners" => Some(super::events::instance_call(recv, method, args.to_vec())),
263        _ => None,
264    }
265}
266
267/// Raw bytes of a `write`/`end`/`key`/`cert` argument (Buffer bytes or UTF-8).
268fn value_bytes(v: Option<&Value>) -> Vec<u8> {
269    let Some(v) = v else { return Vec::new() };
270    let is_buffer =
271        with_host(|h| matches!(h.get(v), Some(JsObj::Object(p)) if p.contains_key("@@bytes")));
272    if is_buffer {
273        return with_host(|h| match h.get(v) {
274            Some(JsObj::Object(p)) => match p.get("@@bytes").and_then(|b| h.get(b)) {
275                Some(JsObj::Array(items)) => items.iter().map(|x| h.to_number(x) as u8).collect(),
276                _ => Vec::new(),
277            },
278            _ => Vec::new(),
279        });
280    }
281    with_host(|h| h.str_of(v)).into_bytes()
282}
283
284/// Enumerable string key/value pairs of a plain object (for a `respond`/`writeHead`
285/// headers argument). Hidden `@@`/`#` keys are skipped.
286fn object_pairs(obj: &Value) -> Vec<(String, String)> {
287    with_host(|h| match h.get(obj) {
288        Some(JsObj::Object(p)) => p
289            .iter()
290            .filter(|(k, _)| !k.starts_with("@@") && !k.starts_with('#'))
291            .map(|(k, v)| (k.clone(), h.str_of(v)))
292            .collect(),
293        _ => Vec::new(),
294    })
295}
296
297// ── module entry ─────────────────────────────────────────────────────────────
298
299/// `stdlib::call` entry for `http2.<method>`.
300pub fn call(method: &str, args: &[Value]) -> Option<Result<Value, String>> {
301    Some(match method {
302        "createSecureServer" => create_secure_server(args),
303        "createServer" => Err(
304            "Error: http2.createServer (cleartext h2c / prior-knowledge) \
305            is not implemented in node-js; use http2.createSecureServer (h2 over TLS)"
306                .to_string(),
307        ),
308        "connect" => Err(
309            "Error: http2.connect (HTTP/2 client) is not implemented in node-js; \
310            only the HTTP/2 server (http2.createSecureServer) is implemented"
311                .to_string(),
312        ),
313        "getDefaultSettings" => Ok(default_settings_object()),
314        "getPackedSettings" => Ok(pack_settings(args)),
315        "getUnpackedSettings" => unpack_settings(args),
316        _ => return None,
317    })
318}
319
320/// `http2.constants` and other non-function properties, reachable via
321/// `namespace_property` → `stdlib::constant`.
322pub fn constant(name: &str) -> Option<Value> {
323    match name {
324        "constants" => Some(constants_object()),
325        // Expose ctor namespaces so `http2.Http2ServerRequest.prototype` etc. resolve
326        // to *something* (they are not separately constructable here).
327        "Http2ServerRequest" => Some(with_host(|h| {
328            h.alloc(JsObj::Builtin("Http2ServerRequest".into()))
329        })),
330        "Http2ServerResponse" => Some(with_host(|h| {
331            h.alloc(JsObj::Builtin("Http2ServerResponse".into()))
332        })),
333        _ => None,
334    }
335}
336
337/// The `http2.constants` object (a representative subset of RFC 7540 / nghttp2
338/// names — HTTP status codes, header-name constants, and error/settings codes).
339fn constants_object() -> Value {
340    with_host(|h| {
341        let mut m = IndexMap::new();
342        let put_i = |m: &mut IndexMap<String, Value>, k: &str, v: i64| {
343            m.insert(k.to_string(), Value::Float(v as f64));
344        };
345        // Common HTTP status constants.
346        put_i(&mut m, "HTTP_STATUS_OK", 200);
347        put_i(&mut m, "HTTP_STATUS_NO_CONTENT", 204);
348        put_i(&mut m, "HTTP_STATUS_MOVED_PERMANENTLY", 301);
349        put_i(&mut m, "HTTP_STATUS_FOUND", 302);
350        put_i(&mut m, "HTTP_STATUS_NOT_MODIFIED", 304);
351        put_i(&mut m, "HTTP_STATUS_BAD_REQUEST", 400);
352        put_i(&mut m, "HTTP_STATUS_UNAUTHORIZED", 401);
353        put_i(&mut m, "HTTP_STATUS_FORBIDDEN", 403);
354        put_i(&mut m, "HTTP_STATUS_NOT_FOUND", 404);
355        put_i(&mut m, "HTTP_STATUS_INTERNAL_SERVER_ERROR", 500);
356        // NGHTTP2 error codes (RFC 7540 §7).
357        put_i(&mut m, "NGHTTP2_NO_ERROR", 0x0);
358        put_i(&mut m, "NGHTTP2_PROTOCOL_ERROR", 0x1);
359        put_i(&mut m, "NGHTTP2_INTERNAL_ERROR", 0x2);
360        put_i(&mut m, "NGHTTP2_FLOW_CONTROL_ERROR", 0x3);
361        put_i(&mut m, "NGHTTP2_SETTINGS_TIMEOUT", 0x4);
362        put_i(&mut m, "NGHTTP2_STREAM_CLOSED", 0x5);
363        put_i(&mut m, "NGHTTP2_FRAME_SIZE_ERROR", 0x6);
364        put_i(&mut m, "NGHTTP2_REFUSED_STREAM", 0x7);
365        put_i(&mut m, "NGHTTP2_CANCEL", 0x8);
366        put_i(&mut m, "NGHTTP2_COMPRESSION_ERROR", 0x9);
367        put_i(&mut m, "NGHTTP2_ENHANCE_YOUR_CALM", 0xb);
368        // SETTINGS identifiers (RFC 7540 §6.5.2).
369        put_i(&mut m, "NGHTTP2_SETTINGS_HEADER_TABLE_SIZE", 0x1);
370        put_i(&mut m, "NGHTTP2_SETTINGS_ENABLE_PUSH", 0x2);
371        put_i(&mut m, "NGHTTP2_SETTINGS_MAX_CONCURRENT_STREAMS", 0x3);
372        put_i(&mut m, "NGHTTP2_SETTINGS_INITIAL_WINDOW_SIZE", 0x4);
373        put_i(&mut m, "NGHTTP2_SETTINGS_MAX_FRAME_SIZE", 0x5);
374        put_i(&mut m, "NGHTTP2_SETTINGS_MAX_HEADER_LIST_SIZE", 0x6);
375        // A representative subset of the HTTP2_HEADER_* string constants.
376        let hdr =
377            |m: &mut IndexMap<String, Value>, k: &str, v: &str, h: &mut crate::host::JsHost| {
378                let s = h.new_str(v);
379                m.insert(k.to_string(), s);
380            };
381        hdr(&mut m, "HTTP2_HEADER_STATUS", ":status", h);
382        hdr(&mut m, "HTTP2_HEADER_METHOD", ":method", h);
383        hdr(&mut m, "HTTP2_HEADER_AUTHORITY", ":authority", h);
384        hdr(&mut m, "HTTP2_HEADER_SCHEME", ":scheme", h);
385        hdr(&mut m, "HTTP2_HEADER_PATH", ":path", h);
386        hdr(&mut m, "HTTP2_HEADER_CONTENT_TYPE", "content-type", h);
387        hdr(&mut m, "HTTP2_HEADER_CONTENT_LENGTH", "content-length", h);
388        hdr(&mut m, "HTTP2_METHOD_GET", "GET", h);
389        hdr(&mut m, "HTTP2_METHOD_POST", "POST", h);
390        h.new_object(m)
391    })
392}
393
394/// A minimal default-settings object (Node's `http2.getDefaultSettings()` shape).
395fn default_settings_object() -> Value {
396    with_host(|h| {
397        let mut m = IndexMap::new();
398        m.insert("headerTableSize".into(), Value::Float(4096.0));
399        m.insert("enablePush".into(), Value::Bool(false));
400        m.insert("initialWindowSize".into(), Value::Float(65535.0));
401        m.insert("maxFrameSize".into(), Value::Float(MAX_FRAME_SIZE as f64));
402        m.insert("maxConcurrentStreams".into(), Value::Float(100.0));
403        h.new_object(m)
404    })
405}
406
407// ── getPackedSettings / getUnpackedSettings (RFC 7540 §6.5.1 wire form) ───────
408
409/// Append one SETTINGS entry (2-byte identifier + 4-byte value, big-endian).
410fn push_setting(out: &mut Vec<u8>, id: u16, val: u32) {
411    out.extend_from_slice(&id.to_be_bytes());
412    out.extend_from_slice(&val.to_be_bytes());
413}
414
415/// `http2.getPackedSettings(settings)` — serialize the SETTINGS values present on
416/// `settings` into the RFC 7540 §6.5.1 wire form (6 octets per entry: a 2-byte
417/// identifier followed by a 4-byte value, big-endian). Only keys actually present
418/// on the object are emitted, in the canonical identifier order (1..6) Node uses;
419/// this is the exact inverse of `getUnpackedSettings`.
420fn pack_settings(args: &[Value]) -> Value {
421    let settings = args.first().cloned().unwrap_or(Value::Undef);
422    let num = |key: &str| -> Option<u32> {
423        get_prop(&settings, key)
424            .filter(|v| !matches!(v, Value::Undef))
425            .map(|v| with_host(|h| h.to_number(&v)) as u32)
426    };
427    let mut out: Vec<u8> = Vec::new();
428    if let Some(v) = num("headerTableSize") {
429        push_setting(&mut out, 0x1, v);
430    }
431    if let Some(p) = get_prop(&settings, "enablePush").filter(|v| !matches!(v, Value::Undef)) {
432        let on = with_host(|h| h.truthy(&p));
433        push_setting(&mut out, 0x2, u32::from(on));
434    }
435    if let Some(v) = num("maxConcurrentStreams") {
436        push_setting(&mut out, 0x3, v);
437    }
438    if let Some(v) = num("initialWindowSize") {
439        push_setting(&mut out, 0x4, v);
440    }
441    if let Some(v) = num("maxFrameSize") {
442        push_setting(&mut out, 0x5, v);
443    }
444    if let Some(v) = num("maxHeaderListSize").or_else(|| num("maxHeaderSize")) {
445        push_setting(&mut out, 0x6, v);
446    }
447    super::buffer::from_bytes(&out)
448}
449
450/// `http2.getUnpackedSettings(buf)` — parse a packed SETTINGS payload (6 octets per
451/// entry: 2-byte identifier + 4-byte value, big-endian) back into a settings object
452/// (`{ headerTableSize, enablePush, maxConcurrentStreams, initialWindowSize,
453/// maxFrameSize, maxHeaderSize, maxHeaderListSize }`, only the keys present in the
454/// buffer). The inverse of `getPackedSettings`. Throws `ERR_HTTP2_INVALID_PACKED_
455/// SETTINGS_LENGTH` when the length is not a multiple of six.
456fn unpack_settings(args: &[Value]) -> Result<Value, String> {
457    let bytes = value_bytes(args.first());
458    if bytes.len() % 6 != 0 {
459        return Err("RangeError [ERR_HTTP2_INVALID_PACKED_SETTINGS_LENGTH]: \
460                    Packed settings length must be a multiple of six"
461            .to_string());
462    }
463    let mut m = IndexMap::new();
464    for chunk in bytes.chunks_exact(6) {
465        let id = u16::from_be_bytes([chunk[0], chunk[1]]);
466        let val = u32::from_be_bytes([chunk[2], chunk[3], chunk[4], chunk[5]]);
467        match id {
468            0x1 => {
469                m.insert("headerTableSize".to_string(), Value::Float(val as f64));
470            }
471            0x2 => {
472                m.insert("enablePush".to_string(), Value::Bool(val != 0));
473            }
474            0x3 => {
475                m.insert("maxConcurrentStreams".to_string(), Value::Float(val as f64));
476            }
477            0x4 => {
478                m.insert("initialWindowSize".to_string(), Value::Float(val as f64));
479            }
480            0x5 => {
481                m.insert("maxFrameSize".to_string(), Value::Float(val as f64));
482            }
483            // Identifier 6 maps to BOTH `maxHeaderSize` and its `maxHeaderListSize`
484            // alias (Node emits both, in that order).
485            0x6 => {
486                m.insert("maxHeaderSize".to_string(), Value::Float(val as f64));
487                m.insert("maxHeaderListSize".to_string(), Value::Float(val as f64));
488            }
489            // Unknown identifiers are ignored per RFC 7540 §6.5.
490            _ => {}
491        }
492    }
493    Ok(with_host(|h| h.new_object(m)))
494}
495
496// ── http2.createSecureServer ─────────────────────────────────────────────────
497
498/// `http2.createSecureServer(options[, onRequestHandler])`. Parses `key`+`cert`
499/// into a `ServerConfig` with ALPN `h2` (a bad cert throws synchronously) and
500/// returns an `Http2Server` emitter. A supplied handler is registered as a compat
501/// `request` listener (Node semantics).
502fn create_secure_server(args: &[Value]) -> Result<Value, String> {
503    let mut options: Option<Value> = None;
504    let mut handler: Option<Value> = None;
505    for a in args {
506        if with_host(|h| crate::host::is_callable(h, a)) {
507            handler = Some(a.clone());
508        } else if matches!(a, Value::Obj(_)) {
509            options = Some(a.clone());
510        }
511    }
512    let opts = options.ok_or_else(|| {
513        crate::host::type_error("http2.createSecureServer requires options with `key` and `cert`")
514    })?;
515    let cert = value_bytes(get_prop(&opts, "cert").as_ref());
516    let key = value_bytes(get_prop(&opts, "key").as_ref());
517    if cert.is_empty() || key.is_empty() {
518        return Err(crate::host::type_error(
519            "http2.createSecureServer requires `key` and `cert`",
520        ));
521    }
522    let config = build_h2_server_config(&cert, &key)?;
523
524    let server = new_emitter_object("Http2Server", IndexMap::new());
525    if let Some(cb) = handler {
526        // Node registers the createSecureServer handler as a `request` listener.
527        super::events::instance_call(&server, "on", vec![with_host(|h| h.new_str("request")), cb])?;
528    }
529    PENDING_CONFIGS.with(|p| p.borrow_mut().push((server.clone(), config)));
530    Ok(server)
531}
532
533/// Build a `ServerConfig` from PEM `key`+`cert`, offering ALPN `h2` only.
534fn build_h2_server_config(cert_pem: &[u8], key_pem: &[u8]) -> Result<Arc<ServerConfig>, String> {
535    let certs: Vec<CertificateDer<'static>> = rustls_pemfile::certs(&mut &cert_pem[..])
536        .collect::<Result<_, _>>()
537        .map_err(|e| format!("Error: http2: bad certificate PEM: {e}"))?;
538    if certs.is_empty() {
539        return Err("Error: http2: no certificates found in `cert`".to_string());
540    }
541    let key: PrivateKeyDer<'static> = rustls_pemfile::private_key(&mut &key_pem[..])
542        .map_err(|e| format!("Error: http2: bad private key PEM: {e}"))?
543        .ok_or_else(|| "Error: http2: no private key found in `key`".to_string())?;
544    let mut cfg = ServerConfig::builder()
545        .with_no_client_auth()
546        .with_single_cert(certs, key)
547        .map_err(|e| format!("Error: http2: invalid key/cert: {e}"))?;
548    // ALPN: offer only "h2" so the handshake negotiates HTTP/2.
549    cfg.alpn_protocols = vec![b"h2".to_vec()];
550    Ok(Arc::new(cfg))
551}
552
553fn take_pending_config(server: &Value) -> Option<Arc<ServerConfig>> {
554    PENDING_CONFIGS.with(|p| {
555        let mut p = p.borrow_mut();
556        p.iter()
557            .position(|(s, _)| s == server)
558            .map(|pos| p.remove(pos).1)
559    })
560}
561
562// ── instance dispatch (Http2Server / Http2Stream / Http2Session) ─────────────
563
564pub fn instance_call(
565    tag: &str,
566    recv: &Value,
567    method: &str,
568    args: Vec<Value>,
569) -> Result<Value, String> {
570    match tag {
571        "Http2Server" => server_call(recv, method, args),
572        "Http2Stream" => stream_call(recv, method, args),
573        "Http2Session" => session_call(recv, method, args),
574        _ => Err(crate::host::type_error(&format!(
575            "{method} is not a function"
576        ))),
577    }
578}
579
580fn server_call(recv: &Value, method: &str, args: Vec<Value>) -> Result<Value, String> {
581    if let Some(r) = emitter_dispatch(recv, method, &args) {
582        return r;
583    }
584    match method {
585        "listen" => server_listen(recv, &args),
586        "close" => server_close(recv, &args),
587        "address" => Ok(get_prop(recv, "@@address").unwrap_or(Value::Undef)),
588        "setTimeout" => Ok(recv.clone()),
589        _ => Err(crate::host::type_error(&format!(
590            "server.{method} is not a function"
591        ))),
592    }
593}
594
595/// `server.listen(port[, host][, callback])`. Binds on the main thread, spawns the
596/// accept loop, and fires `listening` + callback asynchronously.
597fn server_listen(recv: &Value, args: &[Value]) -> Result<Value, String> {
598    let port = with_host(|h| args.first().map(|v| h.to_number(v)).unwrap_or(0.0)) as u16;
599    let mut host = "0.0.0.0".to_string();
600    let mut cb: Option<Value> = None;
601    for a in &args[1.min(args.len())..] {
602        if with_host(|h| h.as_str(a)).is_some() {
603            host = with_host(|h| h.str_of(a));
604        } else if with_host(|h| crate::host::is_callable(h, a)) {
605            cb = Some(a.clone());
606        }
607    }
608
609    let config = take_pending_config(recv)
610        .ok_or_else(|| crate::host::type_error("http2 server has no secure context"))?;
611    let listener = TcpListener::bind((host.as_str(), port))
612        .map_err(|e| format!("Error: listen EADDRINUSE: {e}"))?;
613    let local = listener.local_addr().ok();
614
615    let id = next_server_id();
616    set_prop(recv, "@@serverid", Value::Float(id as f64));
617    if let Some(addr) = local {
618        let mut a = IndexMap::new();
619        a.insert("port".into(), Value::Float(addr.port() as f64));
620        a.insert(
621            "address".into(),
622            with_host(|h| h.new_str(addr.ip().to_string())),
623        );
624        a.insert(
625            "family".into(),
626            with_host(|h| h.new_str(if addr.is_ipv6() { "IPv6" } else { "IPv4" })),
627        );
628        let addr_obj = with_host(|h| h.new_object(a));
629        set_prop(recv, "@@address", addr_obj);
630    }
631    let stop = Arc::new(AtomicBool::new(false));
632    H2.with(|s| {
633        s.borrow_mut().servers.insert(
634            id,
635            H2ServerRec {
636                emitter: recv.clone(),
637                stop: stop.clone(),
638            },
639        );
640    });
641    with_host(|h| h.incr_handle());
642
643    let io_tx = with_host(|h| h.io_sender());
644    listener.set_nonblocking(true).ok();
645    std::thread::spawn(move || loop {
646        if stop.load(Ordering::Acquire) {
647            break;
648        }
649        match listener.accept() {
650            Ok((stream, _addr)) => {
651                let cfg = config.clone();
652                let tx = io_tx.clone();
653                std::thread::spawn(move || serve_connection(id, stream, cfg, tx));
654            }
655            Err(ref e) if e.kind() == std::io::ErrorKind::WouldBlock => {
656                std::thread::sleep(std::time::Duration::from_millis(5));
657            }
658            Err(_) => break,
659        }
660    });
661
662    let server = recv.clone();
663    let _ = with_host(|h| h.io_sender()).send(Box::new(move || {
664        super::events::instance_call(&server, "emit", vec![with_host(|h| h.new_str("listening"))])?;
665        if let Some(cb) = cb {
666            invoke(&cb, Vec::new(), None)?;
667        }
668        Ok(())
669    }));
670    Ok(recv.clone())
671}
672
673fn server_close(recv: &Value, args: &[Value]) -> Result<Value, String> {
674    if let Some(id) = u64_prop(recv, "@@serverid") {
675        let rec = H2.with(|s| s.borrow_mut().servers.remove(&id));
676        if let Some(rec) = rec {
677            rec.stop.store(true, Ordering::Release);
678            with_host(|h| h.decr_handle());
679            let _ = with_host(|h| h.io_sender()).send(Box::new(|| Ok(())));
680        }
681    }
682    if let Some(cb) = args
683        .first()
684        .filter(|v| with_host(|h| crate::host::is_callable(h, v)))
685    {
686        invoke(cb, Vec::new(), None)?;
687    }
688    super::events::instance_call(recv, "emit", vec![with_host(|h| h.new_str("close"))])?;
689    Ok(recv.clone())
690}
691
692// ── the per-connection owner thread: TLS + HTTP/2 framing ────────────────────
693
694/// One background thread per accepted TCP connection: complete the TLS handshake
695/// (negotiating ALPN `h2`), then run the HTTP/2 framing loop. Owns the
696/// `StreamOwned` and the HPACK `Encoder`/`Decoder` for the whole connection.
697fn serve_connection(
698    server_id: u64,
699    mut sock: TcpStream,
700    config: Arc<ServerConfig>,
701    io_tx: Sender<IoTask>,
702) {
703    // The socket is inherited non-blocking from the accept poll loop; put it in
704    // blocking mode so the TLS handshake and the initial SETTINGS write can't
705    // fail with WouldBlock. A per-read timeout (set later) gives the framing loop
706    // its read/write interleave without making writes non-blocking.
707    sock.set_nonblocking(false).ok();
708    let mut conn = match ServerConnection::new(config) {
709        Ok(c) => c,
710        Err(_) => return,
711    };
712    if conn.complete_io(&mut sock).is_err() {
713        return;
714    }
715    // ALPN must have negotiated "h2"; otherwise this is not an HTTP/2 connection.
716    let is_h2 = conn.alpn_protocol().map(|p| p == b"h2").unwrap_or(false);
717    let mut stream = StreamOwned::new(conn, sock);
718    if !is_h2 {
719        // Honest fallback: we only speak h2. Close the connection.
720        stream.conn.send_close_notify();
721        let _ = stream.flush();
722        let _ = stream.sock.shutdown(std::net::Shutdown::Both);
723        return;
724    }
725    // Command channel: main-thread stream/session methods post frames to write.
726    let (tx, rx) = std::sync::mpsc::channel::<H2Cmd>();
727
728    // Announce the session on the main thread.
729    let session_key = next_session_key();
730    {
731        let tx_sess = tx.clone();
732        let _ = io_tx.send(Box::new(move || {
733            on_session(server_id, session_key, tx_sess)
734        }));
735    }
736
737    if h2_debug() {
738        eprintln!("[http2] connection {session_key}: ALPN h2 negotiated, starting framing loop");
739    }
740
741    // Send our SETTINGS immediately (empty payload is valid). This runs in
742    // BLOCKING mode — the read timeout is set AFTERWARDS, because rustls's write
743    // internally completes pending handshake I/O (a read), and a read-timeout set
744    // beforehand makes that read WouldBlock → the write fails → the loop never
745    // starts (the connection-startup race).
746    let mut ok = true;
747    ok &= write_frame(&mut stream, FT_SETTINGS, 0, 0, &[]).is_ok();
748    ok &= stream.flush().is_ok();
749    // Now switch to a short read timeout so the loop interleaves reads with the
750    // outbound-command drain.
751    stream
752        .sock
753        .set_read_timeout(Some(std::time::Duration::from_millis(20)))
754        .ok();
755
756    let mut decoder = Decoder::new();
757    let mut encoder = Encoder::new();
758    let mut inbuf: Vec<u8> = Vec::new();
759    let mut got_preface = false;
760    let mut max_stream_id: u32 = 0;
761    // Map stream-id → main-thread stream key (for routing inbound DATA).
762    let mut id_to_key: HashMap<u32, u64> = HashMap::new();
763    let mut buf = [0u8; MAX_FRAME_SIZE];
764
765    'conn: loop {
766        // If the initial SETTINGS write failed the connection is already dead;
767        // fall through to the GOAWAY/close cleanup below. (`ok` is only ever set
768        // before the loop, so this is the entry gate, not a per-iteration test.)
769        if !ok {
770            break;
771        }
772        // 1) Drain queued outbound commands (respond/write/end frames enqueued by
773        //    the main thread). This runs on EVERY loop iteration — including after
774        //    a WouldBlock read — so async responses are written promptly.
775        loop {
776            match rx.try_recv() {
777                Ok(cmd) => {
778                    if h2_debug() {
779                        eprintln!(
780                            "[http2] connection {session_key}: draining {}",
781                            cmd_name(&cmd)
782                        );
783                    }
784                    if !apply_cmd(&mut stream, &mut encoder, max_stream_id, cmd) {
785                        if h2_debug() {
786                            eprintln!("[http2] connection {session_key}: write failed, closing");
787                        }
788                        break 'conn;
789                    }
790                }
791                Err(std::sync::mpsc::TryRecvError::Empty) => break,
792                Err(std::sync::mpsc::TryRecvError::Disconnected) => break,
793            }
794        }
795
796        // 2) Read whatever plaintext (HTTP/2 frames) is available.
797        match stream.read(&mut buf) {
798            Ok(0) => {
799                if h2_debug() {
800                    eprintln!("[http2] connection {session_key}: read EOF (Ok 0)");
801                }
802                break;
803            }
804            Ok(n) => inbuf.extend_from_slice(&buf[..n]),
805            Err(ref e)
806                if matches!(
807                    e.kind(),
808                    std::io::ErrorKind::WouldBlock | std::io::ErrorKind::TimedOut
809                ) =>
810            {
811                continue;
812            }
813            Err(e) => {
814                if h2_debug() {
815                    eprintln!(
816                        "[http2] connection {session_key}: read error {:?} ({e})",
817                        e.kind()
818                    );
819                }
820                break;
821            }
822        }
823
824        // 3) Consume the client connection preface (first 24 octets).
825        if !got_preface {
826            if inbuf.len() < PREFACE.len() {
827                continue;
828            }
829            if &inbuf[..PREFACE.len()] != PREFACE {
830                break; // not an HTTP/2 connection preface — bail.
831            }
832            inbuf.drain(..PREFACE.len());
833            got_preface = true;
834        }
835
836        // 4) Parse and dispatch every complete frame currently buffered.
837        loop {
838            if inbuf.len() < 9 {
839                break;
840            }
841            let len =
842                ((inbuf[0] as usize) << 16) | ((inbuf[1] as usize) << 8) | (inbuf[2] as usize);
843            if inbuf.len() < 9 + len {
844                break; // await the rest of the frame body.
845            }
846            let ftype = inbuf[3];
847            let flags = inbuf[4];
848            let stream_id =
849                u32::from_be_bytes([inbuf[5], inbuf[6], inbuf[7], inbuf[8]]) & 0x7fff_ffff;
850            let payload: Vec<u8> = inbuf[9..9 + len].to_vec();
851            inbuf.drain(..9 + len);
852            if h2_debug() {
853                eprintln!(
854                    "[http2] connection {session_key}: recv frame type={ftype} flags={flags:#04x} \
855                     stream={stream_id} len={len}"
856                );
857            }
858
859            match ftype {
860                FT_SETTINGS => {
861                    if flags & FL_ACK == 0 {
862                        // ACK the client's SETTINGS (empty payload, ACK flag).
863                        if write_frame(&mut stream, FT_SETTINGS, FL_ACK, 0, &[])
864                            .and_then(|_| stream.flush())
865                            .is_err()
866                        {
867                            break 'conn;
868                        }
869                    }
870                }
871                FT_PING => {
872                    if flags & FL_ACK == 0 {
873                        // Echo the 8-byte opaque data with the ACK flag set.
874                        if write_frame(&mut stream, FT_PING, FL_ACK, 0, &payload)
875                            .and_then(|_| stream.flush())
876                            .is_err()
877                        {
878                            break 'conn;
879                        }
880                    }
881                }
882                FT_HEADERS => {
883                    if stream_id > max_stream_id {
884                        max_stream_id = stream_id;
885                    }
886                    if flags & FL_END_HEADERS == 0 {
887                        // CONTINUATION reassembly is not implemented; skip this stream.
888                        continue;
889                    }
890                    let block = strip_headers_padding_priority(&payload, flags);
891                    let decoded = match decoder.decode(&block) {
892                        Ok(d) => d,
893                        Err(_) => continue,
894                    };
895                    let headers: Vec<(String, String)> = decoded
896                        .into_iter()
897                        .map(|(k, v)| {
898                            (
899                                String::from_utf8_lossy(&k).into_owned(),
900                                String::from_utf8_lossy(&v).into_owned(),
901                            )
902                        })
903                        .collect();
904                    let end_stream = flags & FL_END_STREAM != 0;
905                    let key = next_stream_key();
906                    id_to_key.insert(stream_id, key);
907                    let tx_stream = tx.clone();
908                    let _ = io_tx.send(Box::new(move || {
909                        on_headers(server_id, key, stream_id, headers, end_stream, tx_stream)
910                    }));
911                }
912                FT_DATA => {
913                    if let Some(&key) = id_to_key.get(&stream_id) {
914                        let data = strip_data_padding(&payload, flags);
915                        let end_stream = flags & FL_END_STREAM != 0;
916                        let _ = io_tx.send(Box::new(move || on_data(key, data, end_stream)));
917                    }
918                }
919                FT_GOAWAY => break 'conn,
920                // WINDOW_UPDATE / PRIORITY / RST_STREAM / CONTINUATION: accepted and
921                // ignored (large-enough default windows; see module limitations).
922                FT_WINDOW_UPDATE | FT_PRIORITY | FT_RST_STREAM | FT_CONTINUATION => {}
923                // Unknown/extension frame types: ignored per RFC 7540 §4.1.
924                _ => {}
925            }
926        }
927    }
928
929    // Best-effort GOAWAY then close.
930    if h2_debug() {
931        eprintln!(
932            "[http2] connection {session_key}: framing loop exited, sending GOAWAY + closing"
933        );
934    }
935    let mut goaway = Vec::with_capacity(8);
936    goaway.extend_from_slice(&(max_stream_id & 0x7fff_ffff).to_be_bytes());
937    goaway.extend_from_slice(&0u32.to_be_bytes()); // NO_ERROR
938    let _ = write_frame(&mut stream, FT_GOAWAY, 0, 0, &goaway);
939    let _ = stream.flush();
940    stream.conn.send_close_notify();
941    let _ = stream.flush();
942    let _ = stream.sock.shutdown(std::net::Shutdown::Both);
943
944    // Release the per-connection handle and drop this connection's records on the
945    // main thread (balances the `incr_handle` in `on_session`).
946    let stream_keys: Vec<u64> = id_to_key.values().copied().collect();
947    let _ = io_tx.send(Box::new(move || on_session_close(session_key, stream_keys)));
948}
949
950/// A short label for an `H2Cmd` (diagnostic tracing only).
951fn cmd_name(cmd: &H2Cmd) -> &'static str {
952    match cmd {
953        H2Cmd::Respond { .. } => "respond(HEADERS)",
954        H2Cmd::Data { .. } => "data(DATA)",
955        H2Cmd::Close { .. } => "close(RST_STREAM)",
956        H2Cmd::Goaway => "goaway(GOAWAY)",
957    }
958}
959
960/// Apply one outbound command by encoding + writing its frame(s). Returns false on
961/// a write error or an explicit GOAWAY (terminating the owner loop).
962fn apply_cmd(
963    stream: &mut StreamOwned<ServerConnection, TcpStream>,
964    encoder: &mut Encoder<'_>,
965    max_stream_id: u32,
966    cmd: H2Cmd,
967) -> bool {
968    match cmd {
969        H2Cmd::Respond {
970            stream_id,
971            headers,
972            end,
973        } => {
974            let block = encode_header_block(encoder, &headers);
975            if h2_debug() {
976                eprintln!(
977                    "[http2] write HEADERS stream={stream_id} end_stream={end} \
978                     hpack_len={} headers={headers:?}",
979                    block.len()
980                );
981            }
982            let flags = FL_END_HEADERS | if end { FL_END_STREAM } else { 0 };
983            write_frame(stream, FT_HEADERS, flags, stream_id, &block)
984                .and_then(|_| stream.flush())
985                .is_ok()
986        }
987        H2Cmd::Data {
988            stream_id,
989            data,
990            end,
991        } => {
992            if h2_debug() {
993                eprintln!(
994                    "[http2] write DATA stream={stream_id} len={} end_stream={end}",
995                    data.len()
996                );
997            }
998            send_data(stream, stream_id, &data, end)
999        }
1000        H2Cmd::Close { stream_id } => {
1001            // RST_STREAM(NO_ERROR).
1002            write_frame(stream, FT_RST_STREAM, 0, stream_id, &0u32.to_be_bytes())
1003                .and_then(|_| stream.flush())
1004                .is_ok()
1005        }
1006        H2Cmd::Goaway => {
1007            let mut g = Vec::with_capacity(8);
1008            g.extend_from_slice(&(max_stream_id & 0x7fff_ffff).to_be_bytes());
1009            g.extend_from_slice(&0u32.to_be_bytes());
1010            let _ = write_frame(stream, FT_GOAWAY, 0, 0, &g);
1011            let _ = stream.flush();
1012            false
1013        }
1014    }
1015}
1016
1017/// Send a body as DATA frames, chunked to MAX_FRAME_SIZE. END_STREAM is set on the
1018/// final frame when `end` is true (an empty body still emits one END_STREAM DATA).
1019fn send_data(
1020    stream: &mut StreamOwned<ServerConnection, TcpStream>,
1021    stream_id: u32,
1022    data: &[u8],
1023    end: bool,
1024) -> bool {
1025    if data.is_empty() {
1026        let flags = if end { FL_END_STREAM } else { 0 };
1027        return write_frame(stream, FT_DATA, flags, stream_id, &[])
1028            .and_then(|_| stream.flush())
1029            .is_ok();
1030    }
1031    let chunks: Vec<&[u8]> = data.chunks(MAX_FRAME_SIZE).collect();
1032    let last = chunks.len() - 1;
1033    for (i, chunk) in chunks.iter().enumerate() {
1034        let flags = if end && i == last { FL_END_STREAM } else { 0 };
1035        if write_frame(stream, FT_DATA, flags, stream_id, chunk).is_err() {
1036            return false;
1037        }
1038    }
1039    stream.flush().is_ok()
1040}
1041
1042/// Write one HTTP/2 frame: 9-octet header (24-bit length, 8-bit type, 8-bit flags,
1043/// 31-bit stream id) followed by the payload (RFC 7540 §4.1).
1044fn write_frame<W: Write>(
1045    w: &mut W,
1046    ftype: u8,
1047    flags: u8,
1048    stream_id: u32,
1049    payload: &[u8],
1050) -> std::io::Result<()> {
1051    let len = payload.len();
1052    let mut hdr = [0u8; 9];
1053    hdr[0] = (len >> 16) as u8;
1054    hdr[1] = (len >> 8) as u8;
1055    hdr[2] = len as u8;
1056    hdr[3] = ftype;
1057    hdr[4] = flags;
1058    hdr[5..9].copy_from_slice(&(stream_id & 0x7fff_ffff).to_be_bytes());
1059    w.write_all(&hdr)?;
1060    w.write_all(payload)?;
1061    Ok(())
1062}
1063
1064/// HPACK-encode a header list into a header block fragment.
1065fn encode_header_block(encoder: &mut Encoder<'_>, headers: &[(String, String)]) -> Vec<u8> {
1066    let owned: Vec<(Vec<u8>, Vec<u8>)> = headers
1067        .iter()
1068        .map(|(k, v)| (k.as_bytes().to_vec(), v.as_bytes().to_vec()))
1069        .collect();
1070    encoder.encode(owned.iter().map(|(k, v)| (k.as_slice(), v.as_slice())))
1071}
1072
1073/// Strip PADDED/PRIORITY prefixes/suffixes from a HEADERS payload, yielding the raw
1074/// header block fragment (RFC 7540 §6.2).
1075fn strip_headers_padding_priority(payload: &[u8], flags: u8) -> Vec<u8> {
1076    let mut start = 0usize;
1077    let mut pad_len = 0usize;
1078    if flags & FL_PADDED != 0 && !payload.is_empty() {
1079        pad_len = payload[0] as usize;
1080        start = 1;
1081    }
1082    if flags & FL_PRIORITY != 0 {
1083        start += 5; // 4-byte stream dependency + 1-byte weight
1084    }
1085    let end = payload.len().saturating_sub(pad_len);
1086    if start > end {
1087        return Vec::new();
1088    }
1089    payload[start..end].to_vec()
1090}
1091
1092/// Strip a PADDED prefix/suffix from a DATA payload (RFC 7540 §6.1).
1093fn strip_data_padding(payload: &[u8], flags: u8) -> Vec<u8> {
1094    if flags & FL_PADDED != 0 && !payload.is_empty() {
1095        let pad_len = payload[0] as usize;
1096        let end = payload.len().saturating_sub(pad_len);
1097        if 1 <= end {
1098            return payload[1..end].to_vec();
1099        }
1100        return Vec::new();
1101    }
1102    payload.to_vec()
1103}
1104
1105// ── main-thread event handlers (run from posted IoTasks) ─────────────────────
1106
1107/// Build the `Http2Session` object and emit `session` on the server. Also takes a
1108/// per-connection event-loop handle (released by `on_session_close`) so the loop
1109/// stays alive while this connection is served — mirroring `tls`'s per-socket
1110/// `incr_handle`.
1111fn on_session(server_id: u64, session_key: u64, tx: Sender<H2Cmd>) -> Result<(), String> {
1112    with_host(|h| h.incr_handle());
1113    let server = H2.with(|s| {
1114        s.borrow()
1115            .servers
1116            .get(&server_id)
1117            .map(|r| r.emitter.clone())
1118    });
1119    let Some(server) = server else { return Ok(()) };
1120    let mut extra = IndexMap::new();
1121    extra.insert("@@h2session".into(), Value::Float(session_key as f64));
1122    let session = new_emitter_object("Http2Session", extra);
1123    H2.with(|s| {
1124        s.borrow_mut().sessions.insert(
1125            session_key,
1126            H2SessionRec {
1127                emitter: session.clone(),
1128                tx,
1129            },
1130        );
1131    });
1132    if let Err(e) = super::events::instance_call(
1133        &server,
1134        "emit",
1135        vec![with_host(|h| h.new_str("session")), session],
1136    ) {
1137        report_handler_error("session", &e);
1138    }
1139    Ok(())
1140}
1141
1142/// Release the per-connection handle and drop the session + its stream records
1143/// (posted by the owner thread when its framing loop exits).
1144fn on_session_close(session_key: u64, stream_keys: Vec<u64>) -> Result<(), String> {
1145    H2.with(|s| {
1146        let mut st = s.borrow_mut();
1147        st.sessions.remove(&session_key);
1148        for k in &stream_keys {
1149            st.streams.remove(k);
1150        }
1151    });
1152    with_host(|h| h.decr_handle());
1153    // Wake the loop so a closed last handle lets it exit.
1154    let _ = with_host(|h| h.io_sender()).send(Box::new(|| Ok(())));
1155    Ok(())
1156}
1157
1158/// Handle a decoded HEADERS frame: build the `Http2Stream`, emit `stream`
1159/// (and compat `request`).
1160fn on_headers(
1161    server_id: u64,
1162    stream_key: u64,
1163    stream_id: u32,
1164    headers: Vec<(String, String)>,
1165    end_stream: bool,
1166    tx: Sender<H2Cmd>,
1167) -> Result<(), String> {
1168    let server = H2.with(|s| {
1169        s.borrow()
1170            .servers
1171            .get(&server_id)
1172            .map(|r| r.emitter.clone())
1173    });
1174    let Some(server) = server else { return Ok(()) };
1175
1176    // Build the header object exposed to JS (pseudo-headers kept as `:`-prefixed).
1177    let headers_obj = with_host(|h| {
1178        let mut m = IndexMap::new();
1179        for (k, v) in &headers {
1180            m.insert(k.clone(), h.new_str(v.clone()));
1181        }
1182        h.new_object(m)
1183    });
1184
1185    // The core Http2Stream object.
1186    let mut extra = IndexMap::new();
1187    extra.insert("@@h2key".into(), Value::Float(stream_key as f64));
1188    extra.insert("id".into(), Value::Float(stream_id as f64));
1189    let stream_obj = new_emitter_object("Http2Stream", extra);
1190    H2.with(|s| {
1191        s.borrow_mut().streams.insert(
1192            stream_key,
1193            H2StreamRec {
1194                emitter: stream_obj.clone(),
1195                tx,
1196                stream_id,
1197                responded: false,
1198            },
1199        );
1200    });
1201
1202    let method = header_value(&headers, ":method").unwrap_or_else(|| "GET".to_string());
1203    let path = header_value(&headers, ":path").unwrap_or_else(|| "/".to_string());
1204    if h2_debug() {
1205        eprintln!("[http2] dispatch stream={stream_id} {method} {path} (end_stream={end_stream})");
1206    }
1207
1208    // Emit the core `stream` event: (stream, headers). CRITICAL: a throwing user
1209    // handler must NOT propagate out of this IoTask — the event loop treats an
1210    // `Err` from a posted task as fatal (`drive_event_loop`: `task()?`) and would
1211    // tear the whole process down, closing every live connection (the classic
1212    // "server closed right after HEADERS → curl broken pipe"). So we catch the
1213    // handler error, surface it on stderr, and keep the loop (and other
1214    // connections) alive — mirroring how a server should isolate request faults.
1215    if let Err(e) = super::events::instance_call(
1216        &server,
1217        "emit",
1218        vec![
1219            with_host(|h| h.new_str("stream")),
1220            stream_obj.clone(),
1221            headers_obj.clone(),
1222        ],
1223    ) {
1224        report_handler_error("stream", &e);
1225        return Ok(());
1226    }
1227
1228    // Compat `request` event: (req, res). `req` is a lightweight emitter carrying
1229    // method/url/headers; `res` is the same Http2Stream (writeHead/end mapped).
1230    let req = super::events::new_emitter();
1231    set_prop(&req, "method", with_host(|h| h.new_str(method)));
1232    set_prop(&req, "url", with_host(|h| h.new_str(path)));
1233    set_prop(&req, "headers", headers_obj);
1234    if let Err(e) = super::events::instance_call(
1235        &server,
1236        "emit",
1237        vec![with_host(|h| h.new_str("request")), req, stream_obj.clone()],
1238    ) {
1239        report_handler_error("request", &e);
1240        return Ok(());
1241    }
1242
1243    // A GET (END_STREAM on HEADERS) has no body: signal `end` to the stream.
1244    if end_stream {
1245        if let Err(e) =
1246            super::events::instance_call(&stream_obj, "emit", vec![with_host(|h| h.new_str("end"))])
1247        {
1248            report_handler_error("stream.end", &e);
1249        }
1250    }
1251    Ok(())
1252}
1253
1254/// Report an uncaught error raised by a user request/stream handler. Printed to
1255/// stderr (a genuine program error, like Node's uncaught-exception output) so the
1256/// cause is visible instead of the process silently dying; never propagated, so
1257/// one faulty handler cannot kill the event loop / other in-flight connections.
1258fn report_handler_error(event: &str, err: &str) {
1259    eprintln!("http2: uncaught error in '{event}' handler: {err}");
1260}
1261
1262/// True when `HTTP2_DEBUG` is set in the environment — enables per-frame stderr
1263/// tracing for diagnosing the h2 framing path end-to-end.
1264fn h2_debug() -> bool {
1265    std::env::var_os("HTTP2_DEBUG").is_some()
1266}
1267
1268/// Emit an inbound request-body DATA chunk (and `end` on END_STREAM) on the stream.
1269fn on_data(stream_key: u64, data: Vec<u8>, end_stream: bool) -> Result<(), String> {
1270    let stream = H2.with(|s| {
1271        s.borrow()
1272            .streams
1273            .get(&stream_key)
1274            .map(|r| r.emitter.clone())
1275    });
1276    let Some(stream) = stream else { return Ok(()) };
1277    if !data.is_empty() {
1278        let chunk = super::buffer::from_bytes(&data);
1279        if let Err(e) = super::events::instance_call(
1280            &stream,
1281            "emit",
1282            vec![with_host(|h| h.new_str("data")), chunk],
1283        ) {
1284            report_handler_error("data", &e);
1285            return Ok(());
1286        }
1287    }
1288    if end_stream {
1289        if let Err(e) =
1290            super::events::instance_call(&stream, "emit", vec![with_host(|h| h.new_str("end"))])
1291        {
1292            report_handler_error("end", &e);
1293        }
1294    }
1295    Ok(())
1296}
1297
1298fn header_value(headers: &[(String, String)], name: &str) -> Option<String> {
1299    headers
1300        .iter()
1301        .find(|(k, _)| k == name)
1302        .map(|(_, v)| v.clone())
1303}
1304
1305// ── Http2Stream instance methods ─────────────────────────────────────────────
1306
1307fn stream_key_of(recv: &Value) -> Option<u64> {
1308    u64_prop(recv, "@@h2key")
1309}
1310
1311fn stream_call(recv: &Value, method: &str, args: Vec<Value>) -> Result<Value, String> {
1312    if let Some(r) = emitter_dispatch(recv, method, &args) {
1313        return r;
1314    }
1315    match method {
1316        "respond" => {
1317            let hdrs = args.first().map(object_pairs).unwrap_or_default();
1318            // `options.endStream` (arg 1) → header-only response.
1319            let end = args
1320                .get(1)
1321                .and_then(|o| get_prop(o, "endStream"))
1322                .map(|v| with_host(|h| h.truthy(&v)))
1323                .unwrap_or(false);
1324            do_respond(recv, hdrs, end)?;
1325            Ok(recv.clone())
1326        }
1327        // HTTP/1-compat: writeHead(status[, headersObj]) → respond.
1328        "writeHead" => {
1329            let status =
1330                with_host(|h| args.first().map(|v| h.to_number(v)).unwrap_or(200.0)) as u32;
1331            let mut hdrs: Vec<(String, String)> = Vec::new();
1332            for a in args.iter().skip(1) {
1333                if matches!(a, Value::Obj(_)) {
1334                    hdrs = object_pairs(a);
1335                    break;
1336                }
1337            }
1338            // Inject the `:status` pseudo-header (writeHead's implicit status).
1339            hdrs.insert(0, (":status".to_string(), status.to_string()));
1340            do_respond(recv, hdrs, false)?;
1341            Ok(recv.clone())
1342        }
1343        "setHeader" => {
1344            // Stash pending compat headers until the response HEADERS are sent.
1345            let k = with_host(|h| h.str_of(&args.first().cloned().unwrap_or(Value::Undef)));
1346            let v = with_host(|h| h.str_of(&args.get(1).cloned().unwrap_or(Value::Undef)));
1347            let bag = pending_headers_obj(recv);
1348            set_prop(&bag, &k, with_host(|h| h.new_str(v)));
1349            Ok(Value::Undef)
1350        }
1351        "getHeader" => {
1352            let k = with_host(|h| h.str_of(&args.first().cloned().unwrap_or(Value::Undef)));
1353            Ok(get_prop(recv, "@@pendingHeaders")
1354                .and_then(|bag| get_prop(&bag, &k))
1355                .unwrap_or(Value::Undef))
1356        }
1357        "removeHeader" => {
1358            let k = with_host(|h| h.str_of(&args.first().cloned().unwrap_or(Value::Undef)));
1359            if let Some(bag) = get_prop(recv, "@@pendingHeaders") {
1360                with_host(|h| {
1361                    if let Some(JsObj::Object(p)) = h.get_mut(&bag) {
1362                        p.shift_remove(&k);
1363                    }
1364                });
1365            }
1366            Ok(Value::Undef)
1367        }
1368        "write" => {
1369            ensure_responded(recv)?;
1370            let bytes = value_bytes(args.first());
1371            send_stream_data(recv, bytes, false);
1372            Ok(Value::Bool(true))
1373        }
1374        "end" => {
1375            ensure_responded(recv)?;
1376            let bytes = args
1377                .first()
1378                .filter(|v| !matches!(v, Value::Undef))
1379                .map(|v| value_bytes(Some(v)))
1380                .unwrap_or_default();
1381            send_stream_data(recv, bytes, true);
1382            super::events::instance_call(recv, "emit", vec![with_host(|h| h.new_str("finish"))])?;
1383            Ok(recv.clone())
1384        }
1385        "close" => {
1386            if let Some(key) = stream_key_of(recv) {
1387                let sent = H2.with(|s| {
1388                    s.borrow().streams.get(&key).map(|r| {
1389                        let _ = r.tx.send(H2Cmd::Close {
1390                            stream_id: r.stream_id,
1391                        });
1392                    })
1393                });
1394                let _ = sent;
1395            }
1396            Ok(recv.clone())
1397        }
1398        "setEncoding" | "setTimeout" | "pause" | "resume" => Ok(recv.clone()),
1399        _ => Err(crate::host::type_error(&format!(
1400            "stream.{method} is not a function"
1401        ))),
1402    }
1403}
1404
1405/// The lazily-created object holding compat `setHeader` values before the response.
1406fn pending_headers_obj(recv: &Value) -> Value {
1407    if let Some(bag) = get_prop(recv, "@@pendingHeaders") {
1408        return bag;
1409    }
1410    let bag = with_host(|h| h.new_object(IndexMap::new()));
1411    set_prop(recv, "@@pendingHeaders", bag.clone());
1412    bag
1413}
1414
1415/// Send a HEADERS response frame with the given headers (a `:status` is injected if
1416/// none present). Marks the stream responded.
1417fn do_respond(recv: &Value, mut headers: Vec<(String, String)>, end: bool) -> Result<(), String> {
1418    // Merge any compat setHeader() values collected before respond/writeHead.
1419    if let Some(bag) = get_prop(recv, "@@pendingHeaders") {
1420        for (k, v) in object_pairs(&bag) {
1421            if !headers.iter().any(|(hk, _)| hk.eq_ignore_ascii_case(&k)) {
1422                headers.push((k, v));
1423            }
1424        }
1425    }
1426    // Ensure a single `:status` pseudo-header appears first (HTTP/2 requires
1427    // pseudo-headers to precede regular ones).
1428    let status = headers
1429        .iter()
1430        .find(|(k, _)| k == ":status")
1431        .map(|(_, v)| v.clone())
1432        .unwrap_or_else(|| "200".to_string());
1433    let mut ordered: Vec<(String, String)> = vec![(":status".to_string(), status)];
1434    for (k, v) in headers.into_iter() {
1435        if k == ":status" {
1436            continue;
1437        }
1438        // HTTP/2 header names must be lowercase.
1439        ordered.push((k.to_ascii_lowercase(), v));
1440    }
1441
1442    let Some(key) = stream_key_of(recv) else {
1443        return Ok(());
1444    };
1445    H2.with(|s| {
1446        if let Some(r) = s.borrow_mut().streams.get_mut(&key) {
1447            if !r.responded {
1448                r.responded = true;
1449                let _ = r.tx.send(H2Cmd::Respond {
1450                    stream_id: r.stream_id,
1451                    headers: ordered,
1452                    end,
1453                });
1454            }
1455        }
1456    });
1457    Ok(())
1458}
1459
1460/// Ensure the response HEADERS have been sent (auto-`respond` with 200 otherwise).
1461fn ensure_responded(recv: &Value) -> Result<(), String> {
1462    let Some(key) = stream_key_of(recv) else {
1463        return Ok(());
1464    };
1465    let responded = H2.with(|s| {
1466        s.borrow()
1467            .streams
1468            .get(&key)
1469            .map(|r| r.responded)
1470            .unwrap_or(true)
1471    });
1472    if !responded {
1473        do_respond(recv, Vec::new(), false)?;
1474    }
1475    Ok(())
1476}
1477
1478/// Queue a DATA frame for this stream's connection thread.
1479fn send_stream_data(recv: &Value, data: Vec<u8>, end: bool) {
1480    if let Some(key) = stream_key_of(recv) {
1481        H2.with(|s| {
1482            if let Some(r) = s.borrow().streams.get(&key) {
1483                let _ = r.tx.send(H2Cmd::Data {
1484                    stream_id: r.stream_id,
1485                    data,
1486                    end,
1487                });
1488            }
1489        });
1490    }
1491}
1492
1493// ── Http2Session instance methods ────────────────────────────────────────────
1494
1495fn session_call(recv: &Value, method: &str, args: Vec<Value>) -> Result<Value, String> {
1496    if let Some(r) = emitter_dispatch(recv, method, &args) {
1497        return r;
1498    }
1499    match method {
1500        "close" | "destroy" | "goaway" => {
1501            if let Some(key) = u64_prop(recv, "@@h2session") {
1502                H2.with(|s| {
1503                    if let Some(r) = s.borrow().sessions.get(&key) {
1504                        let _ = r.tx.send(H2Cmd::Goaway);
1505                    }
1506                });
1507            }
1508            super::events::instance_call(recv, "emit", vec![with_host(|h| h.new_str("close"))])?;
1509            Ok(recv.clone())
1510        }
1511        "settings" | "ping" | "ref" | "unref" | "setTimeout" => Ok(recv.clone()),
1512        _ => Err(crate::host::type_error(&format!(
1513            "session.{method} is not a function"
1514        ))),
1515    }
1516}
1517
1518// ── shared emitter constructor (same shape as net/tls) ───────────────────────
1519
1520/// Build a native emitter object (`@@native` tag + `@@on`/`@@once` maps + extras),
1521/// sharing the EventEmitter shape with `events`/`net`/`tls`.
1522pub fn new_emitter_object(tag: &str, mut extra: IndexMap<String, Value>) -> Value {
1523    with_host(|h| {
1524        let on = h.new_object(IndexMap::new());
1525        let once = h.new_object(IndexMap::new());
1526        let mut m = IndexMap::new();
1527        m.insert("@@native".into(), h.new_str(tag));
1528        m.insert("@@on".into(), on);
1529        m.insert("@@once".into(), once);
1530        for (k, v) in extra.drain(..) {
1531            m.insert(k, v);
1532        }
1533        h.new_object(m)
1534    })
1535}