@gjsify/http2 0.3.12 → 0.3.14

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.
package/lib/esm/server.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { constants, getDefaultSettings } from "./protocol.js";
1
2
  import Soup from "@girs/soup-3.0";
2
3
  import Gio from "@girs/gio-2.0";
3
4
  import GLib from "@girs/glib-2.0";
@@ -5,616 +6,594 @@ import { EventEmitter } from "node:events";
5
6
  import { Readable, Writable } from "node:stream";
6
7
  import { Buffer } from "node:buffer";
7
8
  import { deferEmit, ensureMainLoop } from "@gjsify/utils";
8
- import { constants, getDefaultSettings } from "./protocol.js";
9
- class Http2ServerRequest extends Readable {
10
- method = "GET";
11
- url = "/";
12
- headers = {};
13
- rawHeaders = [];
14
- authority = "";
15
- scheme = "https";
16
- httpVersion = "2.0";
17
- httpVersionMajor = 2;
18
- httpVersionMinor = 0;
19
- complete = false;
20
- socket = null;
21
- trailers = {};
22
- rawTrailers = [];
23
- _stream = null;
24
- _timeoutTimer = null;
25
- get stream() {
26
- return this._stream;
27
- }
28
- // Called by Http2Server after stream is created
29
- _setStream(stream) {
30
- this._stream = stream;
31
- }
32
- constructor() {
33
- super();
34
- }
35
- _read(_size) {
36
- }
37
- // 'close' means connection lost, not body-stream end
38
- _autoClose() {
39
- }
40
- _pushBody(body) {
41
- if (body && body.length > 0) {
42
- this.push(Buffer.from(body));
43
- }
44
- this.push(null);
45
- this.complete = true;
46
- if (this._timeoutTimer) {
47
- clearTimeout(this._timeoutTimer);
48
- this._timeoutTimer = null;
49
- }
50
- }
51
- setTimeout(msecs, callback) {
52
- if (this._timeoutTimer) {
53
- clearTimeout(this._timeoutTimer);
54
- this._timeoutTimer = null;
55
- }
56
- if (callback) this.once("timeout", callback);
57
- if (msecs > 0) {
58
- this._timeoutTimer = setTimeout(() => {
59
- this._timeoutTimer = null;
60
- this.emit("timeout");
61
- }, msecs);
62
- }
63
- return this;
64
- }
65
- destroy(error) {
66
- if (this._timeoutTimer) {
67
- clearTimeout(this._timeoutTimer);
68
- this._timeoutTimer = null;
69
- }
70
- return super.destroy(error);
71
- }
72
- }
73
- class Http2ServerResponse extends Writable {
74
- statusCode = 200;
75
- statusMessage = "";
76
- headersSent = false;
77
- finished = false;
78
- sendDate = true;
79
- _soupMsg;
80
- _headers = /* @__PURE__ */ new Map();
81
- _streaming = false;
82
- _timeoutTimer = null;
83
- _stream = null;
84
- get stream() {
85
- return this._stream;
86
- }
87
- get socket() {
88
- return null;
89
- }
90
- // Called by Http2Server after stream is created
91
- _setStream(stream) {
92
- this._stream = stream;
93
- }
94
- constructor(soupMsg) {
95
- super();
96
- this._soupMsg = soupMsg;
97
- }
98
- setHeader(name, value) {
99
- this._headers.set(name.toLowerCase(), typeof value === "number" ? String(value) : value);
100
- return this;
101
- }
102
- getHeader(name) {
103
- return this._headers.get(name.toLowerCase());
104
- }
105
- removeHeader(name) {
106
- this._headers.delete(name.toLowerCase());
107
- }
108
- hasHeader(name) {
109
- return this._headers.has(name.toLowerCase());
110
- }
111
- getHeaderNames() {
112
- return Array.from(this._headers.keys());
113
- }
114
- getHeaders() {
115
- const result = {};
116
- for (const [key, value] of this._headers) {
117
- result[key] = value;
118
- }
119
- return result;
120
- }
121
- appendHeader(name, value) {
122
- const lower = name.toLowerCase();
123
- const existing = this._headers.get(lower);
124
- if (existing === void 0) {
125
- this._headers.set(lower, value);
126
- } else if (Array.isArray(existing)) {
127
- Array.isArray(value) ? existing.push(...value) : existing.push(value);
128
- } else {
129
- this._headers.set(lower, Array.isArray(value) ? [existing, ...value] : [existing, value]);
130
- }
131
- return this;
132
- }
133
- flushHeaders() {
134
- if (!this.headersSent) this.headersSent = true;
135
- }
136
- writeHead(statusCode, statusMessage, headers) {
137
- this.statusCode = statusCode;
138
- if (typeof statusMessage === "object") {
139
- headers = statusMessage;
140
- statusMessage = void 0;
141
- }
142
- if (typeof statusMessage === "string") this.statusMessage = statusMessage;
143
- if (headers) {
144
- for (const [key, value] of Object.entries(headers)) {
145
- this.setHeader(key, value);
146
- }
147
- }
148
- return this;
149
- }
150
- // http2 session-API alias — extracts :status from headers map
151
- respond(headers, options) {
152
- const status = Number(headers[":status"] ?? 200);
153
- const rest = {};
154
- for (const [k, v] of Object.entries(headers)) {
155
- if (k === ":status") continue;
156
- rest[k] = typeof v === "number" ? String(v) : v;
157
- }
158
- this.writeHead(status, rest);
159
- if (options?.endStream) this.end();
160
- }
161
- writeContinue(callback) {
162
- if (callback) Promise.resolve().then(callback);
163
- }
164
- writeEarlyHints(_hints, callback) {
165
- if (callback) Promise.resolve().then(callback);
166
- }
167
- addTrailers(_headers) {
168
- }
169
- setTimeout(msecs, callback) {
170
- if (this._timeoutTimer) {
171
- clearTimeout(this._timeoutTimer);
172
- this._timeoutTimer = null;
173
- }
174
- if (callback) this.once("timeout", callback);
175
- if (msecs > 0) {
176
- this._timeoutTimer = setTimeout(() => {
177
- this._timeoutTimer = null;
178
- this.emit("timeout");
179
- }, msecs);
180
- }
181
- return this;
182
- }
183
- _startStreaming() {
184
- if (this._streaming) return;
185
- this._streaming = true;
186
- this.headersSent = true;
187
- if (this._timeoutTimer) {
188
- clearTimeout(this._timeoutTimer);
189
- this._timeoutTimer = null;
190
- }
191
- this._soupMsg.set_status(this.statusCode, this.statusMessage || null);
192
- const responseHeaders = this._soupMsg.get_response_headers();
193
- if (this._headers.has("content-length")) {
194
- responseHeaders.set_encoding(Soup.Encoding.CONTENT_LENGTH);
195
- } else {
196
- responseHeaders.set_encoding(Soup.Encoding.CHUNKED);
197
- }
198
- for (const [key, value] of this._headers) {
199
- if (Array.isArray(value)) {
200
- for (const v of value) responseHeaders.append(key, v);
201
- } else {
202
- responseHeaders.replace(key, value);
203
- }
204
- }
205
- }
206
- _write(chunk, encoding, callback) {
207
- const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk, encoding);
208
- this._startStreaming();
209
- const responseBody = this._soupMsg.get_response_body();
210
- responseBody.append(new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength));
211
- this._soupMsg.unpause();
212
- callback();
213
- }
214
- _final(callback) {
215
- if (this._streaming) {
216
- const responseBody = this._soupMsg.get_response_body();
217
- responseBody.complete();
218
- this._soupMsg.unpause();
219
- } else {
220
- this._sendBatchResponse();
221
- }
222
- this.finished = true;
223
- callback();
224
- }
225
- _sendBatchResponse() {
226
- if (this.headersSent) return;
227
- this.headersSent = true;
228
- if (this._timeoutTimer) {
229
- clearTimeout(this._timeoutTimer);
230
- this._timeoutTimer = null;
231
- }
232
- this._soupMsg.set_status(this.statusCode, this.statusMessage || null);
233
- const responseHeaders = this._soupMsg.get_response_headers();
234
- for (const [key, value] of this._headers) {
235
- if (Array.isArray(value)) {
236
- for (const v of value) responseHeaders.append(key, v);
237
- } else {
238
- responseHeaders.replace(key, value);
239
- }
240
- }
241
- const contentType = this._headers.get("content-type") || "text/plain";
242
- this._soupMsg.set_response(contentType, Soup.MemoryUse.COPY, new Uint8Array(0));
243
- }
244
- end(chunk, encoding, callback) {
245
- if (typeof chunk === "function") {
246
- callback = chunk;
247
- chunk = void 0;
248
- } else if (typeof encoding === "function") {
249
- callback = encoding;
250
- encoding = void 0;
251
- }
252
- if (chunk != null) {
253
- this.write(chunk, encoding);
254
- }
255
- super.end(callback);
256
- return this;
257
- }
258
- // respondWithFD and respondWithFile stubs (Phase 2)
259
- respondWithFD(_fd, _headers, _options) {
260
- throw new Error("http2 respondWithFD is not yet implemented in GJS (Phase 2)");
261
- }
262
- respondWithFile(_path, _headers, _options) {
263
- throw new Error("http2 respondWithFile is not yet implemented in GJS (Phase 2)");
264
- }
265
- pushStream(_headers, _options, _callback) {
266
- throw new Error("http2 server push is not yet implemented in GJS (Phase 2)");
267
- }
268
- createPushResponse(_headers, _callback) {
269
- throw new Error("http2 server push is not yet implemented in GJS (Phase 2)");
270
- }
271
- }
272
- class ServerHttp2Stream extends EventEmitter {
273
- id = 1;
274
- pushAllowed = false;
275
- sentHeaders = {};
276
- _res;
277
- _session;
278
- get session() {
279
- return this._session;
280
- }
281
- get headersSent() {
282
- return this._res.headersSent;
283
- }
284
- get closed() {
285
- return this._res.writableEnded;
286
- }
287
- get destroyed() {
288
- return this._res.destroyed;
289
- }
290
- get pending() {
291
- return false;
292
- }
293
- get state() {
294
- return this.closed ? constants.NGHTTP2_STREAM_STATE_CLOSED : constants.NGHTTP2_STREAM_STATE_OPEN;
295
- }
296
- constructor(res, session = null) {
297
- super();
298
- this._res = res;
299
- this._session = session;
300
- res.on("finish", () => this.emit("close"));
301
- res.on("error", (err) => this.emit("error", err));
302
- }
303
- // Session API: send response headers
304
- respond(headers, options) {
305
- this._res.respond(headers, options);
306
- }
307
- // Writable-like interface delegating to response
308
- write(chunk, encoding, callback) {
309
- return this._res.write(chunk, encoding, callback);
310
- }
311
- end(chunk, encoding, callback) {
312
- this._res.end(chunk, encoding, callback);
313
- return this;
314
- }
315
- destroy(error) {
316
- this._res.destroy(error);
317
- return this;
318
- }
319
- close(code, callback) {
320
- if (callback) this.once("close", callback);
321
- this._res.end();
322
- }
323
- priority(_options) {
324
- }
325
- setTimeout(msecs, callback) {
326
- this._res.setTimeout(msecs, callback);
327
- return this;
328
- }
329
- sendTrailers(_headers) {
330
- }
331
- additionalHeaders(_headers) {
332
- }
333
- respondWithFD(_fd, _headers, _options) {
334
- throw new Error("http2 respondWithFD is not yet implemented in GJS (Phase 2)");
335
- }
336
- respondWithFile(_path, _headers, _options) {
337
- throw new Error("http2 respondWithFile is not yet implemented in GJS (Phase 2)");
338
- }
339
- pushStream(_headers, _options, _callback) {
340
- throw new Error("http2 server push is not yet implemented in GJS (Phase 2)");
341
- }
342
- }
343
- class ServerHttp2Session extends EventEmitter {
344
- type = constants.NGHTTP2_SESSION_SERVER;
345
- alpnProtocol = "h2";
346
- encrypted = true;
347
- _closed = false;
348
- _destroyed = false;
349
- _settings;
350
- constructor() {
351
- super();
352
- this._settings = getDefaultSettings();
353
- }
354
- get closed() {
355
- return this._closed;
356
- }
357
- get destroyed() {
358
- return this._destroyed;
359
- }
360
- get pendingSettingsAck() {
361
- return false;
362
- }
363
- get localSettings() {
364
- return { ...this._settings };
365
- }
366
- get remoteSettings() {
367
- return getDefaultSettings();
368
- }
369
- get originSet() {
370
- return [];
371
- }
372
- settings(settings, callback) {
373
- Object.assign(this._settings, settings);
374
- if (callback) Promise.resolve().then(callback);
375
- }
376
- goaway(code, _lastStreamId, _data) {
377
- this.emit("goaway", code ?? constants.NGHTTP2_NO_ERROR);
378
- this.destroy();
379
- }
380
- ping(_payload, callback) {
381
- const buf = new Uint8Array(8);
382
- if (callback) Promise.resolve().then(() => callback(null, 0, buf));
383
- return true;
384
- }
385
- close(callback) {
386
- if (this._closed) return;
387
- this._closed = true;
388
- this.emit("close");
389
- if (callback) callback();
390
- }
391
- destroy(error, code) {
392
- if (this._destroyed) return;
393
- this._destroyed = true;
394
- this._closed = true;
395
- if (error) this.emit("error", error);
396
- if (code !== void 0) this.emit("goaway", code);
397
- this.emit("close");
398
- }
399
- altsvc(_alt, _originOrStream) {
400
- }
401
- origin(..._origins) {
402
- }
403
- ref() {
404
- }
405
- unref() {
406
- }
407
- }
408
- const _activeServers = /* @__PURE__ */ new Set();
409
- class Http2Server extends EventEmitter {
410
- listening = false;
411
- maxHeadersCount = 2e3;
412
- timeout = 0;
413
- _soupServer = null;
414
- _address = null;
415
- _options;
416
- get soupServer() {
417
- return this._soupServer;
418
- }
419
- constructor(options, handler) {
420
- super();
421
- if (typeof options === "function") {
422
- handler = options;
423
- options = {};
424
- }
425
- this._options = options ?? {};
426
- if (handler) this.on("request", handler);
427
- }
428
- listen(...args) {
429
- let port = 0;
430
- let hostname = "0.0.0.0";
431
- let callback;
432
- for (const arg of args) {
433
- if (typeof arg === "number") port = arg;
434
- else if (typeof arg === "string") hostname = arg;
435
- else if (typeof arg === "function") callback = arg;
436
- }
437
- if (callback) this.once("listening", callback);
438
- try {
439
- this._soupServer = new Soup.Server({});
440
- this._configureSoupServer(this._soupServer);
441
- this._soupServer.add_handler(null, (_server, msg, _path) => {
442
- this._handleRequest(msg);
443
- });
444
- this._soupServer.listen_local(port, Soup.ServerListenOptions.IPV4_ONLY);
445
- ensureMainLoop();
446
- const listeners = this._soupServer.get_listeners();
447
- let actualPort = port;
448
- if (listeners && listeners.length > 0) {
449
- const addr = listeners[0].get_local_address();
450
- if (addr && typeof addr.get_port === "function") {
451
- actualPort = addr.get_port();
452
- }
453
- }
454
- this.listening = true;
455
- this._address = { port: actualPort, family: "IPv4", address: hostname };
456
- _activeServers.add(this);
457
- deferEmit(this, "listening");
458
- } catch (err) {
459
- const error = err instanceof Error ? err : new Error(String(err));
460
- if (this.listenerCount("error") === 0) throw error;
461
- deferEmit(this, "error", error);
462
- }
463
- return this;
464
- }
465
- // Override in Http2SecureServer to set TLS certificate before listen
466
- _configureSoupServer(_server) {
467
- }
468
- _handleRequest(soupMsg) {
469
- const req = new Http2ServerRequest();
470
- const res = new Http2ServerResponse(soupMsg);
471
- req.method = soupMsg.get_method();
472
- const uri = soupMsg.get_uri();
473
- const path = uri.get_path();
474
- const query = uri.get_query();
475
- req.url = query ? path + "?" + query : path;
476
- req.authority = uri.get_host() ?? "";
477
- req.scheme = uri.get_scheme() ?? "http";
478
- const httpVersion = soupMsg.get_http_version();
479
- if (httpVersion === Soup.HTTPVersion.HTTP_2_0) {
480
- req.httpVersion = "2.0";
481
- req.httpVersionMajor = 2;
482
- req.httpVersionMinor = 0;
483
- } else {
484
- req.httpVersion = "1.1";
485
- req.httpVersionMajor = 1;
486
- req.httpVersionMinor = 1;
487
- }
488
- const requestHeaders = soupMsg.get_request_headers();
489
- requestHeaders.foreach((name, value) => {
490
- const lower = name.toLowerCase();
491
- req.rawHeaders.push(name, value);
492
- if (lower in req.headers) {
493
- const existing = req.headers[lower];
494
- if (Array.isArray(existing)) {
495
- existing.push(value);
496
- } else {
497
- req.headers[lower] = [existing, value];
498
- }
499
- } else {
500
- req.headers[lower] = value;
501
- }
502
- });
503
- const remoteHost = soupMsg.get_remote_host() ?? "127.0.0.1";
504
- const remoteAddr = soupMsg.get_remote_address();
505
- const remotePort = remoteAddr instanceof Gio.InetSocketAddress ? remoteAddr.get_port() : 0;
506
- req.socket = {
507
- remoteAddress: remoteHost,
508
- remotePort,
509
- localAddress: this._address?.address ?? "127.0.0.1",
510
- localPort: this._address?.port ?? 0,
511
- encrypted: this instanceof Http2SecureServer
512
- };
513
- const body = soupMsg.get_request_body();
514
- if (body?.data && body.data.length > 0) {
515
- req._pushBody(body.data);
516
- } else {
517
- req._pushBody(null);
518
- }
519
- const streamHeaders = {
520
- ":method": req.method,
521
- ":path": req.url,
522
- ":authority": req.authority,
523
- ":scheme": req.scheme,
524
- ...req.headers
525
- };
526
- soupMsg.pause();
527
- res.on("finish", () => soupMsg.unpause());
528
- const session = new ServerHttp2Session();
529
- const stream = new ServerHttp2Stream(res, session);
530
- req._setStream(stream);
531
- res._setStream(stream);
532
- this.emit("stream", stream, streamHeaders);
533
- this.emit("request", req, res);
534
- }
535
- address() {
536
- return this._address;
537
- }
538
- close(callback) {
539
- if (callback) this.once("close", callback);
540
- if (this._soupServer) {
541
- this._soupServer.disconnect();
542
- this._soupServer = null;
543
- }
544
- this.listening = false;
545
- _activeServers.delete(this);
546
- deferEmit(this, "close");
547
- return this;
548
- }
549
- setTimeout(msecs, callback) {
550
- this.timeout = msecs;
551
- if (callback) this.on("timeout", callback);
552
- return this;
553
- }
554
- }
555
- class Http2SecureServer extends Http2Server {
556
- _tlsCert = null;
557
- constructor(options, handler) {
558
- super(options, handler);
559
- if (options.cert && options.key) {
560
- const certPem = _toPemString(options.cert);
561
- const keyPem = _toPemString(options.key);
562
- this._tlsCert = _createTlsCertificate(certPem, keyPem);
563
- } else if (options.pfx) {
564
- }
565
- }
566
- _configureSoupServer(server) {
567
- if (this._tlsCert) {
568
- server.set_tls_certificate(this._tlsCert);
569
- }
570
- }
571
- setSecureContext(options) {
572
- if (options.cert && options.key) {
573
- const certPem = _toPemString(options.cert);
574
- const keyPem = _toPemString(options.key);
575
- this._tlsCert = _createTlsCertificate(certPem, keyPem);
576
- if (this._soupServer && this._tlsCert) {
577
- this._soupServer.set_tls_certificate(this._tlsCert);
578
- }
579
- }
580
- }
581
- }
9
+
10
+ //#region src/server.ts
11
+ var Http2ServerRequest = class extends Readable {
12
+ method = "GET";
13
+ url = "/";
14
+ headers = {};
15
+ rawHeaders = [];
16
+ authority = "";
17
+ scheme = "https";
18
+ httpVersion = "2.0";
19
+ httpVersionMajor = 2;
20
+ httpVersionMinor = 0;
21
+ complete = false;
22
+ socket = null;
23
+ trailers = {};
24
+ rawTrailers = [];
25
+ _stream = null;
26
+ _timeoutTimer = null;
27
+ get stream() {
28
+ return this._stream;
29
+ }
30
+ _setStream(stream) {
31
+ this._stream = stream;
32
+ }
33
+ constructor() {
34
+ super();
35
+ }
36
+ _read(_size) {}
37
+ _autoClose() {}
38
+ _pushBody(body) {
39
+ if (body && body.length > 0) {
40
+ this.push(Buffer.from(body));
41
+ }
42
+ this.push(null);
43
+ this.complete = true;
44
+ if (this._timeoutTimer) {
45
+ clearTimeout(this._timeoutTimer);
46
+ this._timeoutTimer = null;
47
+ }
48
+ }
49
+ setTimeout(msecs, callback) {
50
+ if (this._timeoutTimer) {
51
+ clearTimeout(this._timeoutTimer);
52
+ this._timeoutTimer = null;
53
+ }
54
+ if (callback) this.once("timeout", callback);
55
+ if (msecs > 0) {
56
+ this._timeoutTimer = setTimeout(() => {
57
+ this._timeoutTimer = null;
58
+ this.emit("timeout");
59
+ }, msecs);
60
+ }
61
+ return this;
62
+ }
63
+ destroy(error) {
64
+ if (this._timeoutTimer) {
65
+ clearTimeout(this._timeoutTimer);
66
+ this._timeoutTimer = null;
67
+ }
68
+ return super.destroy(error);
69
+ }
70
+ };
71
+ var Http2ServerResponse = class extends Writable {
72
+ statusCode = 200;
73
+ statusMessage = "";
74
+ headersSent = false;
75
+ finished = false;
76
+ sendDate = true;
77
+ _soupMsg;
78
+ _headers = new Map();
79
+ _streaming = false;
80
+ _timeoutTimer = null;
81
+ _stream = null;
82
+ get stream() {
83
+ return this._stream;
84
+ }
85
+ get socket() {
86
+ return null;
87
+ }
88
+ _setStream(stream) {
89
+ this._stream = stream;
90
+ }
91
+ constructor(soupMsg) {
92
+ super();
93
+ this._soupMsg = soupMsg;
94
+ }
95
+ setHeader(name, value) {
96
+ this._headers.set(name.toLowerCase(), typeof value === "number" ? String(value) : value);
97
+ return this;
98
+ }
99
+ getHeader(name) {
100
+ return this._headers.get(name.toLowerCase());
101
+ }
102
+ removeHeader(name) {
103
+ this._headers.delete(name.toLowerCase());
104
+ }
105
+ hasHeader(name) {
106
+ return this._headers.has(name.toLowerCase());
107
+ }
108
+ getHeaderNames() {
109
+ return Array.from(this._headers.keys());
110
+ }
111
+ getHeaders() {
112
+ const result = {};
113
+ for (const [key, value] of this._headers) {
114
+ result[key] = value;
115
+ }
116
+ return result;
117
+ }
118
+ appendHeader(name, value) {
119
+ const lower = name.toLowerCase();
120
+ const existing = this._headers.get(lower);
121
+ if (existing === undefined) {
122
+ this._headers.set(lower, value);
123
+ } else if (Array.isArray(existing)) {
124
+ Array.isArray(value) ? existing.push(...value) : existing.push(value);
125
+ } else {
126
+ this._headers.set(lower, Array.isArray(value) ? [existing, ...value] : [existing, value]);
127
+ }
128
+ return this;
129
+ }
130
+ flushHeaders() {
131
+ if (!this.headersSent) this.headersSent = true;
132
+ }
133
+ writeHead(statusCode, statusMessage, headers) {
134
+ this.statusCode = statusCode;
135
+ if (typeof statusMessage === "object") {
136
+ headers = statusMessage;
137
+ statusMessage = undefined;
138
+ }
139
+ if (typeof statusMessage === "string") this.statusMessage = statusMessage;
140
+ if (headers) {
141
+ for (const [key, value] of Object.entries(headers)) {
142
+ this.setHeader(key, value);
143
+ }
144
+ }
145
+ return this;
146
+ }
147
+ respond(headers, options) {
148
+ const status = Number(headers[":status"] ?? 200);
149
+ const rest = {};
150
+ for (const [k, v] of Object.entries(headers)) {
151
+ if (k === ":status") continue;
152
+ rest[k] = typeof v === "number" ? String(v) : v;
153
+ }
154
+ this.writeHead(status, rest);
155
+ if (options?.endStream) this.end();
156
+ }
157
+ writeContinue(callback) {
158
+ if (callback) Promise.resolve().then(callback);
159
+ }
160
+ writeEarlyHints(_hints, callback) {
161
+ if (callback) Promise.resolve().then(callback);
162
+ }
163
+ addTrailers(_headers) {}
164
+ setTimeout(msecs, callback) {
165
+ if (this._timeoutTimer) {
166
+ clearTimeout(this._timeoutTimer);
167
+ this._timeoutTimer = null;
168
+ }
169
+ if (callback) this.once("timeout", callback);
170
+ if (msecs > 0) {
171
+ this._timeoutTimer = setTimeout(() => {
172
+ this._timeoutTimer = null;
173
+ this.emit("timeout");
174
+ }, msecs);
175
+ }
176
+ return this;
177
+ }
178
+ _startStreaming() {
179
+ if (this._streaming) return;
180
+ this._streaming = true;
181
+ this.headersSent = true;
182
+ if (this._timeoutTimer) {
183
+ clearTimeout(this._timeoutTimer);
184
+ this._timeoutTimer = null;
185
+ }
186
+ this._soupMsg.set_status(this.statusCode, this.statusMessage || null);
187
+ const responseHeaders = this._soupMsg.get_response_headers();
188
+ if (this._headers.has("content-length")) {
189
+ responseHeaders.set_encoding(Soup.Encoding.CONTENT_LENGTH);
190
+ } else {
191
+ responseHeaders.set_encoding(Soup.Encoding.CHUNKED);
192
+ }
193
+ for (const [key, value] of this._headers) {
194
+ if (Array.isArray(value)) {
195
+ for (const v of value) responseHeaders.append(key, v);
196
+ } else {
197
+ responseHeaders.replace(key, value);
198
+ }
199
+ }
200
+ }
201
+ _write(chunk, encoding, callback) {
202
+ const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk, encoding);
203
+ this._startStreaming();
204
+ const responseBody = this._soupMsg.get_response_body();
205
+ responseBody.append(new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength));
206
+ this._soupMsg.unpause();
207
+ callback();
208
+ }
209
+ _final(callback) {
210
+ if (this._streaming) {
211
+ const responseBody = this._soupMsg.get_response_body();
212
+ responseBody.complete();
213
+ this._soupMsg.unpause();
214
+ } else {
215
+ this._sendBatchResponse();
216
+ }
217
+ this.finished = true;
218
+ callback();
219
+ }
220
+ _sendBatchResponse() {
221
+ if (this.headersSent) return;
222
+ this.headersSent = true;
223
+ if (this._timeoutTimer) {
224
+ clearTimeout(this._timeoutTimer);
225
+ this._timeoutTimer = null;
226
+ }
227
+ this._soupMsg.set_status(this.statusCode, this.statusMessage || null);
228
+ const responseHeaders = this._soupMsg.get_response_headers();
229
+ for (const [key, value] of this._headers) {
230
+ if (Array.isArray(value)) {
231
+ for (const v of value) responseHeaders.append(key, v);
232
+ } else {
233
+ responseHeaders.replace(key, value);
234
+ }
235
+ }
236
+ const contentType = this._headers.get("content-type") || "text/plain";
237
+ this._soupMsg.set_response(contentType, Soup.MemoryUse.COPY, new Uint8Array(0));
238
+ }
239
+ end(chunk, encoding, callback) {
240
+ if (typeof chunk === "function") {
241
+ callback = chunk;
242
+ chunk = undefined;
243
+ } else if (typeof encoding === "function") {
244
+ callback = encoding;
245
+ encoding = undefined;
246
+ }
247
+ if (chunk != null) {
248
+ this.write(chunk, encoding);
249
+ }
250
+ super.end(callback);
251
+ return this;
252
+ }
253
+ respondWithFD(_fd, _headers, _options) {
254
+ throw new Error("http2 respondWithFD is not yet implemented in GJS (Phase 2)");
255
+ }
256
+ respondWithFile(_path, _headers, _options) {
257
+ throw new Error("http2 respondWithFile is not yet implemented in GJS (Phase 2)");
258
+ }
259
+ pushStream(_headers, _options, _callback) {
260
+ throw new Error("http2 server push is not yet implemented in GJS (Phase 2)");
261
+ }
262
+ createPushResponse(_headers, _callback) {
263
+ throw new Error("http2 server push is not yet implemented in GJS (Phase 2)");
264
+ }
265
+ };
266
+ var ServerHttp2Stream = class extends EventEmitter {
267
+ id = 1;
268
+ pushAllowed = false;
269
+ sentHeaders = {};
270
+ _res;
271
+ _session;
272
+ get session() {
273
+ return this._session;
274
+ }
275
+ get headersSent() {
276
+ return this._res.headersSent;
277
+ }
278
+ get closed() {
279
+ return this._res.writableEnded;
280
+ }
281
+ get destroyed() {
282
+ return this._res.destroyed;
283
+ }
284
+ get pending() {
285
+ return false;
286
+ }
287
+ get state() {
288
+ return this.closed ? constants.NGHTTP2_STREAM_STATE_CLOSED : constants.NGHTTP2_STREAM_STATE_OPEN;
289
+ }
290
+ constructor(res, session = null) {
291
+ super();
292
+ this._res = res;
293
+ this._session = session;
294
+ res.on("finish", () => this.emit("close"));
295
+ res.on("error", (err) => this.emit("error", err));
296
+ }
297
+ respond(headers, options) {
298
+ this._res.respond(headers, options);
299
+ }
300
+ write(chunk, encoding, callback) {
301
+ return this._res.write(chunk, encoding, callback);
302
+ }
303
+ end(chunk, encoding, callback) {
304
+ this._res.end(chunk, encoding, callback);
305
+ return this;
306
+ }
307
+ destroy(error) {
308
+ this._res.destroy(error);
309
+ return this;
310
+ }
311
+ close(code, callback) {
312
+ if (callback) this.once("close", callback);
313
+ this._res.end();
314
+ }
315
+ priority(_options) {}
316
+ setTimeout(msecs, callback) {
317
+ this._res.setTimeout(msecs, callback);
318
+ return this;
319
+ }
320
+ sendTrailers(_headers) {}
321
+ additionalHeaders(_headers) {}
322
+ respondWithFD(_fd, _headers, _options) {
323
+ throw new Error("http2 respondWithFD is not yet implemented in GJS (Phase 2)");
324
+ }
325
+ respondWithFile(_path, _headers, _options) {
326
+ throw new Error("http2 respondWithFile is not yet implemented in GJS (Phase 2)");
327
+ }
328
+ pushStream(_headers, _options, _callback) {
329
+ throw new Error("http2 server push is not yet implemented in GJS (Phase 2)");
330
+ }
331
+ };
332
+ var ServerHttp2Session = class extends EventEmitter {
333
+ type = constants.NGHTTP2_SESSION_SERVER;
334
+ alpnProtocol = "h2";
335
+ encrypted = true;
336
+ _closed = false;
337
+ _destroyed = false;
338
+ _settings;
339
+ constructor() {
340
+ super();
341
+ this._settings = getDefaultSettings();
342
+ }
343
+ get closed() {
344
+ return this._closed;
345
+ }
346
+ get destroyed() {
347
+ return this._destroyed;
348
+ }
349
+ get pendingSettingsAck() {
350
+ return false;
351
+ }
352
+ get localSettings() {
353
+ return { ...this._settings };
354
+ }
355
+ get remoteSettings() {
356
+ return getDefaultSettings();
357
+ }
358
+ get originSet() {
359
+ return [];
360
+ }
361
+ settings(settings, callback) {
362
+ Object.assign(this._settings, settings);
363
+ if (callback) Promise.resolve().then(callback);
364
+ }
365
+ goaway(code, _lastStreamId, _data) {
366
+ this.emit("goaway", code ?? constants.NGHTTP2_NO_ERROR);
367
+ this.destroy();
368
+ }
369
+ ping(_payload, callback) {
370
+ const buf = new Uint8Array(8);
371
+ if (callback) Promise.resolve().then(() => callback(null, 0, buf));
372
+ return true;
373
+ }
374
+ close(callback) {
375
+ if (this._closed) return;
376
+ this._closed = true;
377
+ this.emit("close");
378
+ if (callback) callback();
379
+ }
380
+ destroy(error, code) {
381
+ if (this._destroyed) return;
382
+ this._destroyed = true;
383
+ this._closed = true;
384
+ if (error) this.emit("error", error);
385
+ if (code !== undefined) this.emit("goaway", code);
386
+ this.emit("close");
387
+ }
388
+ altsvc(_alt, _originOrStream) {}
389
+ origin(..._origins) {}
390
+ ref() {}
391
+ unref() {}
392
+ };
393
+ const _activeServers = new Set();
394
+ var Http2Server = class extends EventEmitter {
395
+ listening = false;
396
+ maxHeadersCount = 2e3;
397
+ timeout = 0;
398
+ _soupServer = null;
399
+ _address = null;
400
+ _options;
401
+ get soupServer() {
402
+ return this._soupServer;
403
+ }
404
+ constructor(options, handler) {
405
+ super();
406
+ if (typeof options === "function") {
407
+ handler = options;
408
+ options = {};
409
+ }
410
+ this._options = options ?? {};
411
+ if (handler) this.on("request", handler);
412
+ }
413
+ listen(...args) {
414
+ let port = 0;
415
+ let hostname = "0.0.0.0";
416
+ let callback;
417
+ for (const arg of args) {
418
+ if (typeof arg === "number") port = arg;
419
+ else if (typeof arg === "string") hostname = arg;
420
+ else if (typeof arg === "function") callback = arg;
421
+ }
422
+ if (callback) this.once("listening", callback);
423
+ try {
424
+ this._soupServer = new Soup.Server({});
425
+ this._configureSoupServer(this._soupServer);
426
+ this._soupServer.add_handler(null, (_server, msg, _path) => {
427
+ this._handleRequest(msg);
428
+ });
429
+ this._soupServer.listen_local(port, Soup.ServerListenOptions.IPV4_ONLY);
430
+ ensureMainLoop();
431
+ const listeners = this._soupServer.get_listeners();
432
+ let actualPort = port;
433
+ if (listeners && listeners.length > 0) {
434
+ const addr = listeners[0].get_local_address();
435
+ if (addr && typeof addr.get_port === "function") {
436
+ actualPort = addr.get_port();
437
+ }
438
+ }
439
+ this.listening = true;
440
+ this._address = {
441
+ port: actualPort,
442
+ family: "IPv4",
443
+ address: hostname
444
+ };
445
+ _activeServers.add(this);
446
+ deferEmit(this, "listening");
447
+ } catch (err) {
448
+ const error = err instanceof Error ? err : new Error(String(err));
449
+ if (this.listenerCount("error") === 0) throw error;
450
+ deferEmit(this, "error", error);
451
+ }
452
+ return this;
453
+ }
454
+ _configureSoupServer(_server) {}
455
+ _handleRequest(soupMsg) {
456
+ const req = new Http2ServerRequest();
457
+ const res = new Http2ServerResponse(soupMsg);
458
+ req.method = soupMsg.get_method();
459
+ const uri = soupMsg.get_uri();
460
+ const path = uri.get_path();
461
+ const query = uri.get_query();
462
+ req.url = query ? path + "?" + query : path;
463
+ req.authority = uri.get_host() ?? "";
464
+ req.scheme = uri.get_scheme() ?? "http";
465
+ const httpVersion = soupMsg.get_http_version();
466
+ if (httpVersion === Soup.HTTPVersion.HTTP_2_0) {
467
+ req.httpVersion = "2.0";
468
+ req.httpVersionMajor = 2;
469
+ req.httpVersionMinor = 0;
470
+ } else {
471
+ req.httpVersion = "1.1";
472
+ req.httpVersionMajor = 1;
473
+ req.httpVersionMinor = 1;
474
+ }
475
+ const requestHeaders = soupMsg.get_request_headers();
476
+ requestHeaders.foreach((name, value) => {
477
+ const lower = name.toLowerCase();
478
+ req.rawHeaders.push(name, value);
479
+ if (lower in req.headers) {
480
+ const existing = req.headers[lower];
481
+ if (Array.isArray(existing)) {
482
+ existing.push(value);
483
+ } else {
484
+ req.headers[lower] = [existing, value];
485
+ }
486
+ } else {
487
+ req.headers[lower] = value;
488
+ }
489
+ });
490
+ const remoteHost = soupMsg.get_remote_host() ?? "127.0.0.1";
491
+ const remoteAddr = soupMsg.get_remote_address();
492
+ const remotePort = remoteAddr instanceof Gio.InetSocketAddress ? remoteAddr.get_port() : 0;
493
+ req.socket = {
494
+ remoteAddress: remoteHost,
495
+ remotePort,
496
+ localAddress: this._address?.address ?? "127.0.0.1",
497
+ localPort: this._address?.port ?? 0,
498
+ encrypted: this instanceof Http2SecureServer
499
+ };
500
+ const body = soupMsg.get_request_body();
501
+ if (body?.data && body.data.length > 0) {
502
+ req._pushBody(body.data);
503
+ } else {
504
+ req._pushBody(null);
505
+ }
506
+ const streamHeaders = {
507
+ ":method": req.method,
508
+ ":path": req.url,
509
+ ":authority": req.authority,
510
+ ":scheme": req.scheme,
511
+ ...req.headers
512
+ };
513
+ soupMsg.pause();
514
+ res.on("finish", () => soupMsg.unpause());
515
+ const session = new ServerHttp2Session();
516
+ const stream = new ServerHttp2Stream(res, session);
517
+ req._setStream(stream);
518
+ res._setStream(stream);
519
+ this.emit("stream", stream, streamHeaders);
520
+ this.emit("request", req, res);
521
+ }
522
+ address() {
523
+ return this._address;
524
+ }
525
+ close(callback) {
526
+ if (callback) this.once("close", callback);
527
+ if (this._soupServer) {
528
+ this._soupServer.disconnect();
529
+ this._soupServer = null;
530
+ }
531
+ this.listening = false;
532
+ _activeServers.delete(this);
533
+ deferEmit(this, "close");
534
+ return this;
535
+ }
536
+ setTimeout(msecs, callback) {
537
+ this.timeout = msecs;
538
+ if (callback) this.on("timeout", callback);
539
+ return this;
540
+ }
541
+ };
542
+ var Http2SecureServer = class extends Http2Server {
543
+ _tlsCert = null;
544
+ constructor(options, handler) {
545
+ super(options, handler);
546
+ if (options.cert && options.key) {
547
+ const certPem = _toPemString(options.cert);
548
+ const keyPem = _toPemString(options.key);
549
+ this._tlsCert = _createTlsCertificate(certPem, keyPem);
550
+ } else if (options.pfx) {}
551
+ }
552
+ _configureSoupServer(server) {
553
+ if (this._tlsCert) {
554
+ server.set_tls_certificate(this._tlsCert);
555
+ }
556
+ }
557
+ setSecureContext(options) {
558
+ if (options.cert && options.key) {
559
+ const certPem = _toPemString(options.cert);
560
+ const keyPem = _toPemString(options.key);
561
+ this._tlsCert = _createTlsCertificate(certPem, keyPem);
562
+ if (this._soupServer && this._tlsCert) {
563
+ this._soupServer.set_tls_certificate(this._tlsCert);
564
+ }
565
+ }
566
+ }
567
+ };
582
568
  function _toPemString(value) {
583
- if (Array.isArray(value)) {
584
- return value.map(_toPemString).join("\n");
585
- }
586
- return Buffer.isBuffer(value) ? value.toString("utf8") : value;
569
+ if (Array.isArray(value)) {
570
+ return value.map(_toPemString).join("\n");
571
+ }
572
+ return Buffer.isBuffer(value) ? value.toString("utf8") : value;
587
573
  }
588
574
  function _createTlsCertificate(certPem, keyPem) {
589
- const combined = certPem.trimEnd() + "\n" + keyPem.trimEnd() + "\n";
590
- try {
591
- return Gio.TlsCertificate.new_from_pem(combined, -1);
592
- } catch (err) {
593
- const tmpDir = GLib.get_tmp_dir();
594
- const certPath = GLib.build_filenamev([tmpDir, "gjsify-http2-cert.pem"]);
595
- const keyPath = GLib.build_filenamev([tmpDir, "gjsify-http2-key.pem"]);
596
- try {
597
- GLib.file_set_contents(certPath, certPem);
598
- GLib.file_set_contents(keyPath, keyPem);
599
- const tlsCert = Gio.TlsCertificate.new_from_files(certPath, keyPath);
600
- return tlsCert;
601
- } finally {
602
- try {
603
- Gio.File.new_for_path(certPath).delete(null);
604
- } catch {
605
- }
606
- try {
607
- Gio.File.new_for_path(keyPath).delete(null);
608
- } catch {
609
- }
610
- }
611
- }
575
+ const combined = certPem.trimEnd() + "\n" + keyPem.trimEnd() + "\n";
576
+ try {
577
+ return Gio.TlsCertificate.new_from_pem(combined, -1);
578
+ } catch (err) {
579
+ const tmpDir = GLib.get_tmp_dir();
580
+ const certPath = GLib.build_filenamev([tmpDir, "gjsify-http2-cert.pem"]);
581
+ const keyPath = GLib.build_filenamev([tmpDir, "gjsify-http2-key.pem"]);
582
+ try {
583
+ GLib.file_set_contents(certPath, certPem);
584
+ GLib.file_set_contents(keyPath, keyPem);
585
+ const tlsCert = Gio.TlsCertificate.new_from_files(certPath, keyPath);
586
+ return tlsCert;
587
+ } finally {
588
+ try {
589
+ Gio.File.new_for_path(certPath).delete(null);
590
+ } catch {}
591
+ try {
592
+ Gio.File.new_for_path(keyPath).delete(null);
593
+ } catch {}
594
+ }
595
+ }
612
596
  }
613
- export {
614
- Http2SecureServer,
615
- Http2Server,
616
- Http2ServerRequest,
617
- Http2ServerResponse,
618
- ServerHttp2Session,
619
- ServerHttp2Stream
620
- };
597
+
598
+ //#endregion
599
+ export { Http2SecureServer, Http2Server, Http2ServerRequest, Http2ServerResponse, ServerHttp2Session, ServerHttp2Stream };