@gjsify/http2-native 0.4.3 → 0.4.5

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.
@@ -1,41 +1,234 @@
1
1
  /*
2
- * SessionBridge — placeholder for the future cleartext-HTTP/2 (h2c)
3
- * session driver.
2
+ * SessionBridge — owns an nghttp2 session and surfaces frames to JS as
3
+ * GObject signals.
4
4
  *
5
- * Intent
6
- * ──────
7
- * Once we wire `nghttp2_session_server_new()` against a `Gio.Socket` we
8
- * obtain from a Soup.Server `request-aborted` / raw socket callback (or
9
- * directly from `Gio.SocketService` when bypassing Soup entirely), this
10
- * class will own:
11
- * • the nghttp2_session pointer
12
- * • a 64 KiB read buffer driven by a `g_socket_create_source(IN)`
13
- * watch on the GLib main context
14
- * • a write queue drained on `OUT` readiness
15
- * • mirror signals (`request_received`, `data_received`, `stream_closed`,
16
- * `goaway_received`) re-emitted on the main context via `GLib.Idle.add`
17
- * — same hop pattern as @gjsify/webrtc-native bridges
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.
18
11
  *
19
- * Until that lands, this class only validates the HTTP/2 connection
20
- * preface ("PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n", 24 bytes) so JS-side code
21
- * detecting a prior-knowledge h2c upgrade can route the socket through
22
- * a future native session loop.
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.
23
20
  */
24
21
 
25
22
  namespace GjsifyHttp2 {
26
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
+
27
152
  public class SessionBridge : GLib.Object {
28
153
 
29
- /**
30
- * is_client_preface:
31
- * @bytes: the first ≥ 24 bytes received on a TCP connection
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.
32
166
  *
33
- * Returns %TRUE if @bytes starts with the RFC 7540 §3.5 client
34
- * connection preface ("PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n").
35
- * Used to detect prior-knowledge h2c on a freshly-accepted socket
36
- * before deciding whether to dispatch it to Soup (HTTP/1.1) or
37
- * the future native nghttp2 session loop.
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
38
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
+
39
232
  public static bool is_client_preface (GLib.Bytes? bytes) {
40
233
  if (bytes == null) return false;
41
234
  const uint8 PREFACE[] = {
@@ -43,23 +236,233 @@ namespace GjsifyHttp2 {
43
236
  0x54, 0x50, 0x2f, 0x32, 0x2e, 0x30, 0x0d, 0x0a,
44
237
  0x0d, 0x0a, 0x53, 0x4d, 0x0d, 0x0a, 0x0d, 0x0a
45
238
  };
46
- size_t blen;
47
239
  unowned uint8[] data = bytes.get_data ();
48
- blen = data.length;
49
- if (blen < 24) return false;
240
+ if (data.length < 24) return false;
50
241
  for (int i = 0; i < 24; i++) {
51
242
  if (data[i] != PREFACE[i]) return false;
52
243
  }
53
244
  return true;
54
245
  }
55
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
+
56
272
  /**
57
- * preface_length:
273
+ * drain_output:
58
274
  *
59
- * Returns the size of the HTTP/2 client connection preface (24).
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.
60
278
  */
61
- public static uint preface_length () {
62
- return 24;
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 ();
63
466
  }
64
467
  }
65
468
  }