@gjsify/http2 0.1.15 → 0.3.0
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/client-session.js +280 -0
- package/lib/esm/index.js +62 -543
- package/lib/esm/protocol.js +317 -0
- package/lib/esm/server.js +620 -0
- package/lib/types/client-session.d.ts +100 -0
- package/lib/types/http2.gjs.spec.d.ts +2 -0
- package/lib/types/index.d.ts +14 -366
- package/lib/types/protocol.d.ts +265 -0
- package/lib/types/server.d.ts +186 -0
- package/package.json +11 -6
- package/src/client-session.ts +352 -0
- package/src/http2.gjs.spec.ts +303 -0
- package/src/index.ts +85 -592
- package/src/protocol.ts +347 -0
- package/src/server.ts +754 -0
- package/src/test.mts +2 -1
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -0,0 +1,620 @@
|
|
|
1
|
+
import Soup from "@girs/soup-3.0";
|
|
2
|
+
import Gio from "@girs/gio-2.0";
|
|
3
|
+
import GLib from "@girs/glib-2.0";
|
|
4
|
+
import { EventEmitter } from "node:events";
|
|
5
|
+
import { Readable, Writable } from "node:stream";
|
|
6
|
+
import { Buffer } from "node:buffer";
|
|
7
|
+
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
|
+
}
|
|
582
|
+
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;
|
|
587
|
+
}
|
|
588
|
+
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
|
+
}
|
|
612
|
+
}
|
|
613
|
+
export {
|
|
614
|
+
Http2SecureServer,
|
|
615
|
+
Http2Server,
|
|
616
|
+
Http2ServerRequest,
|
|
617
|
+
Http2ServerResponse,
|
|
618
|
+
ServerHttp2Session,
|
|
619
|
+
ServerHttp2Stream
|
|
620
|
+
};
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import Soup from '@girs/soup-3.0';
|
|
2
|
+
import { EventEmitter } from 'node:events';
|
|
3
|
+
import { Duplex } from 'node:stream';
|
|
4
|
+
import { Buffer } from 'node:buffer';
|
|
5
|
+
import { type Http2Settings } from './protocol.js';
|
|
6
|
+
export type { Http2Settings };
|
|
7
|
+
export interface ClientSessionOptions {
|
|
8
|
+
maxDeflateDynamicTableSize?: number;
|
|
9
|
+
maxSessionMemory?: number;
|
|
10
|
+
maxHeaderListPairs?: number;
|
|
11
|
+
maxOutstandingPings?: number;
|
|
12
|
+
maxReservedRemoteStreams?: number;
|
|
13
|
+
maxSendHeaderBlockLength?: number;
|
|
14
|
+
paddingStrategy?: number;
|
|
15
|
+
peerMaxHeaderListSize?: number;
|
|
16
|
+
protocol?: string;
|
|
17
|
+
settings?: Http2Settings;
|
|
18
|
+
rejectUnauthorized?: boolean;
|
|
19
|
+
ca?: string | Buffer | Array<string | Buffer>;
|
|
20
|
+
cert?: string | Buffer | Array<string | Buffer>;
|
|
21
|
+
key?: string | Buffer | Array<string | Buffer>;
|
|
22
|
+
ALPNProtocols?: string[];
|
|
23
|
+
}
|
|
24
|
+
export interface ClientStreamOptions {
|
|
25
|
+
endStream?: boolean;
|
|
26
|
+
exclusive?: boolean;
|
|
27
|
+
parent?: number;
|
|
28
|
+
weight?: number;
|
|
29
|
+
waitForTrailers?: boolean;
|
|
30
|
+
signal?: AbortSignal;
|
|
31
|
+
}
|
|
32
|
+
export declare class Http2Session extends EventEmitter {
|
|
33
|
+
readonly type: number;
|
|
34
|
+
readonly alpnProtocol: string | undefined;
|
|
35
|
+
readonly encrypted: boolean;
|
|
36
|
+
private _closed;
|
|
37
|
+
private _destroyed;
|
|
38
|
+
private _settings;
|
|
39
|
+
constructor();
|
|
40
|
+
get closed(): boolean;
|
|
41
|
+
get destroyed(): boolean;
|
|
42
|
+
get connecting(): boolean;
|
|
43
|
+
get pendingSettingsAck(): boolean;
|
|
44
|
+
get localSettings(): Http2Settings;
|
|
45
|
+
get remoteSettings(): Http2Settings;
|
|
46
|
+
get originSet(): Set<string>;
|
|
47
|
+
settings(settings: Http2Settings, callback?: () => void): void;
|
|
48
|
+
goaway(code?: number, _lastStreamId?: number, _data?: Uint8Array): void;
|
|
49
|
+
ping(payload?: Uint8Array, callback?: (err: Error | null, duration: number, payload: Uint8Array) => void): boolean;
|
|
50
|
+
close(callback?: () => void): void;
|
|
51
|
+
destroy(error?: Error, code?: number): void;
|
|
52
|
+
ref(): void;
|
|
53
|
+
unref(): void;
|
|
54
|
+
}
|
|
55
|
+
export declare class ClientHttp2Stream extends Duplex {
|
|
56
|
+
readonly id = 1;
|
|
57
|
+
readonly pending = false;
|
|
58
|
+
readonly aborted = false;
|
|
59
|
+
readonly bufferSize = 0;
|
|
60
|
+
readonly endAfterHeaders = false;
|
|
61
|
+
private _session;
|
|
62
|
+
private _requestHeaders;
|
|
63
|
+
private _requestChunks;
|
|
64
|
+
private _cancellable;
|
|
65
|
+
private _state;
|
|
66
|
+
private _responseHeaders;
|
|
67
|
+
get state(): number;
|
|
68
|
+
get rstCode(): number;
|
|
69
|
+
get session(): ClientHttp2Session;
|
|
70
|
+
get sentHeaders(): Record<string, string | string[]>;
|
|
71
|
+
constructor(session: ClientHttp2Session, requestHeaders: Record<string, string | string[]>);
|
|
72
|
+
_read(_size: number): void;
|
|
73
|
+
_write(chunk: any, encoding: string, callback: (error?: Error | null) => void): void;
|
|
74
|
+
_final(callback: (error?: Error | null) => void): void;
|
|
75
|
+
private _sendRequest;
|
|
76
|
+
close(code?: number, callback?: () => void): void;
|
|
77
|
+
priority(_options: {
|
|
78
|
+
exclusive?: boolean;
|
|
79
|
+
parent?: number;
|
|
80
|
+
weight?: number;
|
|
81
|
+
silent?: boolean;
|
|
82
|
+
}): void;
|
|
83
|
+
sendTrailers(_headers: Record<string, string | string[]>): void;
|
|
84
|
+
setTimeout(_msecs: number, _callback?: () => void): this;
|
|
85
|
+
}
|
|
86
|
+
export declare class ClientHttp2Session extends Http2Session {
|
|
87
|
+
readonly type: 1;
|
|
88
|
+
readonly encrypted: boolean;
|
|
89
|
+
private _authority;
|
|
90
|
+
private _soupSession;
|
|
91
|
+
private _streams;
|
|
92
|
+
constructor(authority: string, options?: ClientSessionOptions);
|
|
93
|
+
/** @internal Used by ClientHttp2Stream to get the Soup.Session */
|
|
94
|
+
_getSoupSession(): Soup.Session;
|
|
95
|
+
/** @internal Used by ClientHttp2Stream to build the request URL */
|
|
96
|
+
_getAuthority(): string;
|
|
97
|
+
request(headers: Record<string, string | string[]>, options?: ClientStreamOptions): ClientHttp2Stream;
|
|
98
|
+
close(callback?: () => void): void;
|
|
99
|
+
destroy(error?: Error, code?: number): void;
|
|
100
|
+
}
|