@gjsify/http2 0.1.15 → 0.2.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,280 @@
|
|
|
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 { Duplex } from "node:stream";
|
|
6
|
+
import { Buffer } from "node:buffer";
|
|
7
|
+
import { readBytesAsync } from "@gjsify/utils";
|
|
8
|
+
import { constants, getDefaultSettings } from "./protocol.js";
|
|
9
|
+
class Http2Session extends EventEmitter {
|
|
10
|
+
type = constants.NGHTTP2_SESSION_CLIENT;
|
|
11
|
+
alpnProtocol = void 0;
|
|
12
|
+
encrypted = false;
|
|
13
|
+
_closed = false;
|
|
14
|
+
_destroyed = false;
|
|
15
|
+
_settings;
|
|
16
|
+
constructor() {
|
|
17
|
+
super();
|
|
18
|
+
this._settings = getDefaultSettings();
|
|
19
|
+
}
|
|
20
|
+
get closed() {
|
|
21
|
+
return this._closed;
|
|
22
|
+
}
|
|
23
|
+
get destroyed() {
|
|
24
|
+
return this._destroyed;
|
|
25
|
+
}
|
|
26
|
+
get connecting() {
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
get pendingSettingsAck() {
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
get localSettings() {
|
|
33
|
+
return { ...this._settings };
|
|
34
|
+
}
|
|
35
|
+
get remoteSettings() {
|
|
36
|
+
return getDefaultSettings();
|
|
37
|
+
}
|
|
38
|
+
get originSet() {
|
|
39
|
+
return /* @__PURE__ */ new Set();
|
|
40
|
+
}
|
|
41
|
+
settings(settings, callback) {
|
|
42
|
+
Object.assign(this._settings, settings);
|
|
43
|
+
if (callback) Promise.resolve().then(callback);
|
|
44
|
+
}
|
|
45
|
+
goaway(code, _lastStreamId, _data) {
|
|
46
|
+
this.emit("goaway", code ?? constants.NGHTTP2_NO_ERROR);
|
|
47
|
+
this.destroy();
|
|
48
|
+
}
|
|
49
|
+
ping(payload, callback) {
|
|
50
|
+
const buf = payload || new Uint8Array(8);
|
|
51
|
+
if (callback) Promise.resolve().then(() => callback(null, 0, buf));
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
54
|
+
close(callback) {
|
|
55
|
+
if (this._closed) return;
|
|
56
|
+
this._closed = true;
|
|
57
|
+
this.emit("close");
|
|
58
|
+
if (callback) callback();
|
|
59
|
+
}
|
|
60
|
+
destroy(error, code) {
|
|
61
|
+
if (this._destroyed) return;
|
|
62
|
+
this._destroyed = true;
|
|
63
|
+
this._closed = true;
|
|
64
|
+
if (error) this.emit("error", error);
|
|
65
|
+
if (code !== void 0) this.emit("goaway", code);
|
|
66
|
+
this.emit("close");
|
|
67
|
+
}
|
|
68
|
+
ref() {
|
|
69
|
+
}
|
|
70
|
+
unref() {
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
class ClientHttp2Stream extends Duplex {
|
|
74
|
+
id = 1;
|
|
75
|
+
pending = false;
|
|
76
|
+
aborted = false;
|
|
77
|
+
bufferSize = 0;
|
|
78
|
+
endAfterHeaders = false;
|
|
79
|
+
_session;
|
|
80
|
+
_requestHeaders;
|
|
81
|
+
_requestChunks = [];
|
|
82
|
+
_cancellable;
|
|
83
|
+
_state = constants.NGHTTP2_STREAM_STATE_OPEN;
|
|
84
|
+
_responseHeaders = {};
|
|
85
|
+
get state() {
|
|
86
|
+
return this._state;
|
|
87
|
+
}
|
|
88
|
+
get rstCode() {
|
|
89
|
+
return constants.NGHTTP2_NO_ERROR;
|
|
90
|
+
}
|
|
91
|
+
get session() {
|
|
92
|
+
return this._session;
|
|
93
|
+
}
|
|
94
|
+
get sentHeaders() {
|
|
95
|
+
return this._requestHeaders;
|
|
96
|
+
}
|
|
97
|
+
constructor(session, requestHeaders) {
|
|
98
|
+
super();
|
|
99
|
+
this._session = session;
|
|
100
|
+
this._requestHeaders = requestHeaders;
|
|
101
|
+
this._cancellable = new Gio.Cancellable();
|
|
102
|
+
}
|
|
103
|
+
_read(_size) {
|
|
104
|
+
}
|
|
105
|
+
_write(chunk, encoding, callback) {
|
|
106
|
+
const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk, encoding);
|
|
107
|
+
this._requestChunks.push(buf);
|
|
108
|
+
callback();
|
|
109
|
+
}
|
|
110
|
+
_final(callback) {
|
|
111
|
+
this._sendRequest().then(() => callback()).catch((err) => callback(err instanceof Error ? err : new Error(String(err))));
|
|
112
|
+
}
|
|
113
|
+
async _sendRequest() {
|
|
114
|
+
const session = this._session._getSoupSession();
|
|
115
|
+
const authority = this._session._getAuthority();
|
|
116
|
+
const method = this._requestHeaders[":method"] || "GET";
|
|
117
|
+
const path = this._requestHeaders[":path"] || "/";
|
|
118
|
+
const scheme = this._requestHeaders[":scheme"] || (authority.startsWith("https") ? "https" : "http");
|
|
119
|
+
const host = this._requestHeaders[":authority"] || authority.replace(/^https?:\/\//, "");
|
|
120
|
+
const url = `${scheme}://${host}${path}`;
|
|
121
|
+
let uri;
|
|
122
|
+
try {
|
|
123
|
+
uri = GLib.Uri.parse(url, GLib.UriFlags.NONE);
|
|
124
|
+
} catch (err) {
|
|
125
|
+
throw new Error(`Invalid HTTP/2 request URL: ${url}`);
|
|
126
|
+
}
|
|
127
|
+
const message = new Soup.Message({ method, uri });
|
|
128
|
+
const reqHeaders = message.get_request_headers();
|
|
129
|
+
for (const [key, value] of Object.entries(this._requestHeaders)) {
|
|
130
|
+
if (key.startsWith(":")) continue;
|
|
131
|
+
if (Array.isArray(value)) {
|
|
132
|
+
for (const v of value) reqHeaders.append(key, v);
|
|
133
|
+
} else {
|
|
134
|
+
reqHeaders.replace(key, value);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
const body = Buffer.concat(this._requestChunks);
|
|
138
|
+
if (body.length > 0) {
|
|
139
|
+
const contentType = this._requestHeaders["content-type"] || "application/octet-stream";
|
|
140
|
+
message.set_request_body_from_bytes(contentType, new GLib.Bytes(body));
|
|
141
|
+
}
|
|
142
|
+
try {
|
|
143
|
+
const inputStream = await new Promise((resolve, reject) => {
|
|
144
|
+
session.send_async(message, GLib.PRIORITY_DEFAULT, this._cancellable, (_self, asyncRes) => {
|
|
145
|
+
try {
|
|
146
|
+
resolve(session.send_finish(asyncRes));
|
|
147
|
+
} catch (error) {
|
|
148
|
+
reject(error);
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
const statusCode = message.status_code;
|
|
153
|
+
const responseHeaders = message.get_response_headers();
|
|
154
|
+
const headersObj = { ":status": String(statusCode) };
|
|
155
|
+
responseHeaders.foreach((name, value) => {
|
|
156
|
+
const lower = name.toLowerCase();
|
|
157
|
+
if (lower in headersObj) {
|
|
158
|
+
const existing = headersObj[lower];
|
|
159
|
+
if (Array.isArray(existing)) {
|
|
160
|
+
existing.push(value);
|
|
161
|
+
} else {
|
|
162
|
+
headersObj[lower] = [existing, value];
|
|
163
|
+
}
|
|
164
|
+
} else {
|
|
165
|
+
headersObj[lower] = value;
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
this._responseHeaders = headersObj;
|
|
169
|
+
this._state = constants.NGHTTP2_STREAM_STATE_HALF_CLOSED_LOCAL;
|
|
170
|
+
this.emit("response", headersObj, 0);
|
|
171
|
+
try {
|
|
172
|
+
let chunk;
|
|
173
|
+
while ((chunk = await readBytesAsync(inputStream, 16384, GLib.PRIORITY_DEFAULT, this._cancellable)) !== null) {
|
|
174
|
+
if (chunk.length > 0) {
|
|
175
|
+
this.push(Buffer.from(chunk));
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
} catch (_readErr) {
|
|
179
|
+
}
|
|
180
|
+
this.push(null);
|
|
181
|
+
this._state = constants.NGHTTP2_STREAM_STATE_CLOSED;
|
|
182
|
+
} catch (error) {
|
|
183
|
+
this._state = constants.NGHTTP2_STREAM_STATE_CLOSED;
|
|
184
|
+
if (!this._cancellable.is_cancelled()) {
|
|
185
|
+
this.destroy(error instanceof Error ? error : new Error(String(error)));
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
close(code, callback) {
|
|
190
|
+
this._cancellable.cancel();
|
|
191
|
+
this._state = constants.NGHTTP2_STREAM_STATE_CLOSED;
|
|
192
|
+
this.emit("close", code ?? constants.NGHTTP2_NO_ERROR);
|
|
193
|
+
if (callback) callback();
|
|
194
|
+
}
|
|
195
|
+
priority(_options) {
|
|
196
|
+
}
|
|
197
|
+
sendTrailers(_headers) {
|
|
198
|
+
}
|
|
199
|
+
setTimeout(_msecs, _callback) {
|
|
200
|
+
return this;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
class ClientHttp2Session extends Http2Session {
|
|
204
|
+
type = constants.NGHTTP2_SESSION_CLIENT;
|
|
205
|
+
_authority;
|
|
206
|
+
_soupSession;
|
|
207
|
+
_streams = /* @__PURE__ */ new Set();
|
|
208
|
+
constructor(authority, options = {}) {
|
|
209
|
+
super();
|
|
210
|
+
this._authority = authority;
|
|
211
|
+
this.encrypted = authority.startsWith("https:");
|
|
212
|
+
this._soupSession = new Soup.Session();
|
|
213
|
+
if (options.rejectUnauthorized === false) {
|
|
214
|
+
this._soupSession.connect(
|
|
215
|
+
"accept-certificate",
|
|
216
|
+
(_msg, _cert, _errors) => {
|
|
217
|
+
return true;
|
|
218
|
+
}
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
Promise.resolve().then(() => {
|
|
222
|
+
if (!this.destroyed) {
|
|
223
|
+
this.alpnProtocol = this.encrypted ? "h2" : void 0;
|
|
224
|
+
this.emit("connect", this, null);
|
|
225
|
+
}
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
/** @internal Used by ClientHttp2Stream to get the Soup.Session */
|
|
229
|
+
_getSoupSession() {
|
|
230
|
+
return this._soupSession;
|
|
231
|
+
}
|
|
232
|
+
/** @internal Used by ClientHttp2Stream to build the request URL */
|
|
233
|
+
_getAuthority() {
|
|
234
|
+
return this._authority;
|
|
235
|
+
}
|
|
236
|
+
request(headers, options) {
|
|
237
|
+
if (this.destroyed || this.closed) {
|
|
238
|
+
throw new Error("Session is closed");
|
|
239
|
+
}
|
|
240
|
+
const finalHeaders = { ...headers };
|
|
241
|
+
if (!finalHeaders[":scheme"]) {
|
|
242
|
+
finalHeaders[":scheme"] = this.encrypted ? "https" : "http";
|
|
243
|
+
}
|
|
244
|
+
if (!finalHeaders[":authority"]) {
|
|
245
|
+
finalHeaders[":authority"] = this._authority.replace(/^https?:\/\//, "");
|
|
246
|
+
}
|
|
247
|
+
if (!finalHeaders[":method"]) {
|
|
248
|
+
finalHeaders[":method"] = "GET";
|
|
249
|
+
}
|
|
250
|
+
if (!finalHeaders[":path"]) {
|
|
251
|
+
finalHeaders[":path"] = "/";
|
|
252
|
+
}
|
|
253
|
+
const stream = new ClientHttp2Stream(this, finalHeaders);
|
|
254
|
+
this._streams.add(stream);
|
|
255
|
+
stream.once("close", () => this._streams.delete(stream));
|
|
256
|
+
if (options?.endStream) {
|
|
257
|
+
stream.end();
|
|
258
|
+
}
|
|
259
|
+
return stream;
|
|
260
|
+
}
|
|
261
|
+
close(callback) {
|
|
262
|
+
for (const stream of this._streams) {
|
|
263
|
+
stream.close();
|
|
264
|
+
}
|
|
265
|
+
this._streams.clear();
|
|
266
|
+
super.close(callback);
|
|
267
|
+
}
|
|
268
|
+
destroy(error, code) {
|
|
269
|
+
for (const stream of this._streams) {
|
|
270
|
+
stream.close();
|
|
271
|
+
}
|
|
272
|
+
this._streams.clear();
|
|
273
|
+
super.destroy(error, code);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
export {
|
|
277
|
+
ClientHttp2Session,
|
|
278
|
+
ClientHttp2Stream,
|
|
279
|
+
Http2Session
|
|
280
|
+
};
|