@gjsify/http2-native 0.4.3 → 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.
@@ -1,5 +1,5 @@
1
1
  /*
2
- * Tiny C shim around libnghttp2's HPACK encoder + frame helpers.
2
+ * Tiny C shim around libnghttp2's HPACK encoder + frame helpers + session API.
3
3
  * See nghttp2-helpers.h for the rationale + per-function docs.
4
4
  */
5
5
 
@@ -8,6 +8,10 @@
8
8
  #include <string.h>
9
9
  #include <stdlib.h>
10
10
 
11
+ /* ────────────────────────────────────────────────────────────────────── *
12
+ * HPACK + standalone frame builders *
13
+ * ────────────────────────────────────────────────────────────────────── */
14
+
11
15
  GBytes *
12
16
  gjsify_http2_hpack_encode (char **names,
13
17
  char **values,
@@ -131,3 +135,708 @@ gjsify_http2_nghttp2_version (void)
131
135
  nghttp2_info *info = nghttp2_version (0);
132
136
  return info ? info->version_str : "unknown";
133
137
  }
138
+
139
+ /* ────────────────────────────────────────────────────────────────────── *
140
+ * Session API *
141
+ * ────────────────────────────────────────────────────────────────────── */
142
+
143
+ /* Full struct layout for the opaque GjsifyHttp2Event (forward-declared
144
+ * in nghttp2-helpers.h). Vala emits its own forward declaration with
145
+ * the matching tag (`struct _GjsifyHttp2Event`) and never tries to
146
+ * inspect the fields — every access goes through the getter functions
147
+ * below. */
148
+ struct _GjsifyHttp2Event {
149
+ GjsifyHttp2EventKind kind;
150
+ guint32 stream_id;
151
+ guint32 error_code;
152
+ guint32 last_stream_id;
153
+ gboolean end_stream;
154
+ char **headers_names;
155
+ char **headers_values;
156
+ gsize headers_count;
157
+ GBytes *data;
158
+ guint32 promised_stream_id;
159
+ };
160
+
161
+ /* Per-stream HEADERS accumulator. We hold names/values in two GPtrArrays
162
+ * (auto-NULL-terminated, plain `char*` ownership) until the HEADERS frame
163
+ * finishes (on_frame_recv with type==HEADERS) — only then do we emit the
164
+ * EVENT_HEADERS event so JS sees the complete pseudo-header set in one
165
+ * call. Without this, partial header arrival could leak through on_header
166
+ * if nghttp2 ever spreads HEADERS+CONTINUATION across multiple recv()s. */
167
+ typedef struct {
168
+ GPtrArray *names; /* owns each entry via g_free */
169
+ GPtrArray *values; /* owns each entry via g_free */
170
+ } StreamHeaderAccum;
171
+
172
+ static StreamHeaderAccum *
173
+ stream_header_accum_new (void)
174
+ {
175
+ StreamHeaderAccum *a = g_new0 (StreamHeaderAccum, 1);
176
+ a->names = g_ptr_array_new_with_free_func (g_free);
177
+ a->values = g_ptr_array_new_with_free_func (g_free);
178
+ return a;
179
+ }
180
+
181
+ static void
182
+ stream_header_accum_free (gpointer p)
183
+ {
184
+ StreamHeaderAccum *a = p;
185
+ if (!a) return;
186
+ g_ptr_array_unref (a->names);
187
+ g_ptr_array_unref (a->values);
188
+ g_free (a);
189
+ }
190
+
191
+ /* Outgoing-DATA carrier — backs nghttp2_data_provider.
192
+ * We hold a GBytes per pending DATA submit and feed it through the
193
+ * data_source_read_callback. The carrier is destroyed once nghttp2
194
+ * signals the frame's full transmission via on_frame_send. */
195
+ typedef struct {
196
+ GBytes *bytes;
197
+ gsize offset;
198
+ gboolean end_stream;
199
+ } DataSourceCarrier;
200
+
201
+ static DataSourceCarrier *
202
+ data_source_carrier_new (GBytes *bytes, gboolean end_stream)
203
+ {
204
+ DataSourceCarrier *c = g_new0 (DataSourceCarrier, 1);
205
+ c->bytes = bytes ? g_bytes_ref (bytes) : g_bytes_new (NULL, 0);
206
+ c->offset = 0;
207
+ c->end_stream = end_stream;
208
+ return c;
209
+ }
210
+
211
+ static void
212
+ data_source_carrier_free (gpointer p)
213
+ {
214
+ DataSourceCarrier *c = p;
215
+ if (!c) return;
216
+ if (c->bytes) g_bytes_unref (c->bytes);
217
+ g_free (c);
218
+ }
219
+
220
+ struct _GjsifyHttp2Session {
221
+ nghttp2_session *session;
222
+ GjsifyHttp2SessionMode mode;
223
+
224
+ /* stream_id (gpointer key) -> StreamHeaderAccum */
225
+ GHashTable *header_accumulators;
226
+
227
+ /* stream_id (gpointer key) -> DataSourceCarrier (only while a DATA
228
+ * provider is active for that stream). */
229
+ GHashTable *data_carriers;
230
+
231
+ /* GQueue<GjsifyHttp2Event*> — drained on demand. */
232
+ GQueue *events;
233
+ };
234
+
235
+ /* ── helpers ─────────────────────────────────────────────────────────── */
236
+
237
+ static void
238
+ push_event (GjsifyHttp2Session *self, GjsifyHttp2Event *ev)
239
+ {
240
+ g_queue_push_tail (self->events, ev);
241
+ }
242
+
243
+ static StreamHeaderAccum *
244
+ accum_get_or_create (GjsifyHttp2Session *self, guint32 stream_id)
245
+ {
246
+ StreamHeaderAccum *a = g_hash_table_lookup (self->header_accumulators,
247
+ GUINT_TO_POINTER (stream_id));
248
+ if (!a) {
249
+ a = stream_header_accum_new ();
250
+ g_hash_table_insert (self->header_accumulators,
251
+ GUINT_TO_POINTER (stream_id), a);
252
+ }
253
+ return a;
254
+ }
255
+
256
+ /* Move accumulated headers into a freshly-allocated NULL-terminated
257
+ * C-string array. Caller owns the returned array AND each string
258
+ * (g_strfreev frees both correctly). */
259
+ static void
260
+ accum_take_headers (StreamHeaderAccum *a,
261
+ char ***out_names,
262
+ char ***out_values,
263
+ gsize *out_n)
264
+ {
265
+ gsize n = a->names->len;
266
+ char **names = g_new0 (char *, n + 1);
267
+ char **values = g_new0 (char *, n + 1);
268
+ for (gsize i = 0; i < n; i++) {
269
+ names[i] = g_strdup ((const char *) g_ptr_array_index (a->names, i));
270
+ values[i] = g_strdup ((const char *) g_ptr_array_index (a->values, i));
271
+ }
272
+ *out_names = names;
273
+ *out_values = values;
274
+ *out_n = n;
275
+ }
276
+
277
+ /* ── nghttp2 callbacks ───────────────────────────────────────────────── */
278
+
279
+ /* For HEADERS, the headers belong to frame->hd.stream_id. For PUSH_PROMISE,
280
+ * they describe the resource on the PROMISED stream id (the parent stream
281
+ * id is the request the push is associated with). Keep one accumulator per
282
+ * logical stream so PUSH_PROMISE-on-the-client surfaces a complete header
283
+ * set keyed by the promised id. */
284
+ static guint32
285
+ accum_key_for_frame (const nghttp2_frame *frame)
286
+ {
287
+ if (frame->hd.type == NGHTTP2_PUSH_PROMISE) {
288
+ return (guint32) frame->push_promise.promised_stream_id;
289
+ }
290
+ return (guint32) frame->hd.stream_id;
291
+ }
292
+
293
+ static int
294
+ on_begin_headers_cb (nghttp2_session *session,
295
+ const nghttp2_frame *frame,
296
+ void *user_data)
297
+ {
298
+ GjsifyHttp2Session *self = user_data;
299
+ (void) session;
300
+ guint32 key = accum_key_for_frame (frame);
301
+ /* Reset/create accumulator for the logical stream. Closing+reopening
302
+ * the same id is illegal in HTTP/2, so destroy-then-create is safe. */
303
+ g_hash_table_remove (self->header_accumulators, GUINT_TO_POINTER (key));
304
+ accum_get_or_create (self, key);
305
+ return 0;
306
+ }
307
+
308
+ static int
309
+ on_header_cb (nghttp2_session *session,
310
+ const nghttp2_frame *frame,
311
+ const uint8_t *name, size_t namelen,
312
+ const uint8_t *value, size_t valuelen,
313
+ uint8_t flags,
314
+ void *user_data)
315
+ {
316
+ GjsifyHttp2Session *self = user_data;
317
+ (void) session;
318
+ (void) flags;
319
+ guint32 key = accum_key_for_frame (frame);
320
+ StreamHeaderAccum *a = accum_get_or_create (self, key);
321
+ /* g_ptr_array owns via g_free — store NUL-terminated copies. */
322
+ g_ptr_array_add (a->names, g_strndup ((const char *) name, namelen));
323
+ g_ptr_array_add (a->values, g_strndup ((const char *) value, valuelen));
324
+ return 0;
325
+ }
326
+
327
+ static int
328
+ on_frame_recv_cb (nghttp2_session *session,
329
+ const nghttp2_frame *frame,
330
+ void *user_data)
331
+ {
332
+ GjsifyHttp2Session *self = user_data;
333
+ (void) session;
334
+ switch (frame->hd.type) {
335
+ case NGHTTP2_HEADERS: {
336
+ StreamHeaderAccum *a = g_hash_table_lookup (self->header_accumulators,
337
+ GUINT_TO_POINTER (frame->hd.stream_id));
338
+ if (!a) return 0;
339
+ GjsifyHttp2Event *ev = g_new0 (GjsifyHttp2Event, 1);
340
+ ev->kind = GJSIFY_HTTP2_EVENT_HEADERS;
341
+ ev->stream_id = frame->hd.stream_id;
342
+ ev->end_stream = (frame->hd.flags & NGHTTP2_FLAG_END_STREAM) != 0;
343
+ accum_take_headers (a, &ev->headers_names, &ev->headers_values, &ev->headers_count);
344
+ /* Once consumed, drop the accumulator. */
345
+ g_hash_table_remove (self->header_accumulators,
346
+ GUINT_TO_POINTER (frame->hd.stream_id));
347
+ push_event (self, ev);
348
+ break;
349
+ }
350
+
351
+ case NGHTTP2_DATA: {
352
+ /* DATA chunks are surfaced via on_data_chunk_recv; here we only
353
+ * need to notice END_STREAM on a frame carrying no chunks. */
354
+ if (frame->hd.flags & NGHTTP2_FLAG_END_STREAM) {
355
+ GjsifyHttp2Event *ev = g_new0 (GjsifyHttp2Event, 1);
356
+ ev->kind = GJSIFY_HTTP2_EVENT_DATA;
357
+ ev->stream_id = frame->hd.stream_id;
358
+ ev->end_stream = TRUE;
359
+ ev->data = g_bytes_new (NULL, 0);
360
+ push_event (self, ev);
361
+ }
362
+ break;
363
+ }
364
+
365
+ case NGHTTP2_SETTINGS: {
366
+ if (!(frame->hd.flags & NGHTTP2_FLAG_ACK)) {
367
+ GjsifyHttp2Event *ev = g_new0 (GjsifyHttp2Event, 1);
368
+ ev->kind = GJSIFY_HTTP2_EVENT_SETTINGS;
369
+ push_event (self, ev);
370
+ }
371
+ break;
372
+ }
373
+
374
+ case NGHTTP2_GOAWAY: {
375
+ GjsifyHttp2Event *ev = g_new0 (GjsifyHttp2Event, 1);
376
+ ev->kind = GJSIFY_HTTP2_EVENT_GOAWAY;
377
+ ev->error_code = frame->goaway.error_code;
378
+ ev->last_stream_id = frame->goaway.last_stream_id;
379
+ push_event (self, ev);
380
+ break;
381
+ }
382
+
383
+ case NGHTTP2_PUSH_PROMISE: {
384
+ /* Client-side: a push promise arrived. Headers were already
385
+ * accumulated by the begin/on_header callbacks for the PROMISED
386
+ * stream id (frame->push_promise.promised_stream_id). */
387
+ guint32 promised = frame->push_promise.promised_stream_id;
388
+ StreamHeaderAccum *a = g_hash_table_lookup (self->header_accumulators,
389
+ GUINT_TO_POINTER (promised));
390
+ if (a) {
391
+ GjsifyHttp2Event *ev = g_new0 (GjsifyHttp2Event, 1);
392
+ ev->kind = GJSIFY_HTTP2_EVENT_PUSH_PROMISE;
393
+ ev->stream_id = frame->hd.stream_id;
394
+ ev->promised_stream_id = promised;
395
+ accum_take_headers (a, &ev->headers_names, &ev->headers_values, &ev->headers_count);
396
+ g_hash_table_remove (self->header_accumulators,
397
+ GUINT_TO_POINTER (promised));
398
+ push_event (self, ev);
399
+ }
400
+ break;
401
+ }
402
+
403
+ default:
404
+ break;
405
+ }
406
+
407
+ return 0;
408
+ }
409
+
410
+ static int
411
+ on_data_chunk_recv_cb (nghttp2_session *session,
412
+ uint8_t flags,
413
+ int32_t stream_id,
414
+ const uint8_t *data, size_t len,
415
+ void *user_data)
416
+ {
417
+ GjsifyHttp2Session *self = user_data;
418
+ (void) session;
419
+ (void) flags;
420
+
421
+ GjsifyHttp2Event *ev = g_new0 (GjsifyHttp2Event, 1);
422
+ ev->kind = GJSIFY_HTTP2_EVENT_DATA;
423
+ ev->stream_id = (guint32) stream_id;
424
+ ev->end_stream = FALSE; /* END_STREAM is reported via on_frame_recv */
425
+ ev->data = g_bytes_new (data, len);
426
+ push_event (self, ev);
427
+ return 0;
428
+ }
429
+
430
+ static int
431
+ on_stream_close_cb (nghttp2_session *session,
432
+ int32_t stream_id,
433
+ uint32_t error_code,
434
+ void *user_data)
435
+ {
436
+ GjsifyHttp2Session *self = user_data;
437
+ (void) session;
438
+
439
+ GjsifyHttp2Event *ev = g_new0 (GjsifyHttp2Event, 1);
440
+ ev->kind = GJSIFY_HTTP2_EVENT_STREAM_CLOSED;
441
+ ev->stream_id = (guint32) stream_id;
442
+ ev->error_code = error_code;
443
+ push_event (self, ev);
444
+
445
+ /* Drop any leftover state for this stream. */
446
+ g_hash_table_remove (self->header_accumulators,
447
+ GUINT_TO_POINTER ((guint32) stream_id));
448
+ g_hash_table_remove (self->data_carriers,
449
+ GUINT_TO_POINTER ((guint32) stream_id));
450
+ return 0;
451
+ }
452
+
453
+ /* DATA provider — feeds bytes from the carrier we attached for this
454
+ * stream id. We look the carrier up by stream_id in self->data_carriers
455
+ * (not via source.ptr) so submit_data() can replace the carrier mid-
456
+ * stream without touching nghttp2's data_provider struct. */
457
+ static ssize_t
458
+ data_source_read_cb (nghttp2_session *session,
459
+ int32_t stream_id,
460
+ uint8_t *buf, size_t length,
461
+ uint32_t *data_flags,
462
+ nghttp2_data_source *source,
463
+ void *user_data)
464
+ {
465
+ (void) session;
466
+ (void) source;
467
+ GjsifyHttp2Session *self = user_data;
468
+
469
+ DataSourceCarrier *c = g_hash_table_lookup (self->data_carriers,
470
+ GUINT_TO_POINTER ((guint32) stream_id));
471
+ if (!c || !c->bytes) {
472
+ /* No data queued yet — defer. nghttp2 will pause this stream's
473
+ * DATA emission and resume on nghttp2_session_resume_data(). */
474
+ return NGHTTP2_ERR_DEFERRED;
475
+ }
476
+
477
+ gsize total = 0;
478
+ const guint8 *base = g_bytes_get_data (c->bytes, &total);
479
+ gsize remaining = total - c->offset;
480
+ gsize chunk = remaining < length ? remaining : length;
481
+ if (chunk > 0) memcpy (buf, base + c->offset, chunk);
482
+ c->offset += chunk;
483
+
484
+ if (c->offset >= total) {
485
+ if (c->end_stream) {
486
+ *data_flags |= NGHTTP2_DATA_FLAG_EOF;
487
+ } else {
488
+ /* Buffer drained but not the last chunk — defer until the
489
+ * next submit_data() replaces the carrier. */
490
+ return chunk > 0 ? (ssize_t) chunk : NGHTTP2_ERR_DEFERRED;
491
+ }
492
+ }
493
+ return (ssize_t) chunk;
494
+ }
495
+
496
+ /* When nghttp2 finishes transmitting a DATA frame whose source is one of
497
+ * our carriers AND it was the last (EOF reached), drop the carrier. */
498
+ static int
499
+ on_frame_send_cb (nghttp2_session *session,
500
+ const nghttp2_frame *frame,
501
+ void *user_data)
502
+ {
503
+ GjsifyHttp2Session *self = user_data;
504
+ (void) session;
505
+ if (frame->hd.type != NGHTTP2_DATA) return 0;
506
+ DataSourceCarrier *c = g_hash_table_lookup (self->data_carriers,
507
+ GUINT_TO_POINTER (frame->hd.stream_id));
508
+ if (!c) return 0;
509
+ gsize total = 0;
510
+ g_bytes_get_data (c->bytes, &total);
511
+ if (c->offset >= total) {
512
+ g_hash_table_remove (self->data_carriers,
513
+ GUINT_TO_POINTER (frame->hd.stream_id));
514
+ }
515
+ return 0;
516
+ }
517
+
518
+ /* ── construction / destruction ──────────────────────────────────────── */
519
+
520
+ static int
521
+ configure_callbacks (nghttp2_session_callbacks *cb)
522
+ {
523
+ nghttp2_session_callbacks_set_on_frame_recv_callback (cb, on_frame_recv_cb);
524
+ nghttp2_session_callbacks_set_on_data_chunk_recv_callback (cb, on_data_chunk_recv_cb);
525
+ nghttp2_session_callbacks_set_on_stream_close_callback (cb, on_stream_close_cb);
526
+ nghttp2_session_callbacks_set_on_header_callback (cb, on_header_cb);
527
+ nghttp2_session_callbacks_set_on_begin_headers_callback (cb, on_begin_headers_cb);
528
+ nghttp2_session_callbacks_set_on_frame_send_callback (cb, on_frame_send_cb);
529
+ return 0;
530
+ }
531
+
532
+ GjsifyHttp2Session *
533
+ gjsify_http2_session_new (GjsifyHttp2SessionMode mode)
534
+ {
535
+ GjsifyHttp2Session *self = g_new0 (GjsifyHttp2Session, 1);
536
+ self->mode = mode;
537
+ self->header_accumulators = g_hash_table_new_full (
538
+ g_direct_hash, g_direct_equal, NULL, stream_header_accum_free);
539
+ self->data_carriers = g_hash_table_new_full (
540
+ g_direct_hash, g_direct_equal, NULL, data_source_carrier_free);
541
+ self->events = g_queue_new ();
542
+
543
+ nghttp2_session_callbacks *cb = NULL;
544
+ if (nghttp2_session_callbacks_new (&cb) != 0 || cb == NULL) {
545
+ gjsify_http2_session_free (self);
546
+ return NULL;
547
+ }
548
+ configure_callbacks (cb);
549
+
550
+ int rv;
551
+ if (mode == GJSIFY_HTTP2_SESSION_MODE_SERVER) {
552
+ rv = nghttp2_session_server_new (&self->session, cb, self);
553
+ } else {
554
+ rv = nghttp2_session_client_new (&self->session, cb, self);
555
+ }
556
+ nghttp2_session_callbacks_del (cb);
557
+
558
+ if (rv != 0 || self->session == NULL) {
559
+ gjsify_http2_session_free (self);
560
+ return NULL;
561
+ }
562
+ return self;
563
+ }
564
+
565
+ void
566
+ gjsify_http2_session_free (GjsifyHttp2Session *self)
567
+ {
568
+ if (!self) return;
569
+ if (self->session) nghttp2_session_del (self->session);
570
+ if (self->header_accumulators) g_hash_table_destroy (self->header_accumulators);
571
+ if (self->data_carriers) g_hash_table_destroy (self->data_carriers);
572
+ if (self->events) {
573
+ GjsifyHttp2Event *ev;
574
+ while ((ev = g_queue_pop_head (self->events)) != NULL) {
575
+ gjsify_http2_event_free (ev);
576
+ }
577
+ g_queue_free (self->events);
578
+ }
579
+ g_free (self);
580
+ }
581
+
582
+ void
583
+ gjsify_http2_event_free (GjsifyHttp2Event *event)
584
+ {
585
+ if (!event) return;
586
+ if (event->headers_names) g_strfreev (event->headers_names);
587
+ if (event->headers_values) g_strfreev (event->headers_values);
588
+ if (event->data) g_bytes_unref (event->data);
589
+ g_free (event);
590
+ }
591
+
592
+ /* ── Event getters (see header for ownership rules) ─────────────────── */
593
+
594
+ guint32 gjsify_http2_event_get_kind (GjsifyHttp2Event *e) { return e ? (guint32) e->kind : 0; }
595
+ guint32 gjsify_http2_event_get_stream_id (GjsifyHttp2Event *e) { return e ? e->stream_id : 0; }
596
+ guint32 gjsify_http2_event_get_error_code (GjsifyHttp2Event *e) { return e ? e->error_code : 0; }
597
+ guint32 gjsify_http2_event_get_last_stream_id (GjsifyHttp2Event *e) { return e ? e->last_stream_id : 0; }
598
+ gboolean gjsify_http2_event_get_end_stream (GjsifyHttp2Event *e) { return e ? e->end_stream : FALSE; }
599
+ guint32 gjsify_http2_event_get_promised_stream_id (GjsifyHttp2Event *e) { return e ? e->promised_stream_id : 0; }
600
+ gsize gjsify_http2_event_get_header_count (GjsifyHttp2Event *e) { return e ? e->headers_count : 0; }
601
+
602
+ const char *
603
+ gjsify_http2_event_get_header_name (GjsifyHttp2Event *e, gsize index)
604
+ {
605
+ if (!e || !e->headers_names || index >= e->headers_count) return NULL;
606
+ return e->headers_names[index];
607
+ }
608
+
609
+ const char *
610
+ gjsify_http2_event_get_header_value (GjsifyHttp2Event *e, gsize index)
611
+ {
612
+ if (!e || !e->headers_values || index >= e->headers_count) return NULL;
613
+ return e->headers_values[index];
614
+ }
615
+
616
+ GBytes *
617
+ gjsify_http2_event_get_data (GjsifyHttp2Event *e)
618
+ {
619
+ return e ? e->data : NULL;
620
+ }
621
+
622
+ /* ── I/O ─────────────────────────────────────────────────────────────── */
623
+
624
+ gssize
625
+ gjsify_http2_session_feed (GjsifyHttp2Session *self, GBytes *input)
626
+ {
627
+ if (!self || !self->session) return -1;
628
+ gsize len = 0;
629
+ const guint8 *data = input ? g_bytes_get_data (input, &len) : NULL;
630
+ if (len == 0) return 0;
631
+ ssize_t rv = nghttp2_session_mem_recv (self->session, data, len);
632
+ return (gssize) rv;
633
+ }
634
+
635
+ GBytes *
636
+ gjsify_http2_session_drain_output (GjsifyHttp2Session *self)
637
+ {
638
+ if (!self || !self->session) return g_bytes_new (NULL, 0);
639
+
640
+ GByteArray *accum = g_byte_array_new ();
641
+ const uint8_t *chunk = NULL;
642
+ ssize_t rv;
643
+ while ((rv = nghttp2_session_mem_send (self->session, &chunk)) > 0) {
644
+ g_byte_array_append (accum, chunk, (guint) rv);
645
+ }
646
+ /* rv < 0 indicates an error; we still return whatever we collected
647
+ * so the caller can flush before tearing down. */
648
+
649
+ return g_byte_array_free_to_bytes (accum);
650
+ }
651
+
652
+ GjsifyHttp2Event **
653
+ gjsify_http2_session_drain_events (GjsifyHttp2Session *self, gsize *out_n)
654
+ {
655
+ if (!self) {
656
+ if (out_n) *out_n = 0;
657
+ return NULL;
658
+ }
659
+ gsize n = g_queue_get_length (self->events);
660
+ if (out_n) *out_n = n;
661
+ if (n == 0) return NULL;
662
+
663
+ GjsifyHttp2Event **arr = g_new0 (GjsifyHttp2Event *, n + 1);
664
+ for (gsize i = 0; i < n; i++) {
665
+ arr[i] = g_queue_pop_head (self->events);
666
+ }
667
+ arr[n] = NULL;
668
+ return arr;
669
+ }
670
+
671
+ /* ── submits ─────────────────────────────────────────────────────────── */
672
+
673
+ int
674
+ gjsify_http2_session_submit_settings (GjsifyHttp2Session *self)
675
+ {
676
+ if (!self || !self->session) return -1;
677
+ /* Default-shaped SETTINGS: no entries. nghttp2 sends the empty frame
678
+ * which the peer ACKs; that's sufficient for handshake completion. */
679
+ return nghttp2_submit_settings (self->session, NGHTTP2_FLAG_NONE, NULL, 0);
680
+ }
681
+
682
+ static void
683
+ fill_nva (nghttp2_nv *nva,
684
+ char **names,
685
+ char **values,
686
+ gsize n_pairs)
687
+ {
688
+ for (gsize i = 0; i < n_pairs; i++) {
689
+ const char *n = names[i] ? names[i] : "";
690
+ const char *v = values[i] ? values[i] : "";
691
+ nva[i].name = (uint8_t *) n;
692
+ nva[i].value = (uint8_t *) v;
693
+ nva[i].namelen = strlen (n);
694
+ nva[i].valuelen = strlen (v);
695
+ nva[i].flags = NGHTTP2_NV_FLAG_NONE;
696
+ }
697
+ }
698
+
699
+ int
700
+ gjsify_http2_session_submit_response (GjsifyHttp2Session *self,
701
+ guint32 stream_id,
702
+ char **names,
703
+ char **values,
704
+ gsize n_pairs,
705
+ gboolean end_stream)
706
+ {
707
+ if (!self || !self->session) return -1;
708
+ if (!names || !values) return -1;
709
+
710
+ nghttp2_nv *nva = g_new0 (nghttp2_nv, n_pairs);
711
+ fill_nva (nva, names, values, n_pairs);
712
+
713
+ nghttp2_data_provider *prdp = NULL;
714
+ nghttp2_data_provider prd;
715
+ if (!end_stream) {
716
+ /* Caller will follow up with submit_data(). We do NOT pre-create
717
+ * a carrier — the data_source_read_cb returns NGHTTP2_ERR_DEFERRED
718
+ * until submit_data() replaces the entry, at which point
719
+ * nghttp2_session_resume_data() unblocks the stream. */
720
+ memset (&prd, 0, sizeof (prd));
721
+ prd.source.ptr = NULL;
722
+ prd.read_callback = data_source_read_cb;
723
+ prdp = &prd;
724
+ }
725
+
726
+ int rv = nghttp2_submit_response (self->session, (int32_t) stream_id,
727
+ nva, n_pairs, prdp);
728
+ g_free (nva);
729
+ return rv;
730
+ }
731
+
732
+ guint32
733
+ gjsify_http2_session_submit_request (GjsifyHttp2Session *self,
734
+ char **names,
735
+ char **values,
736
+ gsize n_pairs,
737
+ gboolean end_stream)
738
+ {
739
+ if (!self || !self->session) return 0;
740
+ if (!names || !values) return 0;
741
+
742
+ nghttp2_nv *nva = g_new0 (nghttp2_nv, n_pairs);
743
+ fill_nva (nva, names, values, n_pairs);
744
+
745
+ nghttp2_data_provider *prdp = NULL;
746
+ nghttp2_data_provider prd;
747
+ if (!end_stream) {
748
+ memset (&prd, 0, sizeof (prd));
749
+ prd.source.ptr = NULL;
750
+ prd.read_callback = data_source_read_cb;
751
+ prdp = &prd;
752
+ }
753
+
754
+ int32_t sid = nghttp2_submit_request (self->session, NULL,
755
+ nva, n_pairs, prdp, NULL);
756
+ g_free (nva);
757
+ if (sid < 0) return 0;
758
+ /* No pre-created carrier — submit_data() will lazily attach. */
759
+ return (guint32) sid;
760
+ }
761
+
762
+ int
763
+ gjsify_http2_session_submit_data (GjsifyHttp2Session *self,
764
+ guint32 stream_id,
765
+ GBytes *data,
766
+ gboolean end_stream)
767
+ {
768
+ if (!self || !self->session) return -1;
769
+
770
+ DataSourceCarrier *c = data_source_carrier_new (data, end_stream);
771
+ /* Replace any existing (empty) carrier from submit_response. */
772
+ g_hash_table_replace (self->data_carriers,
773
+ GUINT_TO_POINTER (stream_id), c);
774
+
775
+ /* Resume sending after replacing the data source. nghttp2 supports
776
+ * resume_data only AFTER a previous submit_response set a deferred
777
+ * provider; calling it on a stream with no provider is harmless. */
778
+ nghttp2_session_resume_data (self->session, (int32_t) stream_id);
779
+ return 0;
780
+ }
781
+
782
+ guint32
783
+ gjsify_http2_session_submit_push_promise (GjsifyHttp2Session *self,
784
+ guint32 parent_id,
785
+ char **names,
786
+ char **values,
787
+ gsize n_pairs)
788
+ {
789
+ if (!self || !self->session) return 0;
790
+ if (!names || !values) return 0;
791
+
792
+ nghttp2_nv *nva = g_new0 (nghttp2_nv, n_pairs);
793
+ fill_nva (nva, names, values, n_pairs);
794
+
795
+ int rv = nghttp2_submit_push_promise (self->session,
796
+ NGHTTP2_FLAG_NONE,
797
+ (int32_t) parent_id,
798
+ nva, n_pairs,
799
+ NULL);
800
+ g_free (nva);
801
+ if (rv < 0) return 0;
802
+ return (guint32) rv;
803
+ }
804
+
805
+ int
806
+ gjsify_http2_session_submit_goaway (GjsifyHttp2Session *self,
807
+ guint32 last_stream_id,
808
+ guint32 error_code)
809
+ {
810
+ if (!self || !self->session) return -1;
811
+ return nghttp2_submit_goaway (self->session,
812
+ NGHTTP2_FLAG_NONE,
813
+ (int32_t) last_stream_id,
814
+ error_code,
815
+ NULL, 0);
816
+ }
817
+
818
+ int
819
+ gjsify_http2_session_submit_rst_stream (GjsifyHttp2Session *self,
820
+ guint32 stream_id,
821
+ guint32 error_code)
822
+ {
823
+ if (!self || !self->session) return -1;
824
+ return nghttp2_submit_rst_stream (self->session,
825
+ NGHTTP2_FLAG_NONE,
826
+ (int32_t) stream_id,
827
+ error_code);
828
+ }
829
+
830
+ gboolean
831
+ gjsify_http2_session_want_read (GjsifyHttp2Session *self)
832
+ {
833
+ if (!self || !self->session) return FALSE;
834
+ return nghttp2_session_want_read (self->session) != 0;
835
+ }
836
+
837
+ gboolean
838
+ gjsify_http2_session_want_write (GjsifyHttp2Session *self)
839
+ {
840
+ if (!self || !self->session) return FALSE;
841
+ return nghttp2_session_want_write (self->session) != 0;
842
+ }