@gjsify/http2 0.3.21 → 0.4.3

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/src/server.ts DELETED
@@ -1,754 +0,0 @@
1
- // Reference: Node.js lib/internal/http2/compat.js, lib/_http_server.js
2
- // Reimplemented for GJS using Soup.Server (HTTP/2 transparently via ALPN when TLS is used)
3
- //
4
- // Phase 1 limitations:
5
- // - createServer() serves HTTP/1.1 only (Soup does not support h2c/cleartext HTTP/2)
6
- // - createSecureServer() negotiates h2 via ALPN automatically when TLS cert is set
7
- // - pushStream(), respondWithFD(), respondWithFile() are stubs (Phase 2)
8
- // - stream IDs are always 1 (Soup internal)
9
-
10
- import Soup from '@girs/soup-3.0';
11
- import Gio from '@girs/gio-2.0';
12
- import GLib from '@girs/glib-2.0';
13
- import { EventEmitter } from 'node:events';
14
- import { Readable, Writable } from 'node:stream';
15
- import { Buffer } from 'node:buffer';
16
- import { deferEmit, ensureMainLoop } from '@gjsify/utils';
17
- import { constants, getDefaultSettings, type Http2Settings } from './protocol.js';
18
-
19
- export type { Http2Settings };
20
-
21
- // ─── Http2ServerRequest ───────────────────────────────────────────────────────
22
-
23
- export class Http2ServerRequest extends Readable {
24
- method = 'GET';
25
- url = '/';
26
- headers: Record<string, string | string[]> = {};
27
- rawHeaders: string[] = [];
28
- authority = '';
29
- scheme = 'https';
30
- httpVersion = '2.0';
31
- httpVersionMajor = 2;
32
- httpVersionMinor = 0;
33
- complete = false;
34
- socket: any = null;
35
- trailers: Record<string, string> = {};
36
- rawTrailers: string[] = [];
37
-
38
- private _stream: ServerHttp2Stream | null = null;
39
- private _timeoutTimer: ReturnType<typeof setTimeout> | null = null;
40
-
41
- get stream(): ServerHttp2Stream | null { return this._stream; }
42
-
43
- // Called by Http2Server after stream is created
44
- _setStream(stream: ServerHttp2Stream): void {
45
- this._stream = stream;
46
- }
47
-
48
- constructor() {
49
- super();
50
- }
51
-
52
- _read(_size: number): void {}
53
-
54
- // 'close' means connection lost, not body-stream end
55
- protected _autoClose(): void {}
56
-
57
- _pushBody(body: Uint8Array | null): void {
58
- if (body && body.length > 0) {
59
- this.push(Buffer.from(body));
60
- }
61
- this.push(null);
62
- this.complete = true;
63
- if (this._timeoutTimer) {
64
- clearTimeout(this._timeoutTimer);
65
- this._timeoutTimer = null;
66
- }
67
- }
68
-
69
- setTimeout(msecs: number, callback?: () => void): this {
70
- if (this._timeoutTimer) {
71
- clearTimeout(this._timeoutTimer);
72
- this._timeoutTimer = null;
73
- }
74
- if (callback) this.once('timeout', callback);
75
- if (msecs > 0) {
76
- this._timeoutTimer = setTimeout(() => {
77
- this._timeoutTimer = null;
78
- this.emit('timeout');
79
- }, msecs);
80
- }
81
- return this;
82
- }
83
-
84
- destroy(error?: Error): this {
85
- if (this._timeoutTimer) {
86
- clearTimeout(this._timeoutTimer);
87
- this._timeoutTimer = null;
88
- }
89
- return super.destroy(error) as this;
90
- }
91
- }
92
-
93
- // ─── Http2ServerResponse ──────────────────────────────────────────────────────
94
-
95
- export class Http2ServerResponse extends Writable {
96
- statusCode = 200;
97
- statusMessage = '';
98
- headersSent = false;
99
- finished = false;
100
- sendDate = true;
101
-
102
- private _soupMsg: Soup.ServerMessage;
103
- private _headers: Map<string, string | string[]> = new Map();
104
- private _streaming = false;
105
- private _timeoutTimer: ReturnType<typeof setTimeout> | null = null;
106
- private _stream: ServerHttp2Stream | null = null;
107
-
108
- get stream(): ServerHttp2Stream | null { return this._stream; }
109
- get socket(): null { return null; }
110
-
111
- // Called by Http2Server after stream is created
112
- _setStream(stream: ServerHttp2Stream): void {
113
- this._stream = stream;
114
- }
115
-
116
- constructor(soupMsg: Soup.ServerMessage) {
117
- super();
118
- this._soupMsg = soupMsg;
119
- }
120
-
121
- setHeader(name: string, value: string | number | string[]): this {
122
- this._headers.set(name.toLowerCase(), typeof value === 'number' ? String(value) : value);
123
- return this;
124
- }
125
-
126
- getHeader(name: string): string | string[] | undefined {
127
- return this._headers.get(name.toLowerCase());
128
- }
129
-
130
- removeHeader(name: string): void {
131
- this._headers.delete(name.toLowerCase());
132
- }
133
-
134
- hasHeader(name: string): boolean {
135
- return this._headers.has(name.toLowerCase());
136
- }
137
-
138
- getHeaderNames(): string[] {
139
- return Array.from(this._headers.keys());
140
- }
141
-
142
- getHeaders(): Record<string, string | string[]> {
143
- const result: Record<string, string | string[]> = {};
144
- for (const [key, value] of this._headers) {
145
- result[key] = value;
146
- }
147
- return result;
148
- }
149
-
150
- appendHeader(name: string, value: string | string[]): this {
151
- const lower = name.toLowerCase();
152
- const existing = this._headers.get(lower);
153
- if (existing === undefined) {
154
- this._headers.set(lower, value);
155
- } else if (Array.isArray(existing)) {
156
- Array.isArray(value) ? existing.push(...value) : existing.push(value);
157
- } else {
158
- this._headers.set(lower, Array.isArray(value) ? [existing as string, ...value] : [existing as string, value]);
159
- }
160
- return this;
161
- }
162
-
163
- flushHeaders(): void {
164
- if (!this.headersSent) this.headersSent = true;
165
- }
166
-
167
- writeHead(statusCode: number, statusMessage?: string | Record<string, string | string[]>, headers?: Record<string, string | string[]>): this {
168
- this.statusCode = statusCode;
169
- if (typeof statusMessage === 'object') {
170
- headers = statusMessage;
171
- statusMessage = undefined;
172
- }
173
- if (typeof statusMessage === 'string') this.statusMessage = statusMessage;
174
- if (headers) {
175
- for (const [key, value] of Object.entries(headers)) {
176
- this.setHeader(key, value);
177
- }
178
- }
179
- return this;
180
- }
181
-
182
- // http2 session-API alias — extracts :status from headers map
183
- respond(headers: Record<string, string | string[] | number>, options?: { endStream?: boolean }): void {
184
- const status = Number(headers[':status'] ?? 200);
185
- const rest: Record<string, string | string[]> = {};
186
- for (const [k, v] of Object.entries(headers)) {
187
- if (k === ':status') continue;
188
- rest[k] = typeof v === 'number' ? String(v) : v;
189
- }
190
- this.writeHead(status, rest);
191
- if (options?.endStream) this.end();
192
- }
193
-
194
- writeContinue(callback?: () => void): void {
195
- if (callback) Promise.resolve().then(callback);
196
- }
197
-
198
- writeEarlyHints(_hints: Record<string, string | string[]>, callback?: () => void): void {
199
- if (callback) Promise.resolve().then(callback);
200
- }
201
-
202
- addTrailers(_headers: Record<string, string>): void {}
203
-
204
- setTimeout(msecs: number, callback?: () => void): this {
205
- if (this._timeoutTimer) {
206
- clearTimeout(this._timeoutTimer);
207
- this._timeoutTimer = null;
208
- }
209
- if (callback) this.once('timeout', callback);
210
- if (msecs > 0) {
211
- this._timeoutTimer = setTimeout(() => {
212
- this._timeoutTimer = null;
213
- this.emit('timeout');
214
- }, msecs);
215
- }
216
- return this;
217
- }
218
-
219
- private _startStreaming(): void {
220
- if (this._streaming) return;
221
- this._streaming = true;
222
- this.headersSent = true;
223
-
224
- if (this._timeoutTimer) {
225
- clearTimeout(this._timeoutTimer);
226
- this._timeoutTimer = null;
227
- }
228
-
229
- this._soupMsg.set_status(this.statusCode, this.statusMessage || null);
230
- const responseHeaders = this._soupMsg.get_response_headers();
231
-
232
- if (this._headers.has('content-length')) {
233
- responseHeaders.set_encoding(Soup.Encoding.CONTENT_LENGTH);
234
- } else {
235
- responseHeaders.set_encoding(Soup.Encoding.CHUNKED);
236
- }
237
-
238
- for (const [key, value] of this._headers) {
239
- if (Array.isArray(value)) {
240
- for (const v of value) responseHeaders.append(key, v);
241
- } else {
242
- responseHeaders.replace(key, value as string);
243
- }
244
- }
245
- }
246
-
247
- _write(chunk: any, encoding: string, callback: (error?: Error | null) => void): void {
248
- const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk, encoding as BufferEncoding);
249
- this._startStreaming();
250
- const responseBody = this._soupMsg.get_response_body();
251
- responseBody.append(new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength));
252
- this._soupMsg.unpause();
253
- callback();
254
- }
255
-
256
- _final(callback: (error?: Error | null) => void): void {
257
- if (this._streaming) {
258
- const responseBody = this._soupMsg.get_response_body();
259
- responseBody.complete();
260
- this._soupMsg.unpause();
261
- } else {
262
- this._sendBatchResponse();
263
- }
264
- this.finished = true;
265
- callback();
266
- }
267
-
268
- private _sendBatchResponse(): void {
269
- if (this.headersSent) return;
270
- this.headersSent = true;
271
-
272
- if (this._timeoutTimer) {
273
- clearTimeout(this._timeoutTimer);
274
- this._timeoutTimer = null;
275
- }
276
-
277
- this._soupMsg.set_status(this.statusCode, this.statusMessage || null);
278
- const responseHeaders = this._soupMsg.get_response_headers();
279
-
280
- for (const [key, value] of this._headers) {
281
- if (Array.isArray(value)) {
282
- for (const v of value) responseHeaders.append(key, v);
283
- } else {
284
- responseHeaders.replace(key, value as string);
285
- }
286
- }
287
-
288
- const contentType = (this._headers.get('content-type') as string) || 'text/plain';
289
- this._soupMsg.set_response(contentType, Soup.MemoryUse.COPY, new Uint8Array(0));
290
- }
291
-
292
- end(chunk?: unknown, encoding?: BufferEncoding | (() => void), callback?: () => void): this {
293
- if (typeof chunk === 'function') {
294
- callback = chunk as () => void;
295
- chunk = undefined;
296
- } else if (typeof encoding === 'function') {
297
- callback = encoding;
298
- encoding = undefined;
299
- }
300
- if (chunk != null) {
301
- this.write(chunk as string | Buffer, encoding as BufferEncoding);
302
- }
303
- super.end(callback);
304
- return this;
305
- }
306
-
307
- // respondWithFD and respondWithFile stubs (Phase 2)
308
- respondWithFD(_fd: any, _headers?: any, _options?: any): void {
309
- throw new Error('http2 respondWithFD is not yet implemented in GJS (Phase 2)');
310
- }
311
-
312
- respondWithFile(_path: string, _headers?: any, _options?: any): void {
313
- throw new Error('http2 respondWithFile is not yet implemented in GJS (Phase 2)');
314
- }
315
-
316
- pushStream(
317
- _headers: Record<string, string | string[]>,
318
- _options: any,
319
- _callback: (err: Error | null, pushStream: any, headers: Record<string, string | string[]>) => void,
320
- ): void {
321
- throw new Error('http2 server push is not yet implemented in GJS (Phase 2)');
322
- }
323
-
324
- createPushResponse(
325
- _headers: Record<string, string | string[]>,
326
- _callback: (err: Error | null, res: Http2ServerResponse) => void,
327
- ): void {
328
- throw new Error('http2 server push is not yet implemented in GJS (Phase 2)');
329
- }
330
- }
331
-
332
- // ─── ServerHttp2Stream ────────────────────────────────────────────────────────
333
- // Facade over Http2ServerResponse exposing the session/stream API.
334
- // Delegates all writes to the underlying response object.
335
-
336
- export class ServerHttp2Stream extends EventEmitter {
337
- readonly id = 1;
338
- readonly pushAllowed = false;
339
- readonly sentHeaders: Record<string, string | string[]> = {};
340
-
341
- private _res: Http2ServerResponse;
342
- private _session: ServerHttp2Session | null;
343
-
344
- get session(): ServerHttp2Session | null { return this._session; }
345
- get headersSent(): boolean { return this._res.headersSent; }
346
- get closed(): boolean { return this._res.writableEnded; }
347
- get destroyed(): boolean { return this._res.destroyed; }
348
- get pending(): boolean { return false; }
349
- get state(): number {
350
- return this.closed ? constants.NGHTTP2_STREAM_STATE_CLOSED : constants.NGHTTP2_STREAM_STATE_OPEN;
351
- }
352
-
353
- constructor(res: Http2ServerResponse, session: ServerHttp2Session | null = null) {
354
- super();
355
- this._res = res;
356
- this._session = session;
357
-
358
- res.on('finish', () => this.emit('close'));
359
- res.on('error', (err: Error) => this.emit('error', err));
360
- }
361
-
362
- // Session API: send response headers
363
- respond(headers: Record<string, string | string[] | number>, options?: { endStream?: boolean }): void {
364
- this._res.respond(headers, options);
365
- }
366
-
367
- // Writable-like interface delegating to response
368
- write(chunk: any, encoding?: BufferEncoding | (() => void), callback?: () => void): boolean {
369
- return this._res.write(chunk as any, encoding as any, callback as any);
370
- }
371
-
372
- end(chunk?: any, encoding?: BufferEncoding | (() => void), callback?: () => void): this {
373
- this._res.end(chunk as any, encoding as any, callback as any);
374
- return this;
375
- }
376
-
377
- destroy(error?: Error): this {
378
- this._res.destroy(error);
379
- return this;
380
- }
381
-
382
- close(code?: number, callback?: () => void): void {
383
- if (callback) this.once('close', callback);
384
- this._res.end();
385
- }
386
-
387
- priority(_options: { exclusive?: boolean; parent?: number; weight?: number; silent?: boolean }): void {}
388
-
389
- setTimeout(msecs: number, callback?: () => void): this {
390
- this._res.setTimeout(msecs, callback);
391
- return this;
392
- }
393
-
394
- sendTrailers(_headers: Record<string, string | string[]>): void {}
395
- additionalHeaders(_headers: Record<string, string | string[]>): void {}
396
-
397
- respondWithFD(_fd: any, _headers?: any, _options?: any): void {
398
- throw new Error('http2 respondWithFD is not yet implemented in GJS (Phase 2)');
399
- }
400
-
401
- respondWithFile(_path: string, _headers?: any, _options?: any): void {
402
- throw new Error('http2 respondWithFile is not yet implemented in GJS (Phase 2)');
403
- }
404
-
405
- pushStream(
406
- _headers: Record<string, string | string[]>,
407
- _options: any,
408
- _callback: (err: Error | null, pushStream: ServerHttp2Stream, headers: Record<string, string | string[]>) => void,
409
- ): void {
410
- throw new Error('http2 server push is not yet implemented in GJS (Phase 2)');
411
- }
412
- }
413
-
414
- // ─── ServerHttp2Session ───────────────────────────────────────────────────────
415
-
416
- export class ServerHttp2Session extends EventEmitter {
417
- readonly type = constants.NGHTTP2_SESSION_SERVER;
418
- readonly alpnProtocol: string | undefined = 'h2';
419
- readonly encrypted: boolean = true;
420
-
421
- private _closed = false;
422
- private _destroyed = false;
423
- private _settings: Http2Settings;
424
-
425
- constructor() {
426
- super();
427
- this._settings = getDefaultSettings();
428
- }
429
-
430
- get closed(): boolean { return this._closed; }
431
- get destroyed(): boolean { return this._destroyed; }
432
- get pendingSettingsAck(): boolean { return false; }
433
- get localSettings(): Http2Settings { return { ...this._settings }; }
434
- get remoteSettings(): Http2Settings { return getDefaultSettings(); }
435
- get originSet(): string[] { return []; }
436
-
437
- settings(settings: Http2Settings, callback?: () => void): void {
438
- Object.assign(this._settings, settings);
439
- if (callback) Promise.resolve().then(callback);
440
- }
441
-
442
- goaway(code?: number, _lastStreamId?: number, _data?: Uint8Array): void {
443
- this.emit('goaway', code ?? constants.NGHTTP2_NO_ERROR);
444
- this.destroy();
445
- }
446
-
447
- ping(_payload?: Uint8Array, callback?: (err: Error | null, duration: number, payload: Uint8Array) => void): boolean {
448
- const buf = new Uint8Array(8);
449
- if (callback) Promise.resolve().then(() => callback(null, 0, buf));
450
- return true;
451
- }
452
-
453
- close(callback?: () => void): void {
454
- if (this._closed) return;
455
- this._closed = true;
456
- this.emit('close');
457
- if (callback) callback();
458
- }
459
-
460
- destroy(error?: Error, code?: number): void {
461
- if (this._destroyed) return;
462
- this._destroyed = true;
463
- this._closed = true;
464
- if (error) this.emit('error', error);
465
- if (code !== undefined) this.emit('goaway', code);
466
- this.emit('close');
467
- }
468
-
469
- altsvc(_alt: string, _originOrStream: string | number): void {}
470
- origin(..._origins: string[]): void {}
471
- ref(): void {}
472
- unref(): void {}
473
- }
474
-
475
- // ─── Http2Server ──────────────────────────────────────────────────────────────
476
-
477
- // GC guard — prevents server from being collected while listening
478
- const _activeServers = new Set<Http2Server>();
479
-
480
- export interface ServerOptions {
481
- allowHTTP1?: boolean;
482
- maxDeflateDynamicTableSize?: number;
483
- maxSessionMemory?: number;
484
- maxHeaderListPairs?: number;
485
- maxOutstandingPings?: number;
486
- maxSendHeaderBlockLength?: number;
487
- paddingStrategy?: number;
488
- peerMaxHeaderListSize?: number;
489
- selectPadding?: (frameLen: number, maxFrameLen: number) => number;
490
- settings?: Http2Settings;
491
- Http1IncomingMessage?: any;
492
- Http1ServerResponse?: any;
493
- unknownProtocolTimeout?: number;
494
- }
495
-
496
- export class Http2Server extends EventEmitter {
497
- listening = false;
498
- maxHeadersCount = 2000;
499
- timeout = 0;
500
-
501
- protected _soupServer: Soup.Server | null = null;
502
- protected _address: { port: number; family: string; address: string } | null = null;
503
- private _options: ServerOptions;
504
-
505
- get soupServer(): Soup.Server | null { return this._soupServer; }
506
-
507
- constructor(options?: ServerOptions | ((req: Http2ServerRequest, res: Http2ServerResponse) => void), handler?: (req: Http2ServerRequest, res: Http2ServerResponse) => void) {
508
- super();
509
- if (typeof options === 'function') {
510
- handler = options;
511
- options = {};
512
- }
513
- this._options = options ?? {};
514
- if (handler) this.on('request', handler);
515
- }
516
-
517
- listen(port?: number, hostname?: string, backlog?: number, callback?: () => void): this;
518
- listen(port?: number, hostname?: string, callback?: () => void): this;
519
- listen(port?: number, callback?: () => void): this;
520
- listen(...args: unknown[]): this {
521
- let port = 0;
522
- let hostname = '0.0.0.0';
523
- let callback: (() => void) | undefined;
524
-
525
- for (const arg of args) {
526
- if (typeof arg === 'number') port = arg;
527
- else if (typeof arg === 'string') hostname = arg;
528
- else if (typeof arg === 'function') callback = arg as () => void;
529
- }
530
-
531
- if (callback) this.once('listening', callback);
532
-
533
- try {
534
- this._soupServer = new Soup.Server({});
535
- this._configureSoupServer(this._soupServer);
536
-
537
- this._soupServer.add_handler(null, (_server: Soup.Server, msg: Soup.ServerMessage, _path: string) => {
538
- this._handleRequest(msg);
539
- });
540
-
541
- this._soupServer.listen_local(port, Soup.ServerListenOptions.IPV4_ONLY);
542
- ensureMainLoop();
543
-
544
- const listeners = this._soupServer.get_listeners();
545
- let actualPort = port;
546
- if (listeners && listeners.length > 0) {
547
- const addr = listeners[0].get_local_address() as Gio.InetSocketAddress;
548
- if (addr && typeof addr.get_port === 'function') {
549
- actualPort = addr.get_port();
550
- }
551
- }
552
-
553
- this.listening = true;
554
- this._address = { port: actualPort, family: 'IPv4', address: hostname };
555
- _activeServers.add(this);
556
-
557
- deferEmit(this, 'listening');
558
- } catch (err: unknown) {
559
- const error = err instanceof Error ? err : new Error(String(err));
560
- if (this.listenerCount('error') === 0) throw error;
561
- deferEmit(this, 'error', error);
562
- }
563
-
564
- return this;
565
- }
566
-
567
- // Override in Http2SecureServer to set TLS certificate before listen
568
- protected _configureSoupServer(_server: Soup.Server): void {}
569
-
570
- private _handleRequest(soupMsg: Soup.ServerMessage): void {
571
- const req = new Http2ServerRequest();
572
- const res = new Http2ServerResponse(soupMsg);
573
-
574
- // Populate request metadata
575
- req.method = soupMsg.get_method();
576
- const uri = soupMsg.get_uri();
577
- const path = uri.get_path();
578
- const query = uri.get_query();
579
- req.url = query ? path + '?' + query : path;
580
- req.authority = uri.get_host() ?? '';
581
- req.scheme = uri.get_scheme() ?? 'http';
582
-
583
- // Detect HTTP version from Soup
584
- const httpVersion = soupMsg.get_http_version();
585
- if (httpVersion === Soup.HTTPVersion.HTTP_2_0) {
586
- req.httpVersion = '2.0';
587
- req.httpVersionMajor = 2;
588
- req.httpVersionMinor = 0;
589
- } else {
590
- req.httpVersion = '1.1';
591
- req.httpVersionMajor = 1;
592
- req.httpVersionMinor = 1;
593
- }
594
-
595
- // Parse request headers
596
- const requestHeaders = soupMsg.get_request_headers();
597
- requestHeaders.foreach((name: string, value: string) => {
598
- const lower = name.toLowerCase();
599
- req.rawHeaders.push(name, value);
600
- if (lower in req.headers) {
601
- const existing = req.headers[lower];
602
- if (Array.isArray(existing)) {
603
- existing.push(value);
604
- } else {
605
- req.headers[lower] = [existing as string, value];
606
- }
607
- } else {
608
- req.headers[lower] = value;
609
- }
610
- });
611
-
612
- // Remote address info
613
- const remoteHost = soupMsg.get_remote_host() ?? '127.0.0.1';
614
- const remoteAddr = soupMsg.get_remote_address();
615
- const remotePort = (remoteAddr instanceof Gio.InetSocketAddress) ? remoteAddr.get_port() : 0;
616
- req.socket = {
617
- remoteAddress: remoteHost,
618
- remotePort,
619
- localAddress: this._address?.address ?? '127.0.0.1',
620
- localPort: this._address?.port ?? 0,
621
- encrypted: this instanceof Http2SecureServer,
622
- } as any;
623
-
624
- // Push request body into the readable stream
625
- const body = soupMsg.get_request_body();
626
- if (body?.data && body.data.length > 0) {
627
- req._pushBody(body.data);
628
- } else {
629
- req._pushBody(null);
630
- }
631
-
632
- // Build headers record for 'stream' event (http2 session API)
633
- const streamHeaders: Record<string, string | string[]> = {
634
- ':method': req.method,
635
- ':path': req.url,
636
- ':authority': req.authority,
637
- ':scheme': req.scheme,
638
- ...req.headers,
639
- };
640
-
641
- // Pause Soup until response is sent
642
- soupMsg.pause();
643
- res.on('finish', () => soupMsg.unpause());
644
-
645
- // Create stream facade and wire references
646
- const session = new ServerHttp2Session();
647
- const stream = new ServerHttp2Stream(res, session);
648
- req._setStream(stream);
649
- res._setStream(stream);
650
-
651
- // Emit both session API ('stream') and compat API ('request') events
652
- this.emit('stream', stream, streamHeaders);
653
- this.emit('request', req, res);
654
- }
655
-
656
- address(): { port: number; family: string; address: string } | null {
657
- return this._address;
658
- }
659
-
660
- close(callback?: (err?: Error) => void): this {
661
- if (callback) this.once('close', callback);
662
- if (this._soupServer) {
663
- this._soupServer.disconnect();
664
- this._soupServer = null;
665
- }
666
- this.listening = false;
667
- _activeServers.delete(this);
668
- deferEmit(this, 'close');
669
- return this;
670
- }
671
-
672
- setTimeout(msecs: number, callback?: () => void): this {
673
- this.timeout = msecs;
674
- if (callback) this.on('timeout', callback);
675
- return this;
676
- }
677
- }
678
-
679
- // ─── Http2SecureServer ────────────────────────────────────────────────────────
680
-
681
- export interface SecureServerOptions extends ServerOptions {
682
- cert?: string | Buffer | Array<string | Buffer>;
683
- key?: string | Buffer | Array<string | Buffer>;
684
- pfx?: string | Buffer | Array<string | Buffer>;
685
- passphrase?: string;
686
- ca?: string | Buffer | Array<string | Buffer>;
687
- requestCert?: boolean;
688
- rejectUnauthorized?: boolean;
689
- ALPNProtocols?: string[];
690
- }
691
-
692
- export class Http2SecureServer extends Http2Server {
693
- private _tlsCert: Gio.TlsCertificate | null = null;
694
-
695
- constructor(options: SecureServerOptions, handler?: (req: Http2ServerRequest, res: Http2ServerResponse) => void) {
696
- super(options, handler);
697
-
698
- if (options.cert && options.key) {
699
- const certPem = _toPemString(options.cert);
700
- const keyPem = _toPemString(options.key);
701
- this._tlsCert = _createTlsCertificate(certPem, keyPem);
702
- } else if (options.pfx) {
703
- // PKCS#12 not supported yet; TLS still works if a cert was set via setSecureContext
704
- }
705
- }
706
-
707
- protected _configureSoupServer(server: Soup.Server): void {
708
- if (this._tlsCert) {
709
- server.set_tls_certificate(this._tlsCert);
710
- }
711
- }
712
-
713
- setSecureContext(options: SecureServerOptions): void {
714
- if (options.cert && options.key) {
715
- const certPem = _toPemString(options.cert);
716
- const keyPem = _toPemString(options.key);
717
- this._tlsCert = _createTlsCertificate(certPem, keyPem);
718
- if (this._soupServer && this._tlsCert) {
719
- this._soupServer.set_tls_certificate(this._tlsCert);
720
- }
721
- }
722
- }
723
- }
724
-
725
- // ─── Helpers ──────────────────────────────────────────────────────────────────
726
-
727
- function _toPemString(value: string | Buffer | Array<string | Buffer>): string {
728
- if (Array.isArray(value)) {
729
- return value.map(_toPemString).join('\n');
730
- }
731
- return Buffer.isBuffer(value) ? value.toString('utf8') : (value as string);
732
- }
733
-
734
- function _createTlsCertificate(certPem: string, keyPem: string): Gio.TlsCertificate {
735
- // Combine cert + key into a single PEM string — Gio.TlsCertificate.new_from_pem() accepts both
736
- const combined = certPem.trimEnd() + '\n' + keyPem.trimEnd() + '\n';
737
- try {
738
- return Gio.TlsCertificate.new_from_pem(combined, -1);
739
- } catch (err) {
740
- // Fall back: write to temp files
741
- const tmpDir = GLib.get_tmp_dir();
742
- const certPath = GLib.build_filenamev([tmpDir, 'gjsify-http2-cert.pem']);
743
- const keyPath = GLib.build_filenamev([tmpDir, 'gjsify-http2-key.pem']);
744
- try {
745
- GLib.file_set_contents(certPath, certPem);
746
- GLib.file_set_contents(keyPath, keyPem);
747
- const tlsCert = Gio.TlsCertificate.new_from_files(certPath, keyPath);
748
- return tlsCert;
749
- } finally {
750
- try { Gio.File.new_for_path(certPath).delete(null); } catch {}
751
- try { Gio.File.new_for_path(keyPath).delete(null); } catch {}
752
- }
753
- }
754
- }