@gjsify/http2-native 0.4.0 → 0.4.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,468 @@
1
+ /*
2
+ * SessionBridge — owns an nghttp2 session and surfaces frames to JS as
3
+ * GObject signals.
4
+ *
5
+ * Lifecycle
6
+ * ─────────
7
+ * One SessionBridge per accepted TCP connection. The JS-side dispatcher
8
+ * (`@gjsify/http2`'s `native-dispatcher.ts`, landing in Phase 1) feeds
9
+ * raw socket bytes via `feed_input()`, drains pending output via
10
+ * `drain_output()`, and reads decoded events via the signals below.
11
+ *
12
+ * Signals always fire on the GLib main loop (same hop pattern as
13
+ * @gjsify/webrtc-native): a single GLib.Idle.add() pass drains every
14
+ * queued event from the C-side queue, so JS observers see a settled
15
+ * sequence rather than partial frame state.
16
+ *
17
+ * Buffer ownership: every byte hand-off uses GLib.Bytes so SpiderMonkey
18
+ * GC never races against nghttp2's allocator. The opaque session pointer
19
+ * lives in private extern storage; only the destructor accesses it.
20
+ */
21
+
22
+ namespace GjsifyHttp2 {
23
+
24
+ /* ── Opaque pointer wrappers + C entry points ─────────────────────── *
25
+ * We avoid [Compact] classes because Vala emits its own
26
+ * `*_free` function definitions for them, clashing with the real
27
+ * gjsify_http2_{session,event}_free in nghttp2-helpers.c. Instead the
28
+ * session/event handles travel as `void*` (gpointer) across the FFI
29
+ * boundary, and the destructor explicitly calls the C free function. */
30
+
31
+ /* Enums live in the C header (nghttp2-helpers.h). We use plain
32
+ * integer constants here so Vala doesn't try to re-emit the typedef
33
+ * into its generated header (which causes a redeclaration clash
34
+ * against the C-side definition). All session-mode + event-kind
35
+ * arguments cross the FFI boundary as `int`. */
36
+
37
+ private const int SESSION_MODE_SERVER = 0;
38
+ private const int SESSION_MODE_CLIENT = 1;
39
+
40
+ private const int EVENT_HEADERS = 1;
41
+ private const int EVENT_DATA = 2;
42
+ private const int EVENT_STREAM_CLOSED = 3;
43
+ private const int EVENT_GOAWAY = 4;
44
+ private const int EVENT_SETTINGS = 5;
45
+ private const int EVENT_PUSH_PROMISE = 6;
46
+
47
+ /* Event accessors — operate on a raw `void*` so Vala does not need a
48
+ * type definition for GjsifyHttp2Event. Pointers come from
49
+ * _session_drain_events() (returned as a `void**` NULL-terminated
50
+ * array). Lifetime: each event must be freed via _event_free() once
51
+ * its data has been emitted. */
52
+ [CCode (cname = "gjsify_http2_event_free", cheader_filename = "nghttp2-helpers.h")]
53
+ private extern void _event_free (void* ev);
54
+ [CCode (cname = "gjsify_http2_event_get_kind", cheader_filename = "nghttp2-helpers.h")]
55
+ private extern uint32 _event_get_kind (void* ev);
56
+ [CCode (cname = "gjsify_http2_event_get_stream_id", cheader_filename = "nghttp2-helpers.h")]
57
+ private extern uint32 _event_get_stream_id (void* ev);
58
+ [CCode (cname = "gjsify_http2_event_get_error_code", cheader_filename = "nghttp2-helpers.h")]
59
+ private extern uint32 _event_get_error_code (void* ev);
60
+ [CCode (cname = "gjsify_http2_event_get_last_stream_id", cheader_filename = "nghttp2-helpers.h")]
61
+ private extern uint32 _event_get_last_stream_id (void* ev);
62
+ [CCode (cname = "gjsify_http2_event_get_end_stream", cheader_filename = "nghttp2-helpers.h")]
63
+ private extern bool _event_get_end_stream (void* ev);
64
+ [CCode (cname = "gjsify_http2_event_get_promised_stream_id", cheader_filename = "nghttp2-helpers.h")]
65
+ private extern uint32 _event_get_promised_stream_id (void* ev);
66
+ [CCode (cname = "gjsify_http2_event_get_header_count", cheader_filename = "nghttp2-helpers.h")]
67
+ private extern size_t _event_get_header_count (void* ev);
68
+ [CCode (cname = "gjsify_http2_event_get_header_name", cheader_filename = "nghttp2-helpers.h")]
69
+ private extern unowned string? _event_get_header_name (void* ev, size_t index);
70
+ [CCode (cname = "gjsify_http2_event_get_header_value", cheader_filename = "nghttp2-helpers.h")]
71
+ private extern unowned string? _event_get_header_value (void* ev, size_t index);
72
+ [CCode (cname = "gjsify_http2_event_get_data", cheader_filename = "nghttp2-helpers.h")]
73
+ private extern unowned GLib.Bytes? _event_get_data (void* ev);
74
+
75
+ [CCode (cname = "gjsify_http2_session_new",
76
+ cheader_filename = "nghttp2-helpers.h")]
77
+ private extern void* _session_new (int mode);
78
+
79
+ [CCode (cname = "gjsify_http2_session_free",
80
+ cheader_filename = "nghttp2-helpers.h")]
81
+ private extern void _session_free (void* session);
82
+
83
+ [CCode (cname = "gjsify_http2_session_feed",
84
+ cheader_filename = "nghttp2-helpers.h")]
85
+ private extern ssize_t _session_feed (void* session, GLib.Bytes input);
86
+
87
+ [CCode (cname = "gjsify_http2_session_drain_output",
88
+ cheader_filename = "nghttp2-helpers.h")]
89
+ private extern GLib.Bytes _session_drain_output (void* session);
90
+
91
+ [CCode (cname = "gjsify_http2_session_drain_events",
92
+ cheader_filename = "nghttp2-helpers.h")]
93
+ private extern void** _session_drain_events (void* session, out size_t n);
94
+
95
+ [CCode (cname = "gjsify_http2_session_submit_settings",
96
+ cheader_filename = "nghttp2-helpers.h")]
97
+ private extern int _session_submit_settings (void* session);
98
+
99
+ [CCode (cname = "gjsify_http2_session_submit_response",
100
+ cheader_filename = "nghttp2-helpers.h")]
101
+ private extern int _session_submit_response (
102
+ void* session,
103
+ uint32 stream_id,
104
+ [CCode (array_length = false, array_null_terminated = true)] string[] names,
105
+ [CCode (array_length = false, array_null_terminated = true)] string[] values,
106
+ size_t n_pairs,
107
+ bool end_stream);
108
+
109
+ [CCode (cname = "gjsify_http2_session_submit_request",
110
+ cheader_filename = "nghttp2-helpers.h")]
111
+ private extern uint32 _session_submit_request (
112
+ void* session,
113
+ [CCode (array_length = false, array_null_terminated = true)] string[] names,
114
+ [CCode (array_length = false, array_null_terminated = true)] string[] values,
115
+ size_t n_pairs,
116
+ bool end_stream);
117
+
118
+ [CCode (cname = "gjsify_http2_session_submit_data",
119
+ cheader_filename = "nghttp2-helpers.h")]
120
+ private extern int _session_submit_data (
121
+ void* session, uint32 stream_id, GLib.Bytes data, bool end_stream);
122
+
123
+ [CCode (cname = "gjsify_http2_session_submit_push_promise",
124
+ cheader_filename = "nghttp2-helpers.h")]
125
+ private extern uint32 _session_submit_push_promise (
126
+ void* session,
127
+ uint32 parent_id,
128
+ [CCode (array_length = false, array_null_terminated = true)] string[] names,
129
+ [CCode (array_length = false, array_null_terminated = true)] string[] values,
130
+ size_t n_pairs);
131
+
132
+ [CCode (cname = "gjsify_http2_session_submit_goaway",
133
+ cheader_filename = "nghttp2-helpers.h")]
134
+ private extern int _session_submit_goaway (
135
+ void* session, uint32 last_stream_id, uint32 error_code);
136
+
137
+ [CCode (cname = "gjsify_http2_session_submit_rst_stream",
138
+ cheader_filename = "nghttp2-helpers.h")]
139
+ private extern int _session_submit_rst_stream (
140
+ void* session, uint32 stream_id, uint32 error_code);
141
+
142
+ [CCode (cname = "gjsify_http2_session_want_read",
143
+ cheader_filename = "nghttp2-helpers.h")]
144
+ private extern bool _session_want_read (void* session);
145
+
146
+ [CCode (cname = "gjsify_http2_session_want_write",
147
+ cheader_filename = "nghttp2-helpers.h")]
148
+ private extern bool _session_want_write (void* session);
149
+
150
+ /* ── SessionBridge ───────────────────────────────────────────────── */
151
+
152
+ public class SessionBridge : GLib.Object {
153
+
154
+ private void* _native = null;
155
+ private bool _drain_scheduled = false;
156
+ private bool _disposed = false;
157
+
158
+ ~SessionBridge () {
159
+ if (_native != null) {
160
+ _session_free (_native);
161
+ _native = null;
162
+ }
163
+ }
164
+
165
+ /* Signals — fired from a GLib.Idle.add() pass after frame arrival.
166
+ *
167
+ * Header signals carry GLib.Variant of signature "a(ss)" (array
168
+ * of name/value tuples) rather than `string[]`. Vala's signal
169
+ * marshaller emits paired string-arrays as `gpointer` (not
170
+ * G_TYPE_STRV) which trips a GJS assertion at signal-emit time;
171
+ * GVariant marshals cleanly through introspection. JS-side code
172
+ * unpacks via `variant.deep_unpack()` (returns nested arrays). */
173
+
174
+ /**
175
+ * headers_received:
176
+ * @stream_id: client-initiated stream the HEADERS frame belongs to
177
+ * @headers: GVariant "a(ss)" — array of (name, value) tuples
178
+ * (lower-cased names, pseudo-headers like ":method" included)
179
+ * @end_stream: %TRUE when this HEADERS also carries END_STREAM
180
+ */
181
+ public signal void headers_received (uint32 stream_id,
182
+ GLib.Variant headers,
183
+ bool end_stream);
184
+
185
+ /** data_received: chunk of a DATA frame. */
186
+ public signal void data_received (uint32 stream_id,
187
+ GLib.Bytes chunk,
188
+ bool end_stream);
189
+
190
+ /** stream_closed: stream finished (after END_STREAM or RST_STREAM). */
191
+ public signal void stream_closed (uint32 stream_id, uint32 error_code);
192
+
193
+ /** frame_send_ready: bytes were produced by nghttp2 (drain via drain_output). */
194
+ public signal void frame_send_ready ();
195
+
196
+ /** goaway_received: peer sent GOAWAY. */
197
+ public signal void goaway_received (uint32 last_stream_id, uint32 error_code);
198
+
199
+ /** settings_received: peer's SETTINGS frame (ACK'd by nghttp2 automatically). */
200
+ public signal void settings_received ();
201
+
202
+ /** push_promise_received: client-side only. */
203
+ public signal void push_promise_received (uint32 stream_id,
204
+ uint32 promised_stream_id,
205
+ GLib.Variant headers);
206
+
207
+ /* ── construction ────────────────────────────────────────────── */
208
+
209
+ public static SessionBridge? new_server () {
210
+ return _construct (SESSION_MODE_SERVER);
211
+ }
212
+
213
+ public static SessionBridge? new_client () {
214
+ return _construct (SESSION_MODE_CLIENT);
215
+ }
216
+
217
+ private static SessionBridge? _construct (int mode) {
218
+ var bridge = new SessionBridge ();
219
+ void* native = _session_new (mode);
220
+ if (native == null) return null;
221
+ bridge._native = native;
222
+ /* Queue the initial SETTINGS frame on construction — the
223
+ * Phase-1 dispatcher will drain it onto the wire after the
224
+ * preface (server) or client-preface write. */
225
+ _session_submit_settings (bridge._native);
226
+ return bridge;
227
+ }
228
+
229
+ /* ── client/preface helpers (kept from the original stub for
230
+ * backwards compat with anything that imported them) ──────── */
231
+
232
+ public static bool is_client_preface (GLib.Bytes? bytes) {
233
+ if (bytes == null) return false;
234
+ const uint8 PREFACE[] = {
235
+ 0x50, 0x52, 0x49, 0x20, 0x2a, 0x20, 0x48, 0x54,
236
+ 0x54, 0x50, 0x2f, 0x32, 0x2e, 0x30, 0x0d, 0x0a,
237
+ 0x0d, 0x0a, 0x53, 0x4d, 0x0d, 0x0a, 0x0d, 0x0a
238
+ };
239
+ unowned uint8[] data = bytes.get_data ();
240
+ if (data.length < 24) return false;
241
+ for (int i = 0; i < 24; i++) {
242
+ if (data[i] != PREFACE[i]) return false;
243
+ }
244
+ return true;
245
+ }
246
+
247
+ public static uint preface_length () {
248
+ return 24;
249
+ }
250
+
251
+ /* ── public API ──────────────────────────────────────────────── */
252
+
253
+ /**
254
+ * feed_input:
255
+ * @input: bytes received from the peer
256
+ *
257
+ * Returns the number of bytes nghttp2 consumed (always == input
258
+ * length on success). Triggers event emission asynchronously
259
+ * via GLib.Idle.add(); returns immediately. Also notifies
260
+ * frame_send_ready() if nghttp2 produced any output.
261
+ */
262
+ public ssize_t feed_input (GLib.Bytes input) {
263
+ if (_native == null || _disposed) return -1;
264
+ ssize_t rv = _session_feed (_native, input);
265
+ if (rv < 0) return rv;
266
+ /* nghttp2 may have queued SETTINGS-ACK, WINDOW_UPDATE etc.
267
+ * in response — surface a write-readiness hint immediately. */
268
+ schedule_drain ();
269
+ return rv;
270
+ }
271
+
272
+ /**
273
+ * drain_output:
274
+ *
275
+ * Returns all bytes nghttp2 wants to send right now (empty Bytes
276
+ * if none). Caller must write the bytes to the socket and call
277
+ * again if want_write() remains true.
278
+ */
279
+ public GLib.Bytes drain_output () {
280
+ if (_native == null) return new GLib.Bytes (null);
281
+ return _session_drain_output (_native);
282
+ }
283
+
284
+ public bool want_read () { return _native != null && _session_want_read (_native); }
285
+ public bool want_write () { return _native != null && _session_want_write (_native); }
286
+
287
+ public int submit_settings () {
288
+ if (_native == null) return -1;
289
+ int rv = _session_submit_settings (_native);
290
+ if (rv == 0) schedule_drain ();
291
+ return rv;
292
+ }
293
+
294
+ public int submit_response (uint32 stream_id,
295
+ string[] names,
296
+ string[] values,
297
+ bool end_stream) {
298
+ if (_native == null) return -1;
299
+ if (names.length != values.length) return -1;
300
+ int rv = _session_submit_response (
301
+ _native, stream_id, names, values, names.length, end_stream);
302
+ if (rv == 0) schedule_drain ();
303
+ return rv;
304
+ }
305
+
306
+ /**
307
+ * submit_request:
308
+ *
309
+ * Client-only: queue a HEADERS frame for a fresh outbound stream.
310
+ * Returns the allocated odd stream-id (1, 3, ...) or 0 on error.
311
+ */
312
+ public uint32 submit_request (string[] names,
313
+ string[] values,
314
+ bool end_stream) {
315
+ if (_native == null) return 0;
316
+ if (names.length != values.length) return 0;
317
+ uint32 sid = _session_submit_request (
318
+ _native, names, values, names.length, end_stream);
319
+ if (sid != 0) schedule_drain ();
320
+ return sid;
321
+ }
322
+
323
+ public int submit_data (uint32 stream_id, GLib.Bytes data, bool end_stream) {
324
+ if (_native == null) return -1;
325
+ int rv = _session_submit_data (_native, stream_id, data, end_stream);
326
+ if (rv == 0) schedule_drain ();
327
+ return rv;
328
+ }
329
+
330
+ public uint32 submit_push_promise (uint32 parent_id,
331
+ string[] names,
332
+ string[] values) {
333
+ if (_native == null) return 0;
334
+ if (names.length != values.length) return 0;
335
+ uint32 promised = _session_submit_push_promise (
336
+ _native, parent_id, names, values, names.length);
337
+ if (promised != 0) schedule_drain ();
338
+ return promised;
339
+ }
340
+
341
+ public int submit_goaway (uint32 last_stream_id, uint32 error_code) {
342
+ if (_native == null) return -1;
343
+ int rv = _session_submit_goaway (_native, last_stream_id, error_code);
344
+ if (rv == 0) schedule_drain ();
345
+ return rv;
346
+ }
347
+
348
+ public int submit_rst_stream (uint32 stream_id, uint32 error_code) {
349
+ if (_native == null) return -1;
350
+ int rv = _session_submit_rst_stream (_native, stream_id, error_code);
351
+ if (rv == 0) schedule_drain ();
352
+ return rv;
353
+ }
354
+
355
+ /**
356
+ * close:
357
+ *
358
+ * Tears down the nghttp2 session and frees C-side state. Safe to
359
+ * call multiple times. After close(), further submit/feed calls
360
+ * return -1.
361
+ */
362
+ public void close () {
363
+ if (_disposed) return;
364
+ _disposed = true;
365
+ if (_native != null) {
366
+ _session_free (_native);
367
+ _native = null;
368
+ }
369
+ }
370
+
371
+ /* ── event dispatch ──────────────────────────────────────────── */
372
+
373
+ private void schedule_drain () {
374
+ if (_drain_scheduled || _disposed) return;
375
+ _drain_scheduled = true;
376
+ GLib.Idle.add (drain_events_idle);
377
+ }
378
+
379
+ /* Synchronous variant for tests that want deterministic delivery
380
+ * without spinning the main loop. */
381
+ public void dispatch_pending () {
382
+ drain_events_now ();
383
+ }
384
+
385
+ private bool drain_events_idle () {
386
+ _drain_scheduled = false;
387
+ drain_events_now ();
388
+ return false; /* remove */
389
+ }
390
+
391
+ private void drain_events_now () {
392
+ if (_native == null) return;
393
+
394
+ /* First surface any pending write — the dispatcher uses
395
+ * this to flush onto the socket. */
396
+ if (_session_want_write (_native)) {
397
+ frame_send_ready ();
398
+ }
399
+
400
+ size_t n_events = 0;
401
+ void** events = _session_drain_events (_native, out n_events);
402
+ if (events == null || n_events == 0) return;
403
+
404
+ for (size_t i = 0; i < n_events; i++) {
405
+ void* ev = events[i];
406
+ if (ev == null) break;
407
+ uint32 kind = _event_get_kind (ev);
408
+ switch (kind) {
409
+ case EVENT_HEADERS: {
410
+ var v = build_headers_variant (ev);
411
+ headers_received (
412
+ _event_get_stream_id (ev),
413
+ v,
414
+ _event_get_end_stream (ev));
415
+ break;
416
+ }
417
+ case EVENT_DATA: {
418
+ unowned GLib.Bytes? data_bytes = _event_get_data (ev);
419
+ data_received (
420
+ _event_get_stream_id (ev),
421
+ data_bytes != null ? data_bytes : new GLib.Bytes (null),
422
+ _event_get_end_stream (ev));
423
+ break;
424
+ }
425
+ case EVENT_STREAM_CLOSED:
426
+ stream_closed (_event_get_stream_id (ev),
427
+ _event_get_error_code (ev));
428
+ break;
429
+ case EVENT_GOAWAY:
430
+ goaway_received (_event_get_last_stream_id (ev),
431
+ _event_get_error_code (ev));
432
+ break;
433
+ case EVENT_SETTINGS:
434
+ settings_received ();
435
+ break;
436
+ case EVENT_PUSH_PROMISE: {
437
+ var v = build_headers_variant (ev);
438
+ push_promise_received (
439
+ _event_get_stream_id (ev),
440
+ _event_get_promised_stream_id (ev),
441
+ v);
442
+ break;
443
+ }
444
+ default:
445
+ break;
446
+ }
447
+ _event_free (ev);
448
+ }
449
+ /* Free the container array (allocated by g_new0 in the C
450
+ * drain_events helper). */
451
+ GLib.free (events);
452
+ }
453
+
454
+ /* Build a GVariant of signature "a(ss)" from the event's header
455
+ * accumulator. Marshalls cleanly through GObject introspection
456
+ * (G_VARIANT_TYPE > G_TYPE_STRV for paired-string signals). */
457
+ private GLib.Variant build_headers_variant (void* ev) {
458
+ size_t n = _event_get_header_count (ev);
459
+ var builder = new GLib.VariantBuilder (new GLib.VariantType ("a(ss)"));
460
+ for (size_t i = 0; i < n; i++) {
461
+ unowned string? name = _event_get_header_name (ev, i);
462
+ unowned string? value = _event_get_header_value (ev, i);
463
+ builder.add ("(ss)", name ?? "", value ?? "");
464
+ }
465
+ return builder.end ();
466
+ }
467
+ }
468
+ }
@@ -0,0 +1,62 @@
1
+ /*
2
+ * StreamIdAllocator — server-side HTTP/2 stream-id sequencer.
3
+ *
4
+ * Per RFC 7540 §5.1.1:
5
+ * • Client-initiated streams use odd ids (1, 3, 5, ...)
6
+ * • Server-initiated (push) streams use even ids (2, 4, 6, ...)
7
+ *
8
+ * The next-id pointer is monotonically increasing per session and must
9
+ * never wrap. Once we exhaust the 31-bit id space we MUST send a GOAWAY
10
+ * with NGHTTP2_NO_ERROR and let a fresh connection take over.
11
+ *
12
+ * One allocator instance per ServerHttp2Session.
13
+ */
14
+
15
+ namespace GjsifyHttp2 {
16
+
17
+ public class StreamIdAllocator : GLib.Object {
18
+
19
+ /* Max valid stream id (31-bit). Anything >= this is exhausted. */
20
+ public const uint32 MAX_STREAM_ID = 0x7fffffffu;
21
+
22
+ private uint32 _next_promised = 2;
23
+ private uint32 _last_client = 0;
24
+
25
+ /**
26
+ * next_promised:
27
+ *
28
+ * Returns the next even stream-id to use for a PUSH_PROMISE.
29
+ * Returns 0 if the id space is exhausted — caller MUST then
30
+ * send GOAWAY and refuse further pushes.
31
+ */
32
+ public uint32 next_promised () {
33
+ if (_next_promised > MAX_STREAM_ID) return 0;
34
+ uint32 id = _next_promised;
35
+ _next_promised += 2;
36
+ return id;
37
+ }
38
+
39
+ /**
40
+ * record_client_stream:
41
+ * @id: client-initiated odd stream-id observed on this session
42
+ *
43
+ * Track the highest client stream-id seen — needed for the
44
+ * `last-stream-id` field of a GOAWAY frame.
45
+ */
46
+ public void record_client_stream (uint32 id) {
47
+ if ((id & 1u) == 1u && id > _last_client) {
48
+ _last_client = id;
49
+ }
50
+ }
51
+
52
+ public uint32 last_client_stream_id { get { return _last_client; } }
53
+
54
+ /** Number of pushes that can still be issued (count, not id). */
55
+ public uint32 remaining_pushes {
56
+ get {
57
+ if (_next_promised > MAX_STREAM_ID) return 0;
58
+ return (MAX_STREAM_ID - _next_promised) / 2u + 1u;
59
+ }
60
+ }
61
+ }
62
+ }