@fedify/vocab-runtime 2.4.5-dev.2453 → 2.4.5
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/deno.json +5 -2
- package/dist/{docloader-BMHljyYb.d.cts → docloader-DOWjnNCz.d.cts} +4 -8
- package/dist/{docloader-BMHljyYb.d.ts → docloader-DOWjnNCz.d.ts} +4 -8
- package/dist/internal/jsonld-cache.cjs +1 -1
- package/dist/internal/jsonld-cache.d.cts +1 -1
- package/dist/internal/jsonld-cache.d.ts +1 -1
- package/dist/internal/jsonld-cache.js +1 -1
- package/dist/internal/portable-dereference.cjs +1 -1
- package/dist/internal/portable-dereference.d.cts +2 -2
- package/dist/internal/portable-dereference.d.ts +2 -2
- package/dist/internal/portable-dereference.js +1 -1
- package/dist/internal.cjs +6 -0
- package/dist/internal.d.cts +108 -0
- package/dist/internal.d.ts +108 -0
- package/dist/internal.js +3 -0
- package/dist/mod.cjs +104 -38
- package/dist/mod.d.cts +3 -336
- package/dist/mod.d.ts +4 -336
- package/dist/mod.js +104 -38
- package/dist/{portable-6Aj-7dZJ.d.cts → portable-CTnZWX7J.d.cts} +1 -1
- package/dist/{portable-nkTiqH0w.d.ts → portable-Cq-rhQgY.d.ts} +1 -1
- package/dist/public-fetch-Dvfvawei.cjs +527 -0
- package/dist/public-fetch-ziZ39L5I.js +510 -0
- package/dist/tests/{body-7TFr1l-z.cjs → body-BO79kuv1.cjs} +1 -1
- package/dist/tests/{body-BgPyhKpw.mjs → body-CUzCsDZ4.mjs} +1 -1
- package/dist/tests/body.test.cjs +1 -1
- package/dist/tests/body.test.mjs +1 -1
- package/dist/tests/decimal.test.cjs +3 -3
- package/dist/tests/decimal.test.mjs +3 -3
- package/dist/tests/{docloader-D89fiHaF.cjs → docloader-B79_YgeY.cjs} +111 -39
- package/dist/tests/{docloader-BlukyWHf.mjs → docloader-C5ZHGEfO.mjs} +106 -40
- package/dist/tests/docloader.test.cjs +531 -1293
- package/dist/tests/docloader.test.mjs +472 -1252
- package/dist/tests/esm-Ds-eHTfY.mjs +1229 -0
- package/dist/tests/esm-Q2tdh5f3.cjs +1215 -0
- package/dist/tests/internal/portable-dereference.test.cjs +3 -3
- package/dist/tests/internal/portable-dereference.test.mjs +3 -3
- package/dist/tests/{jsonld-cache-BEXiiM-V.cjs → jsonld-cache-DKIWKxEX.cjs} +1 -1
- package/dist/tests/{jsonld-cache-DFnWsLuZ.mjs → jsonld-cache-KH40z9_7.mjs} +1 -1
- package/dist/tests/jsonld-cache.test.cjs +2 -2
- package/dist/tests/jsonld-cache.test.mjs +2 -2
- package/dist/tests/node-BvS3AhZ2.cjs +89 -0
- package/dist/tests/node-Cg4pRuYy.mjs +84 -0
- package/dist/tests/pin-grant-CPrpk1l4.cjs +70 -0
- package/dist/tests/pin-grant-H5XhM1Xs.mjs +53 -0
- package/dist/tests/pin-grant.test.cjs +87 -0
- package/dist/tests/pin-grant.test.d.cts +1 -0
- package/dist/tests/pin-grant.test.d.mts +1 -0
- package/dist/tests/pin-grant.test.mjs +88 -0
- package/dist/tests/pinners/bun.test.cjs +166 -0
- package/dist/tests/pinners/bun.test.d.cts +1 -0
- package/dist/tests/pinners/bun.test.d.mts +1 -0
- package/dist/tests/pinners/bun.test.mjs +167 -0
- package/dist/tests/pinners/node.test.cjs +269 -0
- package/dist/tests/pinners/node.test.d.cts +1 -0
- package/dist/tests/pinners/node.test.d.mts +1 -0
- package/dist/tests/pinners/node.test.mjs +268 -0
- package/dist/tests/pinners/pinners.test.cjs +196 -0
- package/dist/tests/pinners/pinners.test.d.cts +1 -0
- package/dist/tests/pinners/pinners.test.d.mts +1 -0
- package/dist/tests/pinners/pinners.test.mjs +197 -0
- package/dist/tests/pinners/testdata/node-child.cjs +135 -0
- package/dist/tests/pinners/testdata/node-child.d.cts +1 -0
- package/dist/tests/pinners/testdata/node-child.d.mts +1 -0
- package/dist/tests/pinners/testdata/node-child.mjs +134 -0
- package/dist/tests/pinners-CJi9ZkYs.cjs +220 -0
- package/dist/tests/pinners-zSofbiy2.mjs +203 -0
- package/dist/tests/{portable-dereference-DmTYDy2p.mjs → portable-dereference-BHRE5Pkm.mjs} +2 -2
- package/dist/tests/{portable-dereference-qTLMqe2_.cjs → portable-dereference-mRHrC7PI.cjs} +2 -2
- package/dist/tests/{portable-media-4hfHamVH.mjs → portable-media-BpVpe1gj.mjs} +3 -3
- package/dist/tests/{portable-media-DH6pp6-g.cjs → portable-media-C3yY0W5E.cjs} +3 -3
- package/dist/tests/portable-media.test.cjs +1 -1
- package/dist/tests/portable-media.test.mjs +1 -1
- package/dist/tests/portable-workers.test.cjs +2 -2
- package/dist/tests/portable-workers.test.mjs +2 -2
- package/dist/tests/public-fetch-DNkPoks1.mjs +170 -0
- package/dist/tests/public-fetch-U822jbKB.cjs +186 -0
- package/dist/tests/public-fetch.test.cjs +1073 -0
- package/dist/tests/public-fetch.test.d.cts +1 -0
- package/dist/tests/public-fetch.test.d.mts +1 -0
- package/dist/tests/public-fetch.test.mjs +1074 -0
- package/dist/tests/{request--AMbrM1o.mjs → request-3AP-HBJ6.mjs} +1 -1
- package/dist/tests/{request-RH-Q21hI.cjs → request-BhopJTZd.cjs} +1 -1
- package/dist/tests/request.test.cjs +1 -1
- package/dist/tests/request.test.mjs +1 -1
- package/dist/tests/support-BmzI3dRJ.cjs +363 -0
- package/dist/tests/support-CiHDVJgh.mjs +272 -0
- package/dist/tests/{url-BNakuZ8k.cjs → url-Bdj-rTcC.cjs} +88 -12
- package/dist/tests/{url-DMxmp7ZG.mjs → url-COj5Q8mb.mjs} +65 -13
- package/dist/tests/url.test.cjs +4 -1
- package/dist/tests/url.test.mjs +4 -1
- package/dist/url-4EE56sNP.d.cts +352 -0
- package/dist/url-4EE56sNP.d.ts +352 -0
- package/dist/{url-DrGTR8yv.cjs → url-DIKZnaJv.cjs} +88 -12
- package/dist/{url-Dzyp-NsC.js → url-mWveR5GA.js} +65 -13
- package/package.json +14 -3
- package/src/docloader.test.ts +696 -48
- package/src/docloader.ts +186 -75
- package/src/internal.ts +13 -0
- package/src/pin-grant.test.ts +118 -0
- package/src/pin-grant.ts +99 -0
- package/src/pinners/bun.test.ts +278 -0
- package/src/pinners/bun.ts +223 -0
- package/src/pinners/mod.ts +59 -0
- package/src/pinners/node.test.ts +415 -0
- package/src/pinners/node.ts +144 -0
- package/src/pinners/pinners.test.ts +316 -0
- package/src/pinners/testdata/node-child.ts +147 -0
- package/src/pinners/testdata/proxy-child.ts +82 -0
- package/src/pinners/testdata/support.ts +343 -0
- package/src/pinners/testdata/tls-cert.pem +12 -0
- package/src/pinners/testdata/tls-child.ts +88 -0
- package/src/pinners/testdata/tls-key.pem +5 -0
- package/src/pinners/types.ts +48 -0
- package/src/public-fetch.test.ts +1286 -0
- package/src/public-fetch.ts +338 -0
- package/src/testdata/public-fetch-deno-worker.ts +74 -0
- package/src/url.test.ts +11 -0
- package/src/url.ts +113 -22
- package/tsdown.config.ts +7 -2
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
import { test } from "@fedify/fixture";
|
|
2
|
+
import { deepStrictEqual, ok, rejects } from "node:assert";
|
|
3
|
+
import { createServer } from "node:http";
|
|
4
|
+
import { createServer as createTcpServer } from "node:net";
|
|
5
|
+
import { issuePinGrant, type PinGrant, revokePinGrant } from "../pin-grant.ts";
|
|
6
|
+
import { assertPublicAddress, UrlError } from "../url.ts";
|
|
7
|
+
import { createBunPinner } from "./bun.ts";
|
|
8
|
+
import { createNodePinner } from "./node.ts";
|
|
9
|
+
import {
|
|
10
|
+
allowAll,
|
|
11
|
+
closeServer,
|
|
12
|
+
getLoopbackSupport,
|
|
13
|
+
ipv4,
|
|
14
|
+
ipv6,
|
|
15
|
+
isAddressRejection,
|
|
16
|
+
isBun,
|
|
17
|
+
isNode,
|
|
18
|
+
listen,
|
|
19
|
+
startRecordingServer,
|
|
20
|
+
} from "./testdata/support.ts";
|
|
21
|
+
import type { ConnectionPinner, ConnectionPinnerOptions } from "./types.ts";
|
|
22
|
+
|
|
23
|
+
interface PinnerUnderTest {
|
|
24
|
+
readonly name: string;
|
|
25
|
+
/** Whether real connections can be tested on the current runtime. */
|
|
26
|
+
readonly connects: boolean;
|
|
27
|
+
/**
|
|
28
|
+
* Whether addresses are checked before the request is handed over to
|
|
29
|
+
* the runtime's `fetch()`, so that the checks can be tested on any runtime.
|
|
30
|
+
*/
|
|
31
|
+
readonly checksBeforeFetch: boolean;
|
|
32
|
+
readonly create: (
|
|
33
|
+
options: ConnectionPinnerOptions,
|
|
34
|
+
) => Promise<ConnectionPinner>;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* The pinners of all runtimes. The behaviors they share are tested here, and
|
|
39
|
+
* the ones specific to each runtime in *bun.test.ts* and *node.test.ts*.
|
|
40
|
+
*/
|
|
41
|
+
const pinners: readonly PinnerUnderTest[] = [
|
|
42
|
+
{
|
|
43
|
+
name: "createBunPinner()",
|
|
44
|
+
connects: isBun,
|
|
45
|
+
checksBeforeFetch: true,
|
|
46
|
+
create: (options) => Promise.resolve(createBunPinner(options)),
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
name: "createNodePinner()",
|
|
50
|
+
connects: isNode,
|
|
51
|
+
// The addresses are checked in the lookup of the undici agent:
|
|
52
|
+
checksBeforeFetch: false,
|
|
53
|
+
create: async (options) =>
|
|
54
|
+
createNodePinner({ ...options, undici: await import("undici") }),
|
|
55
|
+
},
|
|
56
|
+
];
|
|
57
|
+
|
|
58
|
+
for (const { name, connects, checksBeforeFetch, create } of pinners) {
|
|
59
|
+
test(`${name} supports only HTTP and HTTPS`, async () => {
|
|
60
|
+
const pinner = await create(allowAll);
|
|
61
|
+
ok(pinner.supports(new URL("http://pinned-host.invalid/")));
|
|
62
|
+
ok(pinner.supports(new URL("https://pinned-host.invalid/")));
|
|
63
|
+
ok(!pinner.supports(new URL("ftp://pinned-host.invalid/")));
|
|
64
|
+
ok(!pinner.supports(new URL("ws://pinned-host.invalid/")));
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test(`${name} checks addresses right before connecting`, {
|
|
68
|
+
ignore: !connects && !checksBeforeFetch,
|
|
69
|
+
}, async () => {
|
|
70
|
+
const { server, port, requests } = await startRecordingServer();
|
|
71
|
+
try {
|
|
72
|
+
// The production check, with a grant for a private address that
|
|
73
|
+
// openPublicFetch() would never issue:
|
|
74
|
+
const pinner = await create({ verifyAddress: assertPublicAddress });
|
|
75
|
+
const url = new URL(`http://pinned-host.invalid:${port}/`);
|
|
76
|
+
const grant = issuePinGrant(url, [ipv4("127.0.0.1")]);
|
|
77
|
+
for (const method of ["GET", "POST"]) {
|
|
78
|
+
await rejects(
|
|
79
|
+
() => pinner.fetch(grant, new Request(url, { method })),
|
|
80
|
+
isAddressRejection,
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
deepStrictEqual(requests, []);
|
|
84
|
+
} finally {
|
|
85
|
+
await closeServer(server);
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test(`${name} does not fall back past a failed check`, {
|
|
90
|
+
ignore: !connects && !checksBeforeFetch,
|
|
91
|
+
}, async () => {
|
|
92
|
+
const { server, port, requests } = await startRecordingServer();
|
|
93
|
+
try {
|
|
94
|
+
const checked: string[] = [];
|
|
95
|
+
const pinner = await create({
|
|
96
|
+
verifyAddress({ address }) {
|
|
97
|
+
checked.push(address);
|
|
98
|
+
if (address !== "127.0.0.1") {
|
|
99
|
+
throw new UrlError(`Invalid or private address: ${address}`);
|
|
100
|
+
}
|
|
101
|
+
},
|
|
102
|
+
});
|
|
103
|
+
// The second address would reach the server, but the first one fails
|
|
104
|
+
// the check, and is never connected to (192.0.2.0/24 is TEST-NET-1):
|
|
105
|
+
const url = new URL(`http://pinned-host.invalid:${port}/`);
|
|
106
|
+
const grant = issuePinGrant(url, [ipv4("192.0.2.1"), ipv4("127.0.0.1")]);
|
|
107
|
+
await rejects(
|
|
108
|
+
() => pinner.fetch(grant, new Request(url)),
|
|
109
|
+
isAddressRejection,
|
|
110
|
+
);
|
|
111
|
+
deepStrictEqual(checked, ["192.0.2.1"]);
|
|
112
|
+
deepStrictEqual(requests, []);
|
|
113
|
+
} finally {
|
|
114
|
+
await closeServer(server);
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test(`${name} redeems the grant for every request`, async () => {
|
|
119
|
+
const { server, port, requests } = await startRecordingServer();
|
|
120
|
+
try {
|
|
121
|
+
const pinner = await create(allowAll);
|
|
122
|
+
const url = new URL(`http://pinned-host.invalid:${port}/`);
|
|
123
|
+
const grant = issuePinGrant(url, [ipv4("127.0.0.1")]);
|
|
124
|
+
// Other origins:
|
|
125
|
+
for (
|
|
126
|
+
const other of [
|
|
127
|
+
`http://other-host.invalid:${port}/`,
|
|
128
|
+
`https://pinned-host.invalid:${port}/`,
|
|
129
|
+
`http://pinned-host.invalid:${port + 1}/`,
|
|
130
|
+
]
|
|
131
|
+
) {
|
|
132
|
+
await rejects(() => pinner.fetch(grant, new Request(other)), TypeError);
|
|
133
|
+
}
|
|
134
|
+
// Forged grants:
|
|
135
|
+
for (const forged of [{}, Object.freeze(Object.create(null))]) {
|
|
136
|
+
await rejects(
|
|
137
|
+
() => pinner.fetch(forged as PinGrant, new Request(url)),
|
|
138
|
+
TypeError,
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
// Revoked grants:
|
|
142
|
+
revokePinGrant(grant);
|
|
143
|
+
await rejects(() => pinner.fetch(grant, new Request(url)), TypeError);
|
|
144
|
+
deepStrictEqual(requests, []);
|
|
145
|
+
} finally {
|
|
146
|
+
await closeServer(server);
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
test(`${name} connects to the pinned address`, {
|
|
151
|
+
ignore: !connects,
|
|
152
|
+
}, async () => {
|
|
153
|
+
const { server, port, requests } = await startRecordingServer();
|
|
154
|
+
try {
|
|
155
|
+
const pinner = await create(allowAll);
|
|
156
|
+
// The .invalid TLD never resolves, so a response proves that the
|
|
157
|
+
// connection went to the pinned address without a DNS lookup:
|
|
158
|
+
const url = new URL(`http://pinned-host.invalid:${port}/path?query`);
|
|
159
|
+
const grant = issuePinGrant(url, [ipv4("127.0.0.1")]);
|
|
160
|
+
const response = await pinner.fetch(
|
|
161
|
+
grant,
|
|
162
|
+
new Request(url, { headers: { "X-Test": "header" } }),
|
|
163
|
+
);
|
|
164
|
+
deepStrictEqual(response.status, 200);
|
|
165
|
+
deepStrictEqual(await response.text(), "pinned");
|
|
166
|
+
const post = await pinner.fetch(
|
|
167
|
+
grant,
|
|
168
|
+
new Request(url, { method: "POST", body: "payload" }),
|
|
169
|
+
);
|
|
170
|
+
deepStrictEqual(await post.text(), "pinned");
|
|
171
|
+
deepStrictEqual(
|
|
172
|
+
requests.map(({ method, url, host, body }) => ({
|
|
173
|
+
method,
|
|
174
|
+
url,
|
|
175
|
+
host,
|
|
176
|
+
body,
|
|
177
|
+
})),
|
|
178
|
+
[
|
|
179
|
+
{
|
|
180
|
+
method: "GET",
|
|
181
|
+
url: "/path?query",
|
|
182
|
+
host: `pinned-host.invalid:${port}`,
|
|
183
|
+
body: "",
|
|
184
|
+
},
|
|
185
|
+
{
|
|
186
|
+
method: "POST",
|
|
187
|
+
url: "/path?query",
|
|
188
|
+
host: `pinned-host.invalid:${port}`,
|
|
189
|
+
body: "payload",
|
|
190
|
+
},
|
|
191
|
+
],
|
|
192
|
+
);
|
|
193
|
+
} finally {
|
|
194
|
+
await closeServer(server);
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
test(`${name} does not follow redirects`, {
|
|
199
|
+
ignore: !connects,
|
|
200
|
+
}, async () => {
|
|
201
|
+
const { server, port, requests } = await startRecordingServer(
|
|
202
|
+
"127.0.0.1",
|
|
203
|
+
(_, res) => {
|
|
204
|
+
res.writeHead(302, { Location: "/target" });
|
|
205
|
+
res.end();
|
|
206
|
+
},
|
|
207
|
+
);
|
|
208
|
+
try {
|
|
209
|
+
const pinner = await create(allowAll);
|
|
210
|
+
const url = new URL(`http://pinned-host.invalid:${port}/`);
|
|
211
|
+
const grant = issuePinGrant(url, [ipv4("127.0.0.1")]);
|
|
212
|
+
const response = await pinner.fetch(
|
|
213
|
+
grant,
|
|
214
|
+
new Request(url, { redirect: "follow" }),
|
|
215
|
+
);
|
|
216
|
+
deepStrictEqual(response.status, 302);
|
|
217
|
+
deepStrictEqual(response.headers.get("Location"), "/target");
|
|
218
|
+
deepStrictEqual(requests.length, 1);
|
|
219
|
+
} finally {
|
|
220
|
+
await closeServer(server);
|
|
221
|
+
}
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
test(`${name} aborts requests on either signal`, {
|
|
225
|
+
ignore: !connects,
|
|
226
|
+
}, async () => {
|
|
227
|
+
// The server never responds, so only an abort ends the request:
|
|
228
|
+
let notifyReceived = () => {};
|
|
229
|
+
const server = createServer(() => notifyReceived());
|
|
230
|
+
const port = await listen(server);
|
|
231
|
+
try {
|
|
232
|
+
const pinner = await create(allowAll);
|
|
233
|
+
const url = new URL(`http://pinned-host.invalid:${port}/`);
|
|
234
|
+
const grant = issuePinGrant(url, [ipv4("127.0.0.1")]);
|
|
235
|
+
for (const which of ["request", "separate"] as const) {
|
|
236
|
+
const requestController = new AbortController();
|
|
237
|
+
const separateController = new AbortController();
|
|
238
|
+
const received = new Promise<void>((resolve) =>
|
|
239
|
+
notifyReceived = resolve
|
|
240
|
+
);
|
|
241
|
+
const response = pinner.fetch(
|
|
242
|
+
grant,
|
|
243
|
+
new Request(url, { signal: requestController.signal }),
|
|
244
|
+
separateController.signal,
|
|
245
|
+
);
|
|
246
|
+
await received;
|
|
247
|
+
(which === "request" ? requestController : separateController).abort();
|
|
248
|
+
await rejects(response, { name: "AbortError" }, which);
|
|
249
|
+
}
|
|
250
|
+
} finally {
|
|
251
|
+
await closeServer(server);
|
|
252
|
+
}
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
test(`${name} does not deliver a POST request twice`, {
|
|
256
|
+
ignore: !connects,
|
|
257
|
+
}, async () => {
|
|
258
|
+
// Skipped where a server cannot listen on both loopback addresses:
|
|
259
|
+
if (!(await getLoopbackSupport()).dualStack) return;
|
|
260
|
+
// The server receives the whole request, and then drops the connection
|
|
261
|
+
// without responding, as if it crashed right after processing it.
|
|
262
|
+
// It parses requests by itself since the `node:http` server of older Bun
|
|
263
|
+
// versions (e.g., 1.2.0) responds instead of dropping the connection:
|
|
264
|
+
let received = 0;
|
|
265
|
+
const server = createTcpServer((socket) => {
|
|
266
|
+
let data = "";
|
|
267
|
+
socket.setEncoding("latin1");
|
|
268
|
+
socket.on("error", () => {});
|
|
269
|
+
socket.on("data", (chunk: string) => {
|
|
270
|
+
data += chunk;
|
|
271
|
+
const headEnd = data.indexOf("\r\n\r\n");
|
|
272
|
+
if (headEnd < 0) return;
|
|
273
|
+
const length = /^content-length:\s*(\d+)/im.exec(
|
|
274
|
+
data.slice(0, headEnd),
|
|
275
|
+
)?.[1];
|
|
276
|
+
const complete = length == null
|
|
277
|
+
? data.endsWith("\r\n0\r\n\r\n")
|
|
278
|
+
: data.length - headEnd - 4 >= Number(length);
|
|
279
|
+
if (!complete) return;
|
|
280
|
+
received++;
|
|
281
|
+
socket.destroy();
|
|
282
|
+
});
|
|
283
|
+
});
|
|
284
|
+
const port = await listen(server, "::");
|
|
285
|
+
try {
|
|
286
|
+
const pinner = await create(allowAll);
|
|
287
|
+
const url = new URL(`http://pinned-host.invalid:${port}/inbox`);
|
|
288
|
+
const grant = issuePinGrant(url, [ipv4("127.0.0.1"), ipv6("::1")]);
|
|
289
|
+
await rejects(() =>
|
|
290
|
+
pinner.fetch(
|
|
291
|
+
grant,
|
|
292
|
+
new Request(url, { method: "POST", body: "activity" }),
|
|
293
|
+
)
|
|
294
|
+
);
|
|
295
|
+
deepStrictEqual(received, 1);
|
|
296
|
+
// Control: the second address reaches the same server, so falling back
|
|
297
|
+
// would have delivered the activity twice. (Whether a GET request
|
|
298
|
+
// falls back here depends on the runtime: Bun 1.4 raises this
|
|
299
|
+
// ECONNRESET as a TypeError, but Bun 1.2 does not.) The first grant is
|
|
300
|
+
// released, since the Node.js pinner connects to the addresses of all
|
|
301
|
+
// live grants for the hostname:
|
|
302
|
+
revokePinGrant(grant);
|
|
303
|
+
await pinner.release(grant);
|
|
304
|
+
const second = issuePinGrant(url, [ipv6("::1")]);
|
|
305
|
+
await rejects(() =>
|
|
306
|
+
pinner.fetch(
|
|
307
|
+
second,
|
|
308
|
+
new Request(url, { method: "POST", body: "activity" }),
|
|
309
|
+
)
|
|
310
|
+
);
|
|
311
|
+
deepStrictEqual(received, 2);
|
|
312
|
+
} finally {
|
|
313
|
+
await closeServer(server);
|
|
314
|
+
}
|
|
315
|
+
});
|
|
316
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A child process for the regression tests of the Node.js pinner in
|
|
3
|
+
* *../node.test.ts* that depend on the environment at startup: Node.js reads
|
|
4
|
+
* proxy environment variables (with `NODE_USE_ENV_PROXY=1`) and
|
|
5
|
+
* `NODE_EXTRA_CA_CERTS` only at startup.
|
|
6
|
+
*
|
|
7
|
+
* Node.js cannot run TypeScript sources without flags before 22.6.0, so
|
|
8
|
+
* *../../../tsdown.config.ts* compiles this script into
|
|
9
|
+
* *dist/tests/pinners/testdata/node-child.mjs*, which the tests run. It has
|
|
10
|
+
* no top-level `await`, so that it can be compiled to CommonJS as well.
|
|
11
|
+
*
|
|
12
|
+
* Usage:
|
|
13
|
+
*
|
|
14
|
+
* - `node node-child.mjs proxy <http-port> <https-port>`: requests
|
|
15
|
+
* `http://pinned-host.invalid:<http-port>/pinned` and
|
|
16
|
+
* `https://pinned-host.invalid:<https-port>/pinned` through a pinner
|
|
17
|
+
* that allows loopback addresses, with both pinned to 127.0.0.1, and then
|
|
18
|
+
* `http://127.0.0.1:<http-port>/control` with the global `fetch()` as
|
|
19
|
+
* a control.
|
|
20
|
+
* - `node node-child.mjs tls <https-port>`: requests
|
|
21
|
+
* `https://<hostname>:<https-port>/<case>` through a pinner that allows
|
|
22
|
+
* loopback addresses, with every hostname pinned to 127.0.0.1, in the same
|
|
23
|
+
* order as *tls-child.ts* does for Bun.
|
|
24
|
+
*
|
|
25
|
+
* It prints the results as a JSON line.
|
|
26
|
+
* @module
|
|
27
|
+
*/
|
|
28
|
+
import process from "node:process";
|
|
29
|
+
import { issuePinGrant } from "../../pin-grant.ts";
|
|
30
|
+
import { createNodePinner } from "../node.ts";
|
|
31
|
+
import type { ConnectionPinner } from "../types.ts";
|
|
32
|
+
|
|
33
|
+
type Result =
|
|
34
|
+
| { readonly status: number; readonly body: string }
|
|
35
|
+
| { readonly error: string };
|
|
36
|
+
|
|
37
|
+
const loopback = [{ address: "127.0.0.1", family: 4 }] as const;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Describes an error by its code, or by the code of its cause, since
|
|
41
|
+
* the `fetch()` of Node.js rejects with a `TypeError` caused by the actual
|
|
42
|
+
* error.
|
|
43
|
+
*/
|
|
44
|
+
function describeError(error: unknown): string {
|
|
45
|
+
for (let e = error; typeof e === "object" && e !== null;) {
|
|
46
|
+
const { code, cause } = e as { code?: unknown; cause?: unknown };
|
|
47
|
+
if (typeof code === "string") return code;
|
|
48
|
+
e = cause;
|
|
49
|
+
}
|
|
50
|
+
return String(error);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async function attempt(fn: () => Promise<Response>): Promise<Result> {
|
|
54
|
+
try {
|
|
55
|
+
const response = await fn();
|
|
56
|
+
return { status: response.status, body: await response.text() };
|
|
57
|
+
} catch (error) {
|
|
58
|
+
return { error: describeError(error) };
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function fetchPinned(
|
|
63
|
+
pinner: ConnectionPinner,
|
|
64
|
+
url: URL,
|
|
65
|
+
withSignals = false,
|
|
66
|
+
): Promise<Result> {
|
|
67
|
+
const grant = issuePinGrant(url, loopback);
|
|
68
|
+
return attempt(() =>
|
|
69
|
+
withSignals
|
|
70
|
+
? pinner.fetch(
|
|
71
|
+
grant,
|
|
72
|
+
new Request(url, { signal: new AbortController().signal }),
|
|
73
|
+
new AbortController().signal,
|
|
74
|
+
)
|
|
75
|
+
: pinner.fetch(grant, new Request(url))
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async function proxy(
|
|
80
|
+
pinner: ConnectionPinner,
|
|
81
|
+
httpPort: number,
|
|
82
|
+
httpsPort: number,
|
|
83
|
+
): Promise<unknown> {
|
|
84
|
+
const http = await fetchPinned(
|
|
85
|
+
pinner,
|
|
86
|
+
new URL(`http://pinned-host.invalid:${httpPort}/pinned`),
|
|
87
|
+
);
|
|
88
|
+
const https = await fetchPinned(
|
|
89
|
+
pinner,
|
|
90
|
+
new URL(`https://pinned-host.invalid:${httpsPort}/pinned`),
|
|
91
|
+
);
|
|
92
|
+
// Without a dispatcher, so that it shows whether the proxy is in effect:
|
|
93
|
+
const control = await attempt(() =>
|
|
94
|
+
fetch(`http://127.0.0.1:${httpPort}/control`, {
|
|
95
|
+
redirect: "manual",
|
|
96
|
+
signal: AbortSignal.timeout(10_000),
|
|
97
|
+
})
|
|
98
|
+
);
|
|
99
|
+
return { version: process.version, http, https, control };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async function tls(pinner: ConnectionPinner, port: number): Promise<unknown> {
|
|
103
|
+
const results: ({ readonly case: string } & Result)[] = [];
|
|
104
|
+
for (
|
|
105
|
+
const [name, hostname, withSignals] of [
|
|
106
|
+
["pinned", "pinned-host.invalid", false],
|
|
107
|
+
["unlisted", "unlisted-host.invalid", false],
|
|
108
|
+
["unlisted-again", "unlisted-host.invalid", false],
|
|
109
|
+
["alternate", "alternate-host.invalid", false],
|
|
110
|
+
["signals", "pinned-host.invalid", true],
|
|
111
|
+
] as const
|
|
112
|
+
) {
|
|
113
|
+
const url = new URL(`https://${hostname}:${port}/${name}`);
|
|
114
|
+
results.push({
|
|
115
|
+
case: name,
|
|
116
|
+
...await fetchPinned(pinner, url, withSignals),
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
return { version: process.version, results };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async function main(): Promise<unknown> {
|
|
123
|
+
const [mode, ...ports] = process.argv.slice(2);
|
|
124
|
+
const [first, second] = ports.map(Number);
|
|
125
|
+
const pinner = createNodePinner({
|
|
126
|
+
verifyAddress() {},
|
|
127
|
+
undici: await import("undici"),
|
|
128
|
+
});
|
|
129
|
+
if (mode === "proxy" && Number.isInteger(first) && Number.isInteger(second)) {
|
|
130
|
+
return await proxy(pinner, first, second);
|
|
131
|
+
} else if (mode === "tls" && Number.isInteger(first)) {
|
|
132
|
+
return await tls(pinner, first);
|
|
133
|
+
}
|
|
134
|
+
throw new TypeError(
|
|
135
|
+
"Usage: node-child.mjs proxy <http-port> <https-port> | " +
|
|
136
|
+
"node-child.mjs tls <https-port>",
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
main().then(
|
|
141
|
+
(output) =>
|
|
142
|
+
process.stdout.write(`${JSON.stringify(output)}\n`, () => process.exit(0)),
|
|
143
|
+
(error) => {
|
|
144
|
+
console.error(error);
|
|
145
|
+
process.exit(1);
|
|
146
|
+
},
|
|
147
|
+
);
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A child process for the environment proxy regression test of the Bun
|
|
3
|
+
* pinner in *../bun.test.ts*. Bun reads proxy environment variables only at
|
|
4
|
+
* startup in older versions, so the test runs this script in a new Bun
|
|
5
|
+
* process with the variables set.
|
|
6
|
+
*
|
|
7
|
+
* Usage: `bun proxy-child.ts <http-port> <https-port>`
|
|
8
|
+
*
|
|
9
|
+
* It requests `http://pinned-host.invalid:<http-port>/pinned` and
|
|
10
|
+
* `https://pinned-host.invalid:<https-port>/pinned` through a pinner that
|
|
11
|
+
* allows loopback addresses, with both pinned to 127.0.0.1, and then
|
|
12
|
+
* `http://127.0.0.1:<http-port>/control` with the global `fetch()` as a
|
|
13
|
+
* control. It prints the results as a JSON line.
|
|
14
|
+
* @module
|
|
15
|
+
*/
|
|
16
|
+
import { configure, type LogRecord } from "@logtape/logtape";
|
|
17
|
+
import process from "node:process";
|
|
18
|
+
import { issuePinGrant } from "../../pin-grant.ts";
|
|
19
|
+
import { createBunPinner } from "../bun.ts";
|
|
20
|
+
|
|
21
|
+
type Result =
|
|
22
|
+
| { readonly status: number; readonly body: string }
|
|
23
|
+
| { readonly error: string };
|
|
24
|
+
|
|
25
|
+
async function attempt(fn: () => Promise<Response>): Promise<Result> {
|
|
26
|
+
try {
|
|
27
|
+
const response = await fn();
|
|
28
|
+
return { status: response.status, body: await response.text() };
|
|
29
|
+
} catch (error) {
|
|
30
|
+
const code = (error as { code?: unknown } | null)?.code;
|
|
31
|
+
return { error: typeof code === "string" ? code : String(error) };
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const [httpPort, httpsPort] = process.argv.slice(2).map(Number);
|
|
36
|
+
if (!Number.isInteger(httpPort) || !Number.isInteger(httpsPort)) {
|
|
37
|
+
throw new TypeError("Usage: proxy-child.ts <http-port> <https-port>");
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const warnings: LogRecord[] = [];
|
|
41
|
+
await configure({
|
|
42
|
+
sinks: { buffer: (record) => warnings.push(record) },
|
|
43
|
+
loggers: [
|
|
44
|
+
{
|
|
45
|
+
category: ["fedify", "runtime", "fetch"],
|
|
46
|
+
sinks: ["buffer"],
|
|
47
|
+
lowestLevel: "warning",
|
|
48
|
+
},
|
|
49
|
+
{ category: ["logtape", "meta"], sinks: [] },
|
|
50
|
+
],
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
const pinner = createBunPinner({ verifyAddress() {} });
|
|
54
|
+
const loopback = [{ address: "127.0.0.1", family: 4 }] as const;
|
|
55
|
+
|
|
56
|
+
async function fetchPinned(url: URL): Promise<Result> {
|
|
57
|
+
const grant = issuePinGrant(url, loopback);
|
|
58
|
+
return await attempt(() => pinner.fetch(grant, new Request(url)));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const http = await fetchPinned(
|
|
62
|
+
new URL(`http://pinned-host.invalid:${httpPort}/pinned`),
|
|
63
|
+
);
|
|
64
|
+
const https = await fetchPinned(
|
|
65
|
+
new URL(`https://pinned-host.invalid:${httpsPort}/pinned`),
|
|
66
|
+
);
|
|
67
|
+
// Without proxy: false, so that it shows the proxy is in effect:
|
|
68
|
+
const control = await attempt(() =>
|
|
69
|
+
fetch(`http://127.0.0.1:${httpPort}/control`, { redirect: "manual" })
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
const output = JSON.stringify({
|
|
73
|
+
version: (globalThis as { Bun?: { version: string } }).Bun?.version,
|
|
74
|
+
http,
|
|
75
|
+
https,
|
|
76
|
+
control,
|
|
77
|
+
warnings: warnings.map((record) => record.rawMessage),
|
|
78
|
+
});
|
|
79
|
+
await new Promise<void>((resolve) =>
|
|
80
|
+
process.stdout.write(`${output}\n`, () => resolve())
|
|
81
|
+
);
|
|
82
|
+
process.exit(0);
|