@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,241 @@
1
+ /*
2
+ * Http2FrameEncoder — HPACK + frame-builder bridge backed by libnghttp2.
3
+ *
4
+ * Why this exists
5
+ * ───────────────
6
+ * libsoup ≥ 3 speaks HTTP/2 internally, but its high-level GIR API
7
+ * (`Soup.Server` / `Soup.ServerMessage`) does not expose the underlying
8
+ * nghttp2 session — so things like PUSH_PROMISE frames, fine-grained
9
+ * flow-control window updates, and explicit stream-IDs are unreachable
10
+ * from JS. nghttp2 itself has no Vala VAPI, and its callback-driven
11
+ * session API is awkward to bind directly.
12
+ *
13
+ * This bridge wraps just the pieces JS-side `@gjsify/http2` needs:
14
+ * • encode_headers() — HPACK-deflate a flat name/value pair array
15
+ * • build_data_frame() — wrap arbitrary bytes in a DATA frame
16
+ * • build_push_promise() — emit a complete PUSH_PROMISE frame
17
+ *
18
+ * All buffer ownership stays C-side via `GLib.Bytes` to keep the
19
+ * SpiderMonkey GC out of nghttp2's allocations — same pattern as
20
+ * `@gjsify/http-soup-bridge` and `@gjsify/webrtc-native`.
21
+ */
22
+
23
+ namespace GjsifyHttp2 {
24
+
25
+ [CCode (cname = "gjsify_http2_hpack_encode",
26
+ cheader_filename = "nghttp2-helpers.h")]
27
+ private extern GLib.Bytes? _hpack_encode (
28
+ [CCode (array_length = false, array_null_terminated = false)]
29
+ string[] names,
30
+ [CCode (array_length = false, array_null_terminated = false)]
31
+ string[] values,
32
+ size_t n_pairs);
33
+
34
+ [CCode (cname = "gjsify_http2_pack_frame",
35
+ cheader_filename = "nghttp2-helpers.h")]
36
+ private extern GLib.Bytes _pack_frame (uint8 type,
37
+ uint8 flags,
38
+ uint32 stream_id,
39
+ GLib.Bytes payload);
40
+
41
+ [CCode (cname = "gjsify_http2_pack_push_promise",
42
+ cheader_filename = "nghttp2-helpers.h")]
43
+ private extern GLib.Bytes _pack_push_promise (uint32 associated_stream_id,
44
+ uint32 promised_stream_id,
45
+ GLib.Bytes header_block);
46
+
47
+ [CCode (cname = "gjsify_http2_nghttp2_version",
48
+ cheader_filename = "nghttp2-helpers.h")]
49
+ private extern unowned string _nghttp2_version ();
50
+
51
+ /**
52
+ * FrameEncoder — stateless HPACK / frame builder.
53
+ *
54
+ * Each call constructs a fresh nghttp2 deflater (4 KiB dynamic table)
55
+ * for the header block, so callers don't need to track encoder state
56
+ * across multiple pushes. This trades a few microseconds of HPACK
57
+ * setup for simplicity — server-push is not a hot path.
58
+ */
59
+ public class FrameEncoder : GLib.Object {
60
+
61
+ /* Frame types (RFC 7540 §11.2) — exposed as constants so JS
62
+ * code doesn't have to hard-code magic numbers. */
63
+ public const uint8 TYPE_DATA = 0x00;
64
+ public const uint8 TYPE_HEADERS = 0x01;
65
+ public const uint8 TYPE_PRIORITY = 0x02;
66
+ public const uint8 TYPE_RST_STREAM = 0x03;
67
+ public const uint8 TYPE_SETTINGS = 0x04;
68
+ public const uint8 TYPE_PUSH_PROMISE = 0x05;
69
+ public const uint8 TYPE_PING = 0x06;
70
+ public const uint8 TYPE_GOAWAY = 0x07;
71
+ public const uint8 TYPE_WINDOW_UPDATE = 0x08;
72
+
73
+ public const uint8 FLAG_END_STREAM = 0x01;
74
+ public const uint8 FLAG_ACK = 0x01; /* SETTINGS/PING share value */
75
+ public const uint8 FLAG_END_HEADERS = 0x04;
76
+
77
+ /**
78
+ * encode_headers:
79
+ * @names: array of header names (must already be HTTP/2-lowercase)
80
+ * @values: array of header values, same length as @names
81
+ *
82
+ * HPACK-encode the given name/value pairs. Returns the encoded
83
+ * header block as a #GLib.Bytes, or %NULL on error.
84
+ */
85
+ public GLib.Bytes? encode_headers (string[] names, string[] values) {
86
+ if (names.length != values.length) return null;
87
+ return _hpack_encode (names, values, names.length);
88
+ }
89
+
90
+ /**
91
+ * build_data_frame:
92
+ * @stream_id: stream this DATA frame belongs to
93
+ * @end_stream: %TRUE to set END_STREAM flag (last DATA on the stream)
94
+ * @payload: payload bytes (caller must keep ≤ remote MAX_FRAME_SIZE)
95
+ */
96
+ public GLib.Bytes build_data_frame (uint32 stream_id,
97
+ bool end_stream,
98
+ GLib.Bytes payload) {
99
+ uint8 flags = end_stream ? FLAG_END_STREAM : 0;
100
+ return _pack_frame (TYPE_DATA, flags, stream_id, payload);
101
+ }
102
+
103
+ /**
104
+ * build_headers_frame:
105
+ * @stream_id: target stream id
106
+ * @end_stream: set END_STREAM (response with empty body)
107
+ * @end_headers: set END_HEADERS (single-frame header block)
108
+ * @header_block: HPACK-encoded headers from encode_headers()
109
+ */
110
+ public GLib.Bytes build_headers_frame (uint32 stream_id,
111
+ bool end_stream,
112
+ bool end_headers,
113
+ GLib.Bytes header_block) {
114
+ uint8 flags = 0;
115
+ if (end_stream) flags |= FLAG_END_STREAM;
116
+ if (end_headers) flags |= FLAG_END_HEADERS;
117
+ return _pack_frame (TYPE_HEADERS, flags, stream_id, header_block);
118
+ }
119
+
120
+ /**
121
+ * build_push_promise:
122
+ * @associated_stream_id: the request stream this push is associated with
123
+ * @promised_stream_id: the new even-numbered server stream-id (use
124
+ * StreamIdAllocator.next_promised())
125
+ * @header_block: HPACK-encoded request pseudo-headers for the push
126
+ * (`:method`, `:scheme`, `:authority`, `:path` etc.)
127
+ *
128
+ * Builds a complete PUSH_PROMISE frame with END_HEADERS set.
129
+ */
130
+ public GLib.Bytes build_push_promise (uint32 associated_stream_id,
131
+ uint32 promised_stream_id,
132
+ GLib.Bytes header_block) {
133
+ return _pack_push_promise (associated_stream_id, promised_stream_id, header_block);
134
+ }
135
+
136
+ /**
137
+ * build_settings_frame:
138
+ * @ack: %TRUE to emit SETTINGS-ACK (empty payload).
139
+ * @ids: SETTINGS identifier array (e.g. SETTINGS_MAX_CONCURRENT_STREAMS=3)
140
+ * @values: matching values array
141
+ *
142
+ * Builds a SETTINGS frame. Length of @ids must equal @values.
143
+ * RFC 7540 §6.5: payload is N × (16-bit id || 32-bit value).
144
+ */
145
+ public GLib.Bytes build_settings_frame (bool ack, uint16[] ids, uint32[] values) {
146
+ if (ack) {
147
+ return _pack_frame (TYPE_SETTINGS, FLAG_ACK, 0,
148
+ new GLib.Bytes (null));
149
+ }
150
+ int n = (ids.length < values.length) ? ids.length : values.length;
151
+ uint8[] payload = new uint8[n * 6];
152
+ for (int i = 0; i < n; i++) {
153
+ payload[i * 6 + 0] = (uint8)((ids[i] >> 8) & 0xff);
154
+ payload[i * 6 + 1] = (uint8)( ids[i] & 0xff);
155
+ payload[i * 6 + 2] = (uint8)((values[i] >> 24) & 0xff);
156
+ payload[i * 6 + 3] = (uint8)((values[i] >> 16) & 0xff);
157
+ payload[i * 6 + 4] = (uint8)((values[i] >> 8) & 0xff);
158
+ payload[i * 6 + 5] = (uint8)( values[i] & 0xff);
159
+ }
160
+ return _pack_frame (TYPE_SETTINGS, 0, 0, new GLib.Bytes (payload));
161
+ }
162
+
163
+ /**
164
+ * build_window_update_frame:
165
+ * @stream_id: 0 for connection-level, > 0 for per-stream
166
+ * @increment: 31-bit window-size increment
167
+ */
168
+ public GLib.Bytes build_window_update_frame (uint32 stream_id, uint32 increment) {
169
+ uint32 inc = increment & 0x7fffffffu;
170
+ uint8[] payload = new uint8[4];
171
+ payload[0] = (uint8)((inc >> 24) & 0xff);
172
+ payload[1] = (uint8)((inc >> 16) & 0xff);
173
+ payload[2] = (uint8)((inc >> 8) & 0xff);
174
+ payload[3] = (uint8)( inc & 0xff);
175
+ return _pack_frame (TYPE_WINDOW_UPDATE, 0, stream_id, new GLib.Bytes (payload));
176
+ }
177
+
178
+ /**
179
+ * build_ping_frame:
180
+ * @ack: %TRUE to emit PING-ACK (caller MUST echo the payload).
181
+ * @payload: 8-byte opaque data (will be padded/truncated to 8 bytes).
182
+ */
183
+ public GLib.Bytes build_ping_frame (bool ack, GLib.Bytes? payload) {
184
+ uint8[] data = new uint8[8];
185
+ if (payload != null) {
186
+ unowned uint8[] src = payload.get_data ();
187
+ int copy = (src.length < 8) ? src.length : 8;
188
+ for (int i = 0; i < copy; i++) data[i] = src[i];
189
+ }
190
+ return _pack_frame (TYPE_PING, ack ? FLAG_ACK : 0, 0, new GLib.Bytes (data));
191
+ }
192
+
193
+ /**
194
+ * build_rst_stream_frame:
195
+ * @stream_id: stream to reset
196
+ * @error_code: RFC 7540 error code (NO_ERROR=0, PROTOCOL_ERROR=1, ...)
197
+ */
198
+ public GLib.Bytes build_rst_stream_frame (uint32 stream_id, uint32 error_code) {
199
+ uint8[] payload = new uint8[4];
200
+ payload[0] = (uint8)((error_code >> 24) & 0xff);
201
+ payload[1] = (uint8)((error_code >> 16) & 0xff);
202
+ payload[2] = (uint8)((error_code >> 8) & 0xff);
203
+ payload[3] = (uint8)( error_code & 0xff);
204
+ return _pack_frame (TYPE_RST_STREAM, 0, stream_id, new GLib.Bytes (payload));
205
+ }
206
+
207
+ /**
208
+ * build_goaway_frame:
209
+ * @last_stream_id: highest stream id processed (caller tracks)
210
+ * @error_code: RFC 7540 error code
211
+ * @debug_data: optional debug info (often empty)
212
+ */
213
+ public GLib.Bytes build_goaway_frame (uint32 last_stream_id,
214
+ uint32 error_code,
215
+ GLib.Bytes? debug_data) {
216
+ unowned uint8[] dbg = (debug_data != null) ? debug_data.get_data () : new uint8[0];
217
+ uint8[] payload = new uint8[8 + dbg.length];
218
+ uint32 lsi = last_stream_id & 0x7fffffffu;
219
+ payload[0] = (uint8)((lsi >> 24) & 0xff);
220
+ payload[1] = (uint8)((lsi >> 16) & 0xff);
221
+ payload[2] = (uint8)((lsi >> 8) & 0xff);
222
+ payload[3] = (uint8)( lsi & 0xff);
223
+ payload[4] = (uint8)((error_code >> 24) & 0xff);
224
+ payload[5] = (uint8)((error_code >> 16) & 0xff);
225
+ payload[6] = (uint8)((error_code >> 8) & 0xff);
226
+ payload[7] = (uint8)( error_code & 0xff);
227
+ for (int i = 0; i < dbg.length; i++) payload[8 + i] = dbg[i];
228
+ return _pack_frame (TYPE_GOAWAY, 0, 0, new GLib.Bytes (payload));
229
+ }
230
+
231
+ /**
232
+ * nghttp2_version:
233
+ *
234
+ * Returns the libnghttp2 version string the bridge is linked against.
235
+ * Useful for diagnostics + version pinning in error messages.
236
+ */
237
+ public unowned string nghttp2_version () {
238
+ return _nghttp2_version ();
239
+ }
240
+ }
241
+ }