@fedify/vocab-runtime 2.5.0-dev.2446 → 2.5.0-dev.2492
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-BBnBpbsG.d.cts → docloader-DD6binpN.d.cts} +4 -8
- package/dist/{docloader-BBnBpbsG.d.ts → docloader-DD6binpN.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 +72 -7
- package/dist/internal/portable-dereference.d.cts +63 -3
- package/dist/internal/portable-dereference.d.ts +63 -3
- package/dist/internal/portable-dereference.js +72 -8
- 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-BDydjp5H.d.ts → portable-1VSnSJFj.d.ts} +1 -1
- package/dist/{portable-Dfvcg_Wy.d.cts → portable-Cr7NwZwb.d.cts} +1 -1
- package/dist/public-fetch-Dvfvawei.cjs +527 -0
- package/dist/public-fetch-ziZ39L5I.js +510 -0
- package/dist/tests/{body-Ddx6WgR5.cjs → body-C8d1PmDa.cjs} +1 -1
- package/dist/tests/{body-CQi2bVi5.mjs → body-HSLxhexN.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 +4 -4
- package/dist/tests/decimal.test.mjs +4 -4
- package/dist/tests/{docloader-C3vDXwn1.cjs → docloader-D1fNCGam.cjs} +111 -39
- package/dist/tests/{docloader-CrVannnU.mjs → docloader-DkEUFJui.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-CbBR-th-.mjs +84 -0
- package/dist/tests/node-Ch29PDXR.cjs +89 -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-DLH_npLf.cjs +220 -0
- package/dist/tests/pinners-tGcl49uF.mjs +203 -0
- package/dist/tests/{portable-dereference-DaTBBvCj.cjs → portable-dereference-BUoDzMxk.cjs} +2 -2
- package/dist/tests/{portable-dereference-sGRs_qdT.mjs → portable-dereference-Du2k3ngE.mjs} +2 -2
- package/dist/tests/{portable-media-BJf_gGQz.cjs → portable-media-BM9zgj9g.cjs} +3 -3
- package/dist/tests/{portable-media-Cc58dUeE.mjs → portable-media-CTxGibcj.mjs} +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-BGj5pzUD.cjs +186 -0
- package/dist/tests/public-fetch-Cxo3Yzdf.mjs +170 -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-glvxfx2U.cjs → request-7q-Zfe1p.cjs} +2 -2
- package/dist/tests/{request-1pPyYz3e.mjs → request-D5uGtmmq.mjs} +2 -2
- package/dist/tests/request.test.cjs +2 -2
- package/dist/tests/request.test.mjs +2 -2
- package/dist/tests/support-Bj44xlQu.mjs +272 -0
- package/dist/tests/support-BrdlbLJk.cjs +363 -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/portable-dereference.ts +116 -6
- 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,1073 @@
|
|
|
1
|
+
const require_url = require("./url-Bdj-rTcC.cjs");
|
|
2
|
+
const require_pin_grant = require("./pin-grant-CPrpk1l4.cjs");
|
|
3
|
+
const require_pinners = require("./pinners-DLH_npLf.cjs");
|
|
4
|
+
const require_public_fetch = require("./public-fetch-BGj5pzUD.cjs");
|
|
5
|
+
const require_esm = require("./esm-Q2tdh5f3.cjs");
|
|
6
|
+
let _logtape_logtape = require("@logtape/logtape");
|
|
7
|
+
let node_assert = require("node:assert");
|
|
8
|
+
let _fedify_fixture = require("@fedify/fixture");
|
|
9
|
+
let node_http = require("node:http");
|
|
10
|
+
//#region src/public-fetch.test.ts
|
|
11
|
+
function createRecordingPinner(respond = () => new Response("ok")) {
|
|
12
|
+
const calls = [];
|
|
13
|
+
const grants = [];
|
|
14
|
+
const released = [];
|
|
15
|
+
let releaseError;
|
|
16
|
+
return {
|
|
17
|
+
pinner: {
|
|
18
|
+
supports(url) {
|
|
19
|
+
return url.protocol === "http:" || url.protocol === "https:";
|
|
20
|
+
},
|
|
21
|
+
async fetch(grant, request) {
|
|
22
|
+
const { addresses } = require_pin_grant.redeemPinGrant(grant, new URL(request.url));
|
|
23
|
+
grants.push(grant);
|
|
24
|
+
const index = calls.length;
|
|
25
|
+
calls.push({
|
|
26
|
+
url: request.url,
|
|
27
|
+
method: request.method,
|
|
28
|
+
redirect: request.redirect,
|
|
29
|
+
addresses
|
|
30
|
+
});
|
|
31
|
+
return await respond(request, index);
|
|
32
|
+
},
|
|
33
|
+
release(grant) {
|
|
34
|
+
released.push(grant);
|
|
35
|
+
if (releaseError != null) throw releaseError;
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
calls,
|
|
39
|
+
grants,
|
|
40
|
+
released,
|
|
41
|
+
failRelease(error) {
|
|
42
|
+
releaseError = error;
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
function createRecordingLookup(...results) {
|
|
47
|
+
const hostnames = [];
|
|
48
|
+
function lookup(hostname) {
|
|
49
|
+
hostnames.push(hostname);
|
|
50
|
+
const result = results[Math.min(hostnames.length, results.length) - 1];
|
|
51
|
+
return result instanceof Error ? Promise.reject(result) : Promise.resolve(result);
|
|
52
|
+
}
|
|
53
|
+
return {
|
|
54
|
+
lookup,
|
|
55
|
+
hostnames
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
const publicAddresses = [{
|
|
59
|
+
address: "8.8.8.8",
|
|
60
|
+
family: 4
|
|
61
|
+
}];
|
|
62
|
+
function failingLookup() {
|
|
63
|
+
return Promise.reject(/* @__PURE__ */ new Error("The default lookup must not be used."));
|
|
64
|
+
}
|
|
65
|
+
function createTrackedBody(options = {}) {
|
|
66
|
+
const state = {
|
|
67
|
+
cancelled: 0,
|
|
68
|
+
cancelReasons: []
|
|
69
|
+
};
|
|
70
|
+
let sent = false;
|
|
71
|
+
return {
|
|
72
|
+
stream: new ReadableStream({
|
|
73
|
+
pull(controller) {
|
|
74
|
+
if (sent) {
|
|
75
|
+
controller.close();
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
sent = true;
|
|
79
|
+
controller.enqueue(new TextEncoder().encode(options.text ?? "body"));
|
|
80
|
+
},
|
|
81
|
+
cancel(reason) {
|
|
82
|
+
state.cancelled++;
|
|
83
|
+
state.cancelReasons.push(reason);
|
|
84
|
+
if (options.cancelError != null) throw options.cancelError;
|
|
85
|
+
}
|
|
86
|
+
}),
|
|
87
|
+
state
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
async function captureLogs(fn) {
|
|
91
|
+
const records = [];
|
|
92
|
+
await (0, _logtape_logtape.configure)({
|
|
93
|
+
sinks: { buffer: (record) => records.push(record) },
|
|
94
|
+
loggers: [{
|
|
95
|
+
category: [
|
|
96
|
+
"fedify",
|
|
97
|
+
"runtime",
|
|
98
|
+
"fetch"
|
|
99
|
+
],
|
|
100
|
+
sinks: ["buffer"],
|
|
101
|
+
lowestLevel: "debug"
|
|
102
|
+
}, {
|
|
103
|
+
category: ["logtape", "meta"],
|
|
104
|
+
sinks: []
|
|
105
|
+
}],
|
|
106
|
+
reset: true
|
|
107
|
+
});
|
|
108
|
+
try {
|
|
109
|
+
return {
|
|
110
|
+
result: await fn(),
|
|
111
|
+
records
|
|
112
|
+
};
|
|
113
|
+
} finally {
|
|
114
|
+
await (0, _logtape_logtape.reset)();
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Waits until the rejections of body cancellations started without being
|
|
119
|
+
* awaited have been handled.
|
|
120
|
+
*/
|
|
121
|
+
function settleCancellations() {
|
|
122
|
+
return new Promise((resolve) => setTimeout(resolve, 0));
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Fails instead of blocking the test if the given promise does not settle
|
|
126
|
+
* within the given time.
|
|
127
|
+
*/
|
|
128
|
+
async function settlesWithin(promise, milliseconds = 1e3) {
|
|
129
|
+
let timer;
|
|
130
|
+
try {
|
|
131
|
+
return await Promise.race([promise, new Promise((_, reject) => {
|
|
132
|
+
timer = setTimeout(() => reject(/* @__PURE__ */ new Error(`Did not settle in ${milliseconds} ms.`)), milliseconds);
|
|
133
|
+
})]);
|
|
134
|
+
} finally {
|
|
135
|
+
clearTimeout(timer);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
function cancelFailureLogs(records) {
|
|
139
|
+
return records.filter((record) => record.rawMessage === "Failed to cancel a response body from {url}: {error}").map((record) => [
|
|
140
|
+
record.level,
|
|
141
|
+
record.properties.url,
|
|
142
|
+
record.properties.error
|
|
143
|
+
]);
|
|
144
|
+
}
|
|
145
|
+
async function withMockedGlobalFetch(fn) {
|
|
146
|
+
require_esm.esm_default.mockGlobal();
|
|
147
|
+
try {
|
|
148
|
+
return await fn();
|
|
149
|
+
} finally {
|
|
150
|
+
require_esm.esm_default.hardReset();
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
async function listen(server, host = "127.0.0.1") {
|
|
154
|
+
await new Promise((resolve) => server.listen(0, host, resolve));
|
|
155
|
+
return server.address().port;
|
|
156
|
+
}
|
|
157
|
+
async function closeServer(server) {
|
|
158
|
+
server.closeAllConnections?.();
|
|
159
|
+
await new Promise((resolve) => server.close(() => resolve()));
|
|
160
|
+
}
|
|
161
|
+
(0, _fedify_fixture.test)("openPublicFetch() pins IP literal hostnames without lookups", async () => {
|
|
162
|
+
const { lookup, hostnames } = createRecordingLookup(publicAddresses);
|
|
163
|
+
const recorder = createRecordingPinner();
|
|
164
|
+
const { openPublicFetch } = require_public_fetch.createPublicFetch({
|
|
165
|
+
pinner: () => Promise.resolve(recorder.pinner),
|
|
166
|
+
lookup: failingLookup
|
|
167
|
+
});
|
|
168
|
+
for (const [url, address] of [["https://8.8.8.8/", {
|
|
169
|
+
address: "8.8.8.8",
|
|
170
|
+
family: 4
|
|
171
|
+
}], ["https://[2001:4860:4860::8888]:8443/", {
|
|
172
|
+
address: "2001:4860:4860::8888",
|
|
173
|
+
family: 6
|
|
174
|
+
}]]) {
|
|
175
|
+
const session = await openPublicFetch(url, { lookup });
|
|
176
|
+
try {
|
|
177
|
+
(0, node_assert.deepStrictEqual)(await (await session.fetch(url)).text(), "ok");
|
|
178
|
+
} finally {
|
|
179
|
+
await session.close();
|
|
180
|
+
}
|
|
181
|
+
(0, node_assert.deepStrictEqual)(recorder.calls.at(-1)?.addresses, [address]);
|
|
182
|
+
}
|
|
183
|
+
(0, node_assert.deepStrictEqual)(hostnames, []);
|
|
184
|
+
});
|
|
185
|
+
(0, _fedify_fixture.test)("openPublicFetch() pins the addresses the lookup returned", async () => {
|
|
186
|
+
const { lookup, hostnames } = createRecordingLookup([
|
|
187
|
+
{
|
|
188
|
+
address: "8.8.8.8",
|
|
189
|
+
family: 4
|
|
190
|
+
},
|
|
191
|
+
{
|
|
192
|
+
address: "cname-target.invalid",
|
|
193
|
+
family: 0
|
|
194
|
+
},
|
|
195
|
+
{
|
|
196
|
+
address: "2001:4860:4860::8888",
|
|
197
|
+
family: 6
|
|
198
|
+
}
|
|
199
|
+
]);
|
|
200
|
+
const recorder = createRecordingPinner();
|
|
201
|
+
const { openPublicFetch } = require_public_fetch.createPublicFetch({
|
|
202
|
+
pinner: () => Promise.resolve(recorder.pinner),
|
|
203
|
+
lookup: failingLookup
|
|
204
|
+
});
|
|
205
|
+
const session = await openPublicFetch("https://PINNED-HOST.invalid/object", { lookup });
|
|
206
|
+
try {
|
|
207
|
+
(0, node_assert.deepStrictEqual)(session.url.href, "https://pinned-host.invalid/object");
|
|
208
|
+
session.url.pathname = "/changed";
|
|
209
|
+
(0, node_assert.deepStrictEqual)(session.url.href, "https://pinned-host.invalid/object");
|
|
210
|
+
(0, node_assert.deepStrictEqual)(await (await session.fetch("https://pinned-host.invalid/object")).text(), "ok");
|
|
211
|
+
} finally {
|
|
212
|
+
await session.close();
|
|
213
|
+
}
|
|
214
|
+
(0, node_assert.deepStrictEqual)(hostnames, ["pinned-host.invalid"]);
|
|
215
|
+
(0, node_assert.deepStrictEqual)(recorder.calls, [{
|
|
216
|
+
url: "https://pinned-host.invalid/object",
|
|
217
|
+
method: "GET",
|
|
218
|
+
redirect: "manual",
|
|
219
|
+
addresses: [{
|
|
220
|
+
address: "8.8.8.8",
|
|
221
|
+
family: 4
|
|
222
|
+
}, {
|
|
223
|
+
address: "2001:4860:4860::8888",
|
|
224
|
+
family: 6
|
|
225
|
+
}]
|
|
226
|
+
}]);
|
|
227
|
+
(0, node_assert.deepStrictEqual)(recorder.released, recorder.grants.slice(0, 1));
|
|
228
|
+
});
|
|
229
|
+
(0, _fedify_fixture.test)("openPublicFetch() uses the default lookup without options", async () => {
|
|
230
|
+
const { lookup, hostnames } = createRecordingLookup(publicAddresses);
|
|
231
|
+
const recorder = createRecordingPinner();
|
|
232
|
+
const { openPublicFetch } = require_public_fetch.createPublicFetch({
|
|
233
|
+
pinner: () => Promise.resolve(recorder.pinner),
|
|
234
|
+
lookup
|
|
235
|
+
});
|
|
236
|
+
await (await openPublicFetch("https://pinned-host.invalid/")).close();
|
|
237
|
+
(0, node_assert.deepStrictEqual)(hostnames, ["pinned-host.invalid"]);
|
|
238
|
+
});
|
|
239
|
+
(0, _fedify_fixture.test)("openPublicFetch() rejects disallowed URLs", async (t) => {
|
|
240
|
+
const cases = [
|
|
241
|
+
{
|
|
242
|
+
name: "protocol",
|
|
243
|
+
url: "ftp://pinned-host.invalid/",
|
|
244
|
+
reason: "disallowed"
|
|
245
|
+
},
|
|
246
|
+
{
|
|
247
|
+
name: "localhost",
|
|
248
|
+
url: "https://localhost/",
|
|
249
|
+
reason: "disallowed"
|
|
250
|
+
},
|
|
251
|
+
{
|
|
252
|
+
name: "IPv4 loopback",
|
|
253
|
+
url: "https://127.0.0.1/",
|
|
254
|
+
reason: "disallowed"
|
|
255
|
+
},
|
|
256
|
+
{
|
|
257
|
+
name: "IPv6 loopback",
|
|
258
|
+
url: "https://[::1]/",
|
|
259
|
+
reason: "disallowed"
|
|
260
|
+
},
|
|
261
|
+
{
|
|
262
|
+
name: "IPv4-mapped IPv6",
|
|
263
|
+
url: "https://[::ffff:7f00:1]/",
|
|
264
|
+
reason: "disallowed"
|
|
265
|
+
},
|
|
266
|
+
{
|
|
267
|
+
name: "credentials",
|
|
268
|
+
url: "https://user:password@pinned-host.invalid/",
|
|
269
|
+
lookup: publicAddresses,
|
|
270
|
+
reason: "disallowed"
|
|
271
|
+
},
|
|
272
|
+
{
|
|
273
|
+
name: "username",
|
|
274
|
+
url: "https://user@pinned-host.invalid/",
|
|
275
|
+
lookup: publicAddresses,
|
|
276
|
+
reason: "disallowed"
|
|
277
|
+
},
|
|
278
|
+
{
|
|
279
|
+
name: "private lookup result",
|
|
280
|
+
url: "https://pinned-host.invalid/",
|
|
281
|
+
lookup: [{
|
|
282
|
+
address: "10.0.0.1",
|
|
283
|
+
family: 4
|
|
284
|
+
}],
|
|
285
|
+
reason: "disallowed"
|
|
286
|
+
},
|
|
287
|
+
{
|
|
288
|
+
name: "mixed lookup result",
|
|
289
|
+
url: "https://pinned-host.invalid/",
|
|
290
|
+
lookup: [{
|
|
291
|
+
address: "8.8.8.8",
|
|
292
|
+
family: 4
|
|
293
|
+
}, {
|
|
294
|
+
address: "127.0.0.1",
|
|
295
|
+
family: 4
|
|
296
|
+
}],
|
|
297
|
+
reason: "disallowed"
|
|
298
|
+
},
|
|
299
|
+
{
|
|
300
|
+
name: "mixed IPv6 lookup result",
|
|
301
|
+
url: "https://pinned-host.invalid/",
|
|
302
|
+
lookup: [{
|
|
303
|
+
address: "2001:4860:4860::8888",
|
|
304
|
+
family: 6
|
|
305
|
+
}, {
|
|
306
|
+
address: "fd00::1",
|
|
307
|
+
family: 6
|
|
308
|
+
}],
|
|
309
|
+
reason: "disallowed"
|
|
310
|
+
},
|
|
311
|
+
{
|
|
312
|
+
name: "lookup failure",
|
|
313
|
+
url: "https://pinned-host.invalid/",
|
|
314
|
+
lookup: Object.assign(/* @__PURE__ */ new Error("getaddrinfo ENOTFOUND"), { code: "ENOTFOUND" }),
|
|
315
|
+
reason: "dns"
|
|
316
|
+
},
|
|
317
|
+
{
|
|
318
|
+
name: "empty lookup result",
|
|
319
|
+
url: "https://pinned-host.invalid/",
|
|
320
|
+
lookup: [],
|
|
321
|
+
reason: "dns"
|
|
322
|
+
},
|
|
323
|
+
{
|
|
324
|
+
name: "CNAME-only lookup result",
|
|
325
|
+
url: "https://pinned-host.invalid/",
|
|
326
|
+
lookup: [{
|
|
327
|
+
address: "cname-target.invalid",
|
|
328
|
+
family: 0
|
|
329
|
+
}],
|
|
330
|
+
reason: "dns"
|
|
331
|
+
}
|
|
332
|
+
];
|
|
333
|
+
for (const { name, url, lookup: result, reason } of cases) await t.step(name, async () => {
|
|
334
|
+
const { lookup } = createRecordingLookup(result ?? publicAddresses);
|
|
335
|
+
const recorder = createRecordingPinner();
|
|
336
|
+
let pinnerRequested = 0;
|
|
337
|
+
const { openPublicFetch } = require_public_fetch.createPublicFetch({
|
|
338
|
+
pinner: () => {
|
|
339
|
+
pinnerRequested++;
|
|
340
|
+
return Promise.resolve(recorder.pinner);
|
|
341
|
+
},
|
|
342
|
+
lookup: failingLookup
|
|
343
|
+
});
|
|
344
|
+
await withMockedGlobalFetch(async () => {
|
|
345
|
+
require_esm.esm_default.catch(500);
|
|
346
|
+
await (0, node_assert.rejects)(() => openPublicFetch(url, { lookup }), (error) => {
|
|
347
|
+
(0, node_assert.ok)(error instanceof require_url.UrlError, String(error));
|
|
348
|
+
(0, node_assert.deepStrictEqual)(error.reason, reason);
|
|
349
|
+
return true;
|
|
350
|
+
});
|
|
351
|
+
(0, node_assert.deepStrictEqual)(require_esm.esm_default.callHistory.calls(), []);
|
|
352
|
+
});
|
|
353
|
+
(0, node_assert.deepStrictEqual)(pinnerRequested, 0);
|
|
354
|
+
(0, node_assert.deepStrictEqual)(recorder.calls, []);
|
|
355
|
+
(0, node_assert.deepStrictEqual)(recorder.released, []);
|
|
356
|
+
});
|
|
357
|
+
});
|
|
358
|
+
(0, _fedify_fixture.test)("openPublicFetch() resists DNS rebinding", async () => {
|
|
359
|
+
const { lookup, hostnames } = createRecordingLookup([{
|
|
360
|
+
address: "8.8.8.8",
|
|
361
|
+
family: 4
|
|
362
|
+
}], [{
|
|
363
|
+
address: "127.0.0.1",
|
|
364
|
+
family: 4
|
|
365
|
+
}]);
|
|
366
|
+
const recorder = createRecordingPinner();
|
|
367
|
+
const { openPublicFetch, withPublicFetch } = require_public_fetch.createPublicFetch({
|
|
368
|
+
pinner: () => Promise.resolve(recorder.pinner),
|
|
369
|
+
lookup: failingLookup
|
|
370
|
+
});
|
|
371
|
+
const session = await openPublicFetch("https://rebinding.invalid/", { lookup });
|
|
372
|
+
try {
|
|
373
|
+
for (const input of [
|
|
374
|
+
"https://rebinding.invalid/a",
|
|
375
|
+
new URL("https://rebinding.invalid/b"),
|
|
376
|
+
new Request("https://rebinding.invalid/c", { method: "POST" }),
|
|
377
|
+
"https://rebinding.invalid/a"
|
|
378
|
+
]) await (await session.fetch(input)).body?.cancel();
|
|
379
|
+
} finally {
|
|
380
|
+
await session.close();
|
|
381
|
+
}
|
|
382
|
+
(0, node_assert.deepStrictEqual)(hostnames, ["rebinding.invalid"]);
|
|
383
|
+
(0, node_assert.deepStrictEqual)(recorder.calls.map(({ url, method, addresses }) => [
|
|
384
|
+
url,
|
|
385
|
+
method,
|
|
386
|
+
addresses
|
|
387
|
+
]), [
|
|
388
|
+
[
|
|
389
|
+
"https://rebinding.invalid/a",
|
|
390
|
+
"GET",
|
|
391
|
+
[{
|
|
392
|
+
address: "8.8.8.8",
|
|
393
|
+
family: 4
|
|
394
|
+
}]
|
|
395
|
+
],
|
|
396
|
+
[
|
|
397
|
+
"https://rebinding.invalid/b",
|
|
398
|
+
"GET",
|
|
399
|
+
[{
|
|
400
|
+
address: "8.8.8.8",
|
|
401
|
+
family: 4
|
|
402
|
+
}]
|
|
403
|
+
],
|
|
404
|
+
[
|
|
405
|
+
"https://rebinding.invalid/c",
|
|
406
|
+
"POST",
|
|
407
|
+
[{
|
|
408
|
+
address: "8.8.8.8",
|
|
409
|
+
family: 4
|
|
410
|
+
}]
|
|
411
|
+
],
|
|
412
|
+
[
|
|
413
|
+
"https://rebinding.invalid/a",
|
|
414
|
+
"GET",
|
|
415
|
+
[{
|
|
416
|
+
address: "8.8.8.8",
|
|
417
|
+
family: 4
|
|
418
|
+
}]
|
|
419
|
+
]
|
|
420
|
+
]);
|
|
421
|
+
(0, node_assert.deepStrictEqual)(new Set(recorder.grants).size, 1);
|
|
422
|
+
await (0, node_assert.rejects)(() => withPublicFetch("https://rebinding.invalid/", () => Promise.reject(/* @__PURE__ */ new Error("must not be called")), { lookup }), require_url.UrlError);
|
|
423
|
+
(0, node_assert.deepStrictEqual)(hostnames, ["rebinding.invalid", "rebinding.invalid"]);
|
|
424
|
+
(0, node_assert.deepStrictEqual)(recorder.calls.length, 4);
|
|
425
|
+
});
|
|
426
|
+
(0, _fedify_fixture.test)("PublicFetchSession.fetch() rejects other origins", async () => {
|
|
427
|
+
const { lookup } = createRecordingLookup(publicAddresses);
|
|
428
|
+
const recorder = createRecordingPinner();
|
|
429
|
+
const { openPublicFetch } = require_public_fetch.createPublicFetch({
|
|
430
|
+
pinner: () => Promise.resolve(recorder.pinner),
|
|
431
|
+
lookup: failingLookup
|
|
432
|
+
});
|
|
433
|
+
const session = await openPublicFetch("https://pinned-host.invalid/a", { lookup });
|
|
434
|
+
try {
|
|
435
|
+
for (const input of [
|
|
436
|
+
"http://pinned-host.invalid/a",
|
|
437
|
+
"https://pinned-host.invalid:8443/a",
|
|
438
|
+
"https://other-host.invalid/a",
|
|
439
|
+
"https://8.8.8.8/a",
|
|
440
|
+
new URL("https://other-host.invalid/a"),
|
|
441
|
+
new Request("https://other-host.invalid/a")
|
|
442
|
+
]) await (0, node_assert.rejects)(() => session.fetch(input), TypeError, String(input));
|
|
443
|
+
(0, node_assert.deepStrictEqual)(recorder.calls, []);
|
|
444
|
+
await (await session.fetch("https://pinned-host.invalid/b?c")).body?.cancel();
|
|
445
|
+
(0, node_assert.deepStrictEqual)(recorder.calls.map(({ url }) => url), ["https://pinned-host.invalid/b?c"]);
|
|
446
|
+
} finally {
|
|
447
|
+
await session.close();
|
|
448
|
+
}
|
|
449
|
+
});
|
|
450
|
+
(0, _fedify_fixture.test)("PublicFetchSession.fetch() never follows redirects", async (t) => {
|
|
451
|
+
await t.step("pinned", async () => {
|
|
452
|
+
const { lookup } = createRecordingLookup(publicAddresses);
|
|
453
|
+
const recorder = createRecordingPinner(() => new Response(null, {
|
|
454
|
+
status: 302,
|
|
455
|
+
headers: { Location: "https://pinned-host.invalid/target" }
|
|
456
|
+
}));
|
|
457
|
+
const { openPublicFetch } = require_public_fetch.createPublicFetch({
|
|
458
|
+
pinner: () => Promise.resolve(recorder.pinner),
|
|
459
|
+
lookup: failingLookup
|
|
460
|
+
});
|
|
461
|
+
const session = await openPublicFetch("https://pinned-host.invalid/", { lookup });
|
|
462
|
+
try {
|
|
463
|
+
const url = "https://pinned-host.invalid/";
|
|
464
|
+
for (const [input, init] of [
|
|
465
|
+
[url, void 0],
|
|
466
|
+
[url, { redirect: "follow" }],
|
|
467
|
+
[url, { redirect: "error" }],
|
|
468
|
+
[new Request(url), void 0],
|
|
469
|
+
[new Request(url, { redirect: "follow" }), void 0],
|
|
470
|
+
[new Request(url), { redirect: "follow" }]
|
|
471
|
+
]) (0, node_assert.deepStrictEqual)((await session.fetch(input, init)).status, 302);
|
|
472
|
+
} finally {
|
|
473
|
+
await session.close();
|
|
474
|
+
}
|
|
475
|
+
(0, node_assert.deepStrictEqual)(recorder.calls.map(({ redirect }) => redirect), Array(6).fill("manual"));
|
|
476
|
+
});
|
|
477
|
+
await t.step("unpinned", async () => {
|
|
478
|
+
const { lookup } = createRecordingLookup(publicAddresses);
|
|
479
|
+
const { openPublicFetch } = require_public_fetch.createPublicFetch({
|
|
480
|
+
pinner: () => Promise.resolve(null),
|
|
481
|
+
lookup: failingLookup
|
|
482
|
+
});
|
|
483
|
+
await withMockedGlobalFetch(async () => {
|
|
484
|
+
require_esm.esm_default.route("https://unpinned-host.invalid/", 200);
|
|
485
|
+
const session = await openPublicFetch("https://unpinned-host.invalid/", { lookup });
|
|
486
|
+
try {
|
|
487
|
+
await session.fetch("https://unpinned-host.invalid/", { redirect: "follow" });
|
|
488
|
+
await session.fetch(new Request("https://unpinned-host.invalid/", { redirect: "follow" }));
|
|
489
|
+
} finally {
|
|
490
|
+
await session.close();
|
|
491
|
+
}
|
|
492
|
+
const calls = require_esm.esm_default.callHistory.calls();
|
|
493
|
+
(0, node_assert.deepStrictEqual)(calls.length, 2);
|
|
494
|
+
for (const call of calls) (0, node_assert.deepStrictEqual)(call.args[1].redirect, "manual");
|
|
495
|
+
});
|
|
496
|
+
});
|
|
497
|
+
await t.step("real server", async () => {
|
|
498
|
+
let hits = 0;
|
|
499
|
+
const server = (0, node_http.createServer)((_, res) => {
|
|
500
|
+
hits++;
|
|
501
|
+
res.writeHead(302, { Location: "/target" });
|
|
502
|
+
res.end("redirect");
|
|
503
|
+
});
|
|
504
|
+
const port = await listen(server);
|
|
505
|
+
try {
|
|
506
|
+
const url = `http://127.0.0.1:${port}/`;
|
|
507
|
+
const session = await require_public_fetch.openDirectFetch(url);
|
|
508
|
+
try {
|
|
509
|
+
const response = await session.fetch(url, { redirect: "follow" });
|
|
510
|
+
(0, node_assert.deepStrictEqual)(response.status, 302);
|
|
511
|
+
(0, node_assert.deepStrictEqual)(response.headers.get("Location"), "/target");
|
|
512
|
+
await response.body?.cancel();
|
|
513
|
+
} finally {
|
|
514
|
+
await session.close();
|
|
515
|
+
}
|
|
516
|
+
(0, node_assert.deepStrictEqual)(hits, 1);
|
|
517
|
+
} finally {
|
|
518
|
+
await closeServer(server);
|
|
519
|
+
}
|
|
520
|
+
});
|
|
521
|
+
});
|
|
522
|
+
(0, _fedify_fixture.test)("PublicFetchSession.close()", async () => {
|
|
523
|
+
const bodies = [
|
|
524
|
+
createTrackedBody(),
|
|
525
|
+
createTrackedBody(),
|
|
526
|
+
createTrackedBody()
|
|
527
|
+
];
|
|
528
|
+
const { lookup } = createRecordingLookup(publicAddresses);
|
|
529
|
+
const recorder = createRecordingPinner((_, index) => index < bodies.length ? new Response(bodies[index].stream) : new Response(null, { status: 204 }));
|
|
530
|
+
const { openPublicFetch } = require_public_fetch.createPublicFetch({
|
|
531
|
+
pinner: () => Promise.resolve(recorder.pinner),
|
|
532
|
+
lookup: failingLookup
|
|
533
|
+
});
|
|
534
|
+
const url = "https://pinned-host.invalid/";
|
|
535
|
+
const session = await openPublicFetch(url, { lookup });
|
|
536
|
+
(0, node_assert.deepStrictEqual)(await (await session.fetch(url)).text(), "body");
|
|
537
|
+
const unconsumed = await session.fetch(url);
|
|
538
|
+
const reader = (await session.fetch(url)).body.getReader();
|
|
539
|
+
(0, node_assert.deepStrictEqual)((await session.fetch(url)).body, null);
|
|
540
|
+
await session.close();
|
|
541
|
+
(0, node_assert.deepStrictEqual)(bodies.map(({ state }) => state.cancelled), [
|
|
542
|
+
0,
|
|
543
|
+
1,
|
|
544
|
+
0
|
|
545
|
+
]);
|
|
546
|
+
(0, node_assert.ok)(unconsumed.bodyUsed);
|
|
547
|
+
(0, node_assert.deepStrictEqual)(recorder.released, [recorder.grants[0]]);
|
|
548
|
+
(0, node_assert.throws)(() => require_pin_grant.redeemPinGrant(recorder.grants[0], new URL(url)), TypeError);
|
|
549
|
+
await (0, node_assert.rejects)(() => session.fetch(url), TypeError);
|
|
550
|
+
(0, node_assert.deepStrictEqual)(recorder.calls.length, 4);
|
|
551
|
+
await session.close();
|
|
552
|
+
(0, node_assert.deepStrictEqual)(recorder.released.length, 1);
|
|
553
|
+
(0, node_assert.deepStrictEqual)(bodies.map(({ state }) => state.cancelled), [
|
|
554
|
+
0,
|
|
555
|
+
1,
|
|
556
|
+
0
|
|
557
|
+
]);
|
|
558
|
+
await reader.cancel();
|
|
559
|
+
});
|
|
560
|
+
(0, _fedify_fixture.test)("PublicFetchSession.close() logs cancel failures without throwing", async (t) => {
|
|
561
|
+
const url = "https://pinned-host.invalid/";
|
|
562
|
+
function setUp(cancelErrors) {
|
|
563
|
+
const bodies = cancelErrors.map((cancelError) => createTrackedBody({ cancelError }));
|
|
564
|
+
const { lookup } = createRecordingLookup(publicAddresses);
|
|
565
|
+
const recorder = createRecordingPinner((_, index) => new Response(bodies[index].stream));
|
|
566
|
+
const { openPublicFetch } = require_public_fetch.createPublicFetch({
|
|
567
|
+
pinner: () => Promise.resolve(recorder.pinner),
|
|
568
|
+
lookup: failingLookup
|
|
569
|
+
});
|
|
570
|
+
return {
|
|
571
|
+
bodies,
|
|
572
|
+
recorder,
|
|
573
|
+
lookup,
|
|
574
|
+
openPublicFetch
|
|
575
|
+
};
|
|
576
|
+
}
|
|
577
|
+
async function assertClosed(session, recorder) {
|
|
578
|
+
(0, node_assert.deepStrictEqual)(recorder.released.length, 1);
|
|
579
|
+
for (const grant of recorder.grants) (0, node_assert.strictEqual)(grant, recorder.released[0]);
|
|
580
|
+
(0, node_assert.throws)(() => require_pin_grant.redeemPinGrant(recorder.released[0], new URL(url)), TypeError);
|
|
581
|
+
await (0, node_assert.rejects)(() => session.fetch(url), TypeError);
|
|
582
|
+
await session.close();
|
|
583
|
+
(0, node_assert.deepStrictEqual)(recorder.released.length, 1);
|
|
584
|
+
}
|
|
585
|
+
await t.step("two failures", async () => {
|
|
586
|
+
const errors = [/* @__PURE__ */ new Error("cancel 1"), /* @__PURE__ */ new Error("cancel 2")];
|
|
587
|
+
const { bodies, recorder, lookup, openPublicFetch } = setUp(errors);
|
|
588
|
+
const { records } = await captureLogs(async () => {
|
|
589
|
+
const session = await openPublicFetch(url, { lookup });
|
|
590
|
+
await session.fetch(url);
|
|
591
|
+
await session.fetch(url);
|
|
592
|
+
await session.close();
|
|
593
|
+
(0, node_assert.deepStrictEqual)(bodies.map(({ state }) => state.cancelled), [1, 1]);
|
|
594
|
+
await assertClosed(session, recorder);
|
|
595
|
+
await settleCancellations();
|
|
596
|
+
});
|
|
597
|
+
(0, node_assert.deepStrictEqual)(cancelFailureLogs(records), [[
|
|
598
|
+
"debug",
|
|
599
|
+
url,
|
|
600
|
+
errors[0]
|
|
601
|
+
], [
|
|
602
|
+
"debug",
|
|
603
|
+
url,
|
|
604
|
+
errors[1]
|
|
605
|
+
]]);
|
|
606
|
+
});
|
|
607
|
+
await t.step("one failure", async () => {
|
|
608
|
+
const error = /* @__PURE__ */ new Error("cancel");
|
|
609
|
+
const { bodies, recorder, lookup, openPublicFetch } = setUp([void 0, error]);
|
|
610
|
+
const { records } = await captureLogs(async () => {
|
|
611
|
+
const session = await openPublicFetch(url, { lookup });
|
|
612
|
+
await session.fetch(url);
|
|
613
|
+
await session.fetch(url);
|
|
614
|
+
await session.close();
|
|
615
|
+
(0, node_assert.deepStrictEqual)(bodies.map(({ state }) => state.cancelled), [1, 1]);
|
|
616
|
+
await assertClosed(session, recorder);
|
|
617
|
+
await settleCancellations();
|
|
618
|
+
});
|
|
619
|
+
(0, node_assert.deepStrictEqual)(cancelFailureLogs(records), [[
|
|
620
|
+
"debug",
|
|
621
|
+
url,
|
|
622
|
+
error
|
|
623
|
+
]]);
|
|
624
|
+
});
|
|
625
|
+
await t.step("failure already seen by the caller", async () => {
|
|
626
|
+
const error = /* @__PURE__ */ new Error("cancel");
|
|
627
|
+
const { bodies, recorder, lookup, openPublicFetch } = setUp([error]);
|
|
628
|
+
const { records } = await captureLogs(async () => {
|
|
629
|
+
const session = await openPublicFetch(url, { lookup });
|
|
630
|
+
const response = await session.fetch(url);
|
|
631
|
+
await (0, node_assert.rejects)(() => response.body.cancel(), (e) => e === error);
|
|
632
|
+
(0, node_assert.ok)(response.bodyUsed);
|
|
633
|
+
await session.close();
|
|
634
|
+
(0, node_assert.deepStrictEqual)(bodies[0].state.cancelled, 1);
|
|
635
|
+
await assertClosed(session, recorder);
|
|
636
|
+
await settleCancellations();
|
|
637
|
+
});
|
|
638
|
+
(0, node_assert.deepStrictEqual)(cancelFailureLogs(records), []);
|
|
639
|
+
});
|
|
640
|
+
await t.step("release failure", async () => {
|
|
641
|
+
const releaseError = /* @__PURE__ */ new Error("release");
|
|
642
|
+
const { recorder, lookup, openPublicFetch } = setUp([]);
|
|
643
|
+
recorder.failRelease(releaseError);
|
|
644
|
+
const session = await openPublicFetch(url, { lookup });
|
|
645
|
+
await (0, node_assert.rejects)(() => session.close(), (e) => e === releaseError);
|
|
646
|
+
await assertClosed(session, recorder);
|
|
647
|
+
});
|
|
648
|
+
await t.step("cancel and release failures", async () => {
|
|
649
|
+
const cancelError = /* @__PURE__ */ new Error("cancel");
|
|
650
|
+
const releaseError = /* @__PURE__ */ new Error("release");
|
|
651
|
+
const { bodies, recorder, lookup, openPublicFetch } = setUp([cancelError]);
|
|
652
|
+
recorder.failRelease(releaseError);
|
|
653
|
+
const { records } = await captureLogs(async () => {
|
|
654
|
+
const session = await openPublicFetch(url, { lookup });
|
|
655
|
+
await session.fetch(url);
|
|
656
|
+
await (0, node_assert.rejects)(() => session.close(), (e) => e === releaseError);
|
|
657
|
+
(0, node_assert.deepStrictEqual)(bodies[0].state.cancelled, 1);
|
|
658
|
+
await assertClosed(session, recorder);
|
|
659
|
+
await settleCancellations();
|
|
660
|
+
});
|
|
661
|
+
(0, node_assert.deepStrictEqual)(cancelFailureLogs(records), [[
|
|
662
|
+
"debug",
|
|
663
|
+
url,
|
|
664
|
+
cancelError
|
|
665
|
+
]]);
|
|
666
|
+
});
|
|
667
|
+
});
|
|
668
|
+
(0, _fedify_fixture.test)("PublicFetchSession.close() does not wait for body cancellation", async (t) => {
|
|
669
|
+
const url = "https://pinned-host.invalid/";
|
|
670
|
+
async function assertCloseSettles(respond) {
|
|
671
|
+
const { lookup } = createRecordingLookup(publicAddresses);
|
|
672
|
+
const recorder = createRecordingPinner(respond);
|
|
673
|
+
const { openPublicFetch } = require_public_fetch.createPublicFetch({
|
|
674
|
+
pinner: () => Promise.resolve(recorder.pinner),
|
|
675
|
+
lookup: failingLookup
|
|
676
|
+
});
|
|
677
|
+
const session = await openPublicFetch(url, { lookup });
|
|
678
|
+
const response = await session.fetch(url);
|
|
679
|
+
(0, node_assert.ok)(!response.bodyUsed);
|
|
680
|
+
await settlesWithin(session.close());
|
|
681
|
+
(0, node_assert.ok)(response.bodyUsed);
|
|
682
|
+
(0, node_assert.deepStrictEqual)(recorder.released.length, 1);
|
|
683
|
+
(0, node_assert.throws)(() => require_pin_grant.redeemPinGrant(recorder.grants[0], new URL(url)), TypeError);
|
|
684
|
+
return recorder.calls.length;
|
|
685
|
+
}
|
|
686
|
+
await t.step("never-settling cancel", async () => {
|
|
687
|
+
const cancelled = [];
|
|
688
|
+
(0, node_assert.deepStrictEqual)(await assertCloseSettles(() => new Response(new ReadableStream({ cancel(reason) {
|
|
689
|
+
cancelled.push(reason);
|
|
690
|
+
return new Promise(() => {});
|
|
691
|
+
} }))), 1);
|
|
692
|
+
(0, node_assert.deepStrictEqual)(cancelled.length, 1);
|
|
693
|
+
});
|
|
694
|
+
await t.step("tee branch", async () => {
|
|
695
|
+
const originals = [];
|
|
696
|
+
(0, node_assert.deepStrictEqual)(await assertCloseSettles(() => {
|
|
697
|
+
const original = new Response(createTrackedBody().stream);
|
|
698
|
+
originals.push(original);
|
|
699
|
+
return original.clone();
|
|
700
|
+
}), 1);
|
|
701
|
+
(0, node_assert.ok)(!originals[0].bodyUsed);
|
|
702
|
+
await originals[0].body.cancel();
|
|
703
|
+
});
|
|
704
|
+
await t.step("fetch-mock responses", async () => {
|
|
705
|
+
const { lookup } = createRecordingLookup(publicAddresses);
|
|
706
|
+
const { openPublicFetch } = require_public_fetch.createPublicFetch({
|
|
707
|
+
pinner: () => Promise.resolve(null),
|
|
708
|
+
lookup: failingLookup
|
|
709
|
+
});
|
|
710
|
+
await withMockedGlobalFetch(async () => {
|
|
711
|
+
require_esm.esm_default.route("https://unpinned-host.invalid/string", new Response("string"));
|
|
712
|
+
require_esm.esm_default.route("https://unpinned-host.invalid/stream", () => new Response(createTrackedBody().stream));
|
|
713
|
+
require_esm.esm_default.route("https://unpinned-host.invalid/redirect", () => new Response(createTrackedBody().stream, {
|
|
714
|
+
status: 302,
|
|
715
|
+
headers: { Location: "https://unpinned-host.invalid/string" }
|
|
716
|
+
}));
|
|
717
|
+
for (const path of [
|
|
718
|
+
"string",
|
|
719
|
+
"stream",
|
|
720
|
+
"redirect"
|
|
721
|
+
]) {
|
|
722
|
+
const target = `https://unpinned-host.invalid/${path}`;
|
|
723
|
+
const session = await openPublicFetch(target, { lookup });
|
|
724
|
+
const response = await session.fetch(target);
|
|
725
|
+
(0, node_assert.ok)(!response.bodyUsed);
|
|
726
|
+
await settlesWithin(session.close());
|
|
727
|
+
(0, node_assert.ok)(response.bodyUsed);
|
|
728
|
+
}
|
|
729
|
+
(0, node_assert.deepStrictEqual)(require_esm.esm_default.callHistory.calls().length, 3);
|
|
730
|
+
});
|
|
731
|
+
});
|
|
732
|
+
});
|
|
733
|
+
(0, _fedify_fixture.test)("PublicFetchSession.fetch() cancels responses arriving after close()", async (t) => {
|
|
734
|
+
const url = "https://pinned-host.invalid/";
|
|
735
|
+
async function assertCancelledAfterClose(response) {
|
|
736
|
+
let respond;
|
|
737
|
+
const { lookup } = createRecordingLookup(publicAddresses);
|
|
738
|
+
const recorder = createRecordingPinner(() => new Promise((resolve) => respond = resolve));
|
|
739
|
+
const { openPublicFetch } = require_public_fetch.createPublicFetch({
|
|
740
|
+
pinner: () => Promise.resolve(recorder.pinner),
|
|
741
|
+
lookup: failingLookup
|
|
742
|
+
});
|
|
743
|
+
const session = await openPublicFetch(url, { lookup });
|
|
744
|
+
const pending = session.fetch(url);
|
|
745
|
+
while (recorder.calls.length < 1) await Promise.resolve();
|
|
746
|
+
await session.close();
|
|
747
|
+
respond(response);
|
|
748
|
+
await settlesWithin((0, node_assert.rejects)(() => pending, TypeError));
|
|
749
|
+
(0, node_assert.ok)(response.bodyUsed);
|
|
750
|
+
(0, node_assert.deepStrictEqual)(recorder.released.length, 1);
|
|
751
|
+
}
|
|
752
|
+
await t.step("cancellable body", async () => {
|
|
753
|
+
const body = createTrackedBody();
|
|
754
|
+
await assertCancelledAfterClose(new Response(body.stream));
|
|
755
|
+
(0, node_assert.deepStrictEqual)(body.state.cancelled, 1);
|
|
756
|
+
});
|
|
757
|
+
await t.step("never-settling cancel", async () => {
|
|
758
|
+
let cancelled = 0;
|
|
759
|
+
await assertCancelledAfterClose(new Response(new ReadableStream({ cancel() {
|
|
760
|
+
cancelled++;
|
|
761
|
+
return new Promise(() => {});
|
|
762
|
+
} })));
|
|
763
|
+
(0, node_assert.deepStrictEqual)(cancelled, 1);
|
|
764
|
+
});
|
|
765
|
+
await t.step("failing cancel", async () => {
|
|
766
|
+
const error = /* @__PURE__ */ new Error("cancel");
|
|
767
|
+
const body = createTrackedBody({ cancelError: error });
|
|
768
|
+
const { records } = await captureLogs(async () => {
|
|
769
|
+
await assertCancelledAfterClose(new Response(body.stream));
|
|
770
|
+
await settleCancellations();
|
|
771
|
+
});
|
|
772
|
+
(0, node_assert.deepStrictEqual)(body.state.cancelled, 1);
|
|
773
|
+
(0, node_assert.deepStrictEqual)(cancelFailureLogs(records), [[
|
|
774
|
+
"debug",
|
|
775
|
+
url,
|
|
776
|
+
error
|
|
777
|
+
]]);
|
|
778
|
+
});
|
|
779
|
+
});
|
|
780
|
+
(0, _fedify_fixture.test)("withPublicFetch()", async (t) => {
|
|
781
|
+
const url = "https://pinned-host.invalid/";
|
|
782
|
+
function setUp() {
|
|
783
|
+
const bodies = [];
|
|
784
|
+
const { lookup } = createRecordingLookup(publicAddresses);
|
|
785
|
+
const recorder = createRecordingPinner(() => {
|
|
786
|
+
const body = createTrackedBody();
|
|
787
|
+
bodies.push(body);
|
|
788
|
+
return new Response(body.stream);
|
|
789
|
+
});
|
|
790
|
+
const { withPublicFetch } = require_public_fetch.createPublicFetch({
|
|
791
|
+
pinner: () => Promise.resolve(recorder.pinner),
|
|
792
|
+
lookup: failingLookup
|
|
793
|
+
});
|
|
794
|
+
return {
|
|
795
|
+
bodies,
|
|
796
|
+
recorder,
|
|
797
|
+
lookup,
|
|
798
|
+
withPublicFetch
|
|
799
|
+
};
|
|
800
|
+
}
|
|
801
|
+
await t.step("returns the result", async () => {
|
|
802
|
+
const { bodies, recorder, lookup, withPublicFetch } = setUp();
|
|
803
|
+
(0, node_assert.deepStrictEqual)(await withPublicFetch(url, async (fetch) => await (await fetch(url)).text(), { lookup }), "body");
|
|
804
|
+
(0, node_assert.deepStrictEqual)(bodies.map(({ state }) => state.cancelled), [0]);
|
|
805
|
+
(0, node_assert.deepStrictEqual)(recorder.released.length, 1);
|
|
806
|
+
});
|
|
807
|
+
await t.step("closes the session when the callback throws", async () => {
|
|
808
|
+
const { bodies, recorder, lookup, withPublicFetch } = setUp();
|
|
809
|
+
const error = /* @__PURE__ */ new Error("callback");
|
|
810
|
+
let fetchInScope;
|
|
811
|
+
await (0, node_assert.rejects)(() => withPublicFetch(url, async (fetch) => {
|
|
812
|
+
fetchInScope = fetch;
|
|
813
|
+
await fetch(url);
|
|
814
|
+
throw error;
|
|
815
|
+
}, { lookup }), (e) => e === error);
|
|
816
|
+
(0, node_assert.deepStrictEqual)(bodies.map(({ state }) => state.cancelled), [1]);
|
|
817
|
+
(0, node_assert.deepStrictEqual)(recorder.released.length, 1);
|
|
818
|
+
await (0, node_assert.rejects)(() => fetchInScope(url), TypeError);
|
|
819
|
+
});
|
|
820
|
+
await t.step("rejects returning a Response", async () => {
|
|
821
|
+
const { bodies, recorder, lookup, withPublicFetch } = setUp();
|
|
822
|
+
await (0, node_assert.rejects)(() => withPublicFetch(url, (fetch) => fetch(url), { lookup }), TypeError);
|
|
823
|
+
(0, node_assert.deepStrictEqual)(bodies.map(({ state }) => state.cancelled), [1]);
|
|
824
|
+
(0, node_assert.deepStrictEqual)(recorder.released.length, 1);
|
|
825
|
+
});
|
|
826
|
+
await t.step("does not call the callback for disallowed URLs", async () => {
|
|
827
|
+
const { recorder, withPublicFetch } = setUp();
|
|
828
|
+
let called = false;
|
|
829
|
+
await (0, node_assert.rejects)(() => withPublicFetch(url, () => {
|
|
830
|
+
called = true;
|
|
831
|
+
return Promise.resolve();
|
|
832
|
+
}, { lookup: () => Promise.resolve([{
|
|
833
|
+
address: "10.0.0.1",
|
|
834
|
+
family: 4
|
|
835
|
+
}]) }), require_url.UrlError);
|
|
836
|
+
(0, node_assert.ok)(!called);
|
|
837
|
+
(0, node_assert.deepStrictEqual)(recorder.calls, []);
|
|
838
|
+
(0, node_assert.deepStrictEqual)(recorder.released, []);
|
|
839
|
+
});
|
|
840
|
+
});
|
|
841
|
+
(0, _fedify_fixture.test)("openPublicFetch() honors the abort signal", async () => {
|
|
842
|
+
const recorder = createRecordingPinner();
|
|
843
|
+
const { openPublicFetch } = require_public_fetch.createPublicFetch({
|
|
844
|
+
pinner: () => Promise.resolve(recorder.pinner),
|
|
845
|
+
lookup: failingLookup
|
|
846
|
+
});
|
|
847
|
+
const url = "https://pinned-host.invalid/";
|
|
848
|
+
const aborted = new AbortController();
|
|
849
|
+
aborted.abort(/* @__PURE__ */ new Error("aborted"));
|
|
850
|
+
const before = createRecordingLookup(publicAddresses);
|
|
851
|
+
await (0, node_assert.rejects)(() => openPublicFetch(url, {
|
|
852
|
+
signal: aborted.signal,
|
|
853
|
+
...before
|
|
854
|
+
}), (e) => e === aborted.signal.reason);
|
|
855
|
+
(0, node_assert.deepStrictEqual)(before.hostnames, []);
|
|
856
|
+
const during = new AbortController();
|
|
857
|
+
await (0, node_assert.rejects)(() => openPublicFetch(url, {
|
|
858
|
+
signal: during.signal,
|
|
859
|
+
lookup() {
|
|
860
|
+
during.abort(/* @__PURE__ */ new Error("aborted during lookup"));
|
|
861
|
+
return Promise.resolve(publicAddresses);
|
|
862
|
+
}
|
|
863
|
+
}), (e) => e === during.signal.reason);
|
|
864
|
+
(0, node_assert.deepStrictEqual)(recorder.calls, []);
|
|
865
|
+
(0, node_assert.deepStrictEqual)(recorder.released, []);
|
|
866
|
+
});
|
|
867
|
+
function withNavigatorUserAgent(userAgent, fn) {
|
|
868
|
+
const descriptor = Object.getOwnPropertyDescriptor(navigator, "userAgent");
|
|
869
|
+
Object.defineProperty(navigator, "userAgent", {
|
|
870
|
+
value: userAgent,
|
|
871
|
+
configurable: true
|
|
872
|
+
});
|
|
873
|
+
const restore = () => {
|
|
874
|
+
if (descriptor == null) delete navigator.userAgent;
|
|
875
|
+
else Object.defineProperty(navigator, "userAgent", descriptor);
|
|
876
|
+
};
|
|
877
|
+
return fn().finally(restore);
|
|
878
|
+
}
|
|
879
|
+
function isUnpinnedLog(record) {
|
|
880
|
+
return typeof record.rawMessage === "string" && record.rawMessage.startsWith("Outbound {protocol} connections are not");
|
|
881
|
+
}
|
|
882
|
+
(0, _fedify_fixture.test)("openPublicFetch() uses the global fetch() without a pinner", async () => {
|
|
883
|
+
const { lookup, hostnames } = createRecordingLookup(publicAddresses);
|
|
884
|
+
const unsupported = createRecordingPinner();
|
|
885
|
+
unsupported.pinner.supports = () => false;
|
|
886
|
+
const pinners = [null, unsupported.pinner];
|
|
887
|
+
const { openPublicFetch } = require_public_fetch.createPublicFetch({
|
|
888
|
+
pinner: () => Promise.resolve(pinners[hostnames.length % 2]),
|
|
889
|
+
lookup: failingLookup
|
|
890
|
+
});
|
|
891
|
+
const { records } = await captureLogs(() => withNavigatorUserAgent("Cloudflare-Workers", () => withMockedGlobalFetch(async () => {
|
|
892
|
+
require_esm.esm_default.route("begin:https://unpinned-host.invalid/", "https");
|
|
893
|
+
require_esm.esm_default.route("begin:http://unpinned-host.invalid/", "http");
|
|
894
|
+
for (const protocol of [
|
|
895
|
+
"https",
|
|
896
|
+
"http",
|
|
897
|
+
"https",
|
|
898
|
+
"http",
|
|
899
|
+
"https"
|
|
900
|
+
]) {
|
|
901
|
+
const url = `${protocol}://unpinned-host.invalid/${hostnames.length}`;
|
|
902
|
+
const session = await openPublicFetch(url, { lookup });
|
|
903
|
+
try {
|
|
904
|
+
(0, node_assert.deepStrictEqual)(await (await session.fetch(url)).text(), protocol);
|
|
905
|
+
} finally {
|
|
906
|
+
await session.close();
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
(0, node_assert.deepStrictEqual)(require_esm.esm_default.callHistory.calls().map(({ url }) => url), [
|
|
910
|
+
"https://unpinned-host.invalid/0",
|
|
911
|
+
"http://unpinned-host.invalid/1",
|
|
912
|
+
"https://unpinned-host.invalid/2",
|
|
913
|
+
"http://unpinned-host.invalid/3",
|
|
914
|
+
"https://unpinned-host.invalid/4"
|
|
915
|
+
]);
|
|
916
|
+
await (0, node_assert.rejects)(() => openPublicFetch("https://unpinned-host.invalid/", { lookup: () => Promise.resolve([{
|
|
917
|
+
address: "127.0.0.1",
|
|
918
|
+
family: 4
|
|
919
|
+
}]) }), require_url.UrlError);
|
|
920
|
+
(0, node_assert.deepStrictEqual)(require_esm.esm_default.callHistory.calls().length, 5);
|
|
921
|
+
})));
|
|
922
|
+
(0, node_assert.deepStrictEqual)(unsupported.calls, []);
|
|
923
|
+
(0, node_assert.deepStrictEqual)(unsupported.released, []);
|
|
924
|
+
(0, node_assert.deepStrictEqual)(records.filter(isUnpinnedLog).map((r) => [
|
|
925
|
+
r.level,
|
|
926
|
+
r.properties.runtime,
|
|
927
|
+
r.properties.protocol
|
|
928
|
+
]), [[
|
|
929
|
+
"debug",
|
|
930
|
+
"cfworkers",
|
|
931
|
+
"https:"
|
|
932
|
+
], [
|
|
933
|
+
"debug",
|
|
934
|
+
"cfworkers",
|
|
935
|
+
"http:"
|
|
936
|
+
]]);
|
|
937
|
+
});
|
|
938
|
+
(0, _fedify_fixture.test)("openPublicFetch() uses the global fetch() on Deno", { ignore: !("Deno" in globalThis) }, async () => {
|
|
939
|
+
const { lookup, hostnames } = createRecordingLookup(publicAddresses);
|
|
940
|
+
await withMockedGlobalFetch(async () => {
|
|
941
|
+
require_esm.esm_default.route("https://deno-host.invalid/object", { body: "ok" });
|
|
942
|
+
for (let i = 0; i < 2; i++) {
|
|
943
|
+
const session = await require_public_fetch.openPublicFetch("https://deno-host.invalid/object", { lookup });
|
|
944
|
+
try {
|
|
945
|
+
(0, node_assert.deepStrictEqual)(await (await session.fetch("https://deno-host.invalid/object", { redirect: "follow" })).text(), "ok");
|
|
946
|
+
} finally {
|
|
947
|
+
await session.close();
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
(0, node_assert.deepStrictEqual)(require_esm.esm_default.callHistory.calls().map(({ url, args }) => [url, args[1].redirect]), [["https://deno-host.invalid/object", "manual"], ["https://deno-host.invalid/object", "manual"]]);
|
|
951
|
+
});
|
|
952
|
+
(0, node_assert.deepStrictEqual)(hostnames, ["deno-host.invalid", "deno-host.invalid"]);
|
|
953
|
+
const workerUrl = new URL("./testdata/public-fetch-deno-worker.ts", require("url").pathToFileURL(__filename).href);
|
|
954
|
+
if ((await Deno.permissions.query({
|
|
955
|
+
name: "read",
|
|
956
|
+
path: workerUrl
|
|
957
|
+
})).state !== "granted") return;
|
|
958
|
+
const worker = new Worker(workerUrl, { type: "module" });
|
|
959
|
+
try {
|
|
960
|
+
(0, node_assert.deepStrictEqual)(await settlesWithin(new Promise((resolve, reject) => {
|
|
961
|
+
worker.onmessage = (event) => resolve(event.data);
|
|
962
|
+
worker.onerror = (event) => {
|
|
963
|
+
event.preventDefault();
|
|
964
|
+
reject(new Error(event.message));
|
|
965
|
+
};
|
|
966
|
+
}), 1e4), {
|
|
967
|
+
calls: [
|
|
968
|
+
["https://deno-host.invalid/1", "manual"],
|
|
969
|
+
["https://deno-host.invalid/2", "manual"],
|
|
970
|
+
["http://deno-host.invalid/3", "manual"]
|
|
971
|
+
],
|
|
972
|
+
hostnames: [
|
|
973
|
+
"deno-host.invalid",
|
|
974
|
+
"deno-host.invalid",
|
|
975
|
+
"deno-host.invalid"
|
|
976
|
+
],
|
|
977
|
+
bodies: [
|
|
978
|
+
"ok",
|
|
979
|
+
"ok",
|
|
980
|
+
"ok"
|
|
981
|
+
],
|
|
982
|
+
logs: [[
|
|
983
|
+
"debug",
|
|
984
|
+
"deno",
|
|
985
|
+
"https:"
|
|
986
|
+
], [
|
|
987
|
+
"debug",
|
|
988
|
+
"deno",
|
|
989
|
+
"http:"
|
|
990
|
+
]]
|
|
991
|
+
});
|
|
992
|
+
} finally {
|
|
993
|
+
worker.terminate();
|
|
994
|
+
}
|
|
995
|
+
});
|
|
996
|
+
(0, _fedify_fixture.test)("openPublicFetch() pins connections on Node.js", { ignore: require_pinners.detectRuntime() !== "node" }, async () => {
|
|
997
|
+
const { lookup, hostnames } = createRecordingLookup(publicAddresses);
|
|
998
|
+
await withMockedGlobalFetch(async () => {
|
|
999
|
+
require_esm.esm_default.route("https://node-host.invalid/object", { body: "ok" });
|
|
1000
|
+
const session = await require_public_fetch.openPublicFetch("https://node-host.invalid/object", { lookup });
|
|
1001
|
+
try {
|
|
1002
|
+
(0, node_assert.deepStrictEqual)(await (await session.fetch("https://node-host.invalid/object", { redirect: "follow" })).text(), "ok");
|
|
1003
|
+
} finally {
|
|
1004
|
+
await session.close();
|
|
1005
|
+
}
|
|
1006
|
+
const calls = require_esm.esm_default.callHistory.calls();
|
|
1007
|
+
(0, node_assert.deepStrictEqual)(calls.length, 1);
|
|
1008
|
+
const init = calls[0].args[1];
|
|
1009
|
+
(0, node_assert.deepStrictEqual)(calls[0].url, "https://node-host.invalid/object");
|
|
1010
|
+
(0, node_assert.deepStrictEqual)(init.redirect, "manual");
|
|
1011
|
+
(0, node_assert.ok)(typeof init.dispatcher?.dispatch === "function", String(init.dispatcher));
|
|
1012
|
+
});
|
|
1013
|
+
(0, node_assert.deepStrictEqual)(hostnames, ["node-host.invalid"]);
|
|
1014
|
+
});
|
|
1015
|
+
(0, _fedify_fixture.test)("openPublicFetch() rejects fedify-test.internal", async (t) => {
|
|
1016
|
+
const url = "https://fedify-test.internal/";
|
|
1017
|
+
const cases = [{
|
|
1018
|
+
name: "private address",
|
|
1019
|
+
lookup: () => Promise.resolve([{
|
|
1020
|
+
address: "10.0.0.1",
|
|
1021
|
+
family: 4
|
|
1022
|
+
}]),
|
|
1023
|
+
reason: "disallowed"
|
|
1024
|
+
}, {
|
|
1025
|
+
name: "lookup failure",
|
|
1026
|
+
lookup: () => Promise.reject(Object.assign(/* @__PURE__ */ new Error("getaddrinfo ENOTFOUND"), { code: "ENOTFOUND" })),
|
|
1027
|
+
reason: "dns"
|
|
1028
|
+
}];
|
|
1029
|
+
for (const { name, lookup, reason } of cases) await t.step(name, async () => {
|
|
1030
|
+
const recorder = createRecordingPinner();
|
|
1031
|
+
const recorded = require_public_fetch.createPublicFetch({
|
|
1032
|
+
pinner: () => Promise.resolve(recorder.pinner),
|
|
1033
|
+
lookup: failingLookup
|
|
1034
|
+
});
|
|
1035
|
+
await withMockedGlobalFetch(async () => {
|
|
1036
|
+
require_esm.esm_default.catch(200);
|
|
1037
|
+
for (const open of [require_public_fetch.openPublicFetch, recorded.openPublicFetch]) await (0, node_assert.rejects)(() => open(url, { lookup }), (error) => {
|
|
1038
|
+
(0, node_assert.ok)(error instanceof require_url.UrlError, String(error));
|
|
1039
|
+
(0, node_assert.deepStrictEqual)(error.reason, reason);
|
|
1040
|
+
return true;
|
|
1041
|
+
});
|
|
1042
|
+
(0, node_assert.deepStrictEqual)(require_esm.esm_default.callHistory.calls(), []);
|
|
1043
|
+
});
|
|
1044
|
+
(0, node_assert.deepStrictEqual)(recorder.calls, []);
|
|
1045
|
+
(0, node_assert.deepStrictEqual)(recorder.released, []);
|
|
1046
|
+
});
|
|
1047
|
+
});
|
|
1048
|
+
(0, _fedify_fixture.test)("openDirectFetch() follows the session rules", async () => {
|
|
1049
|
+
let hits = 0;
|
|
1050
|
+
const server = (0, node_http.createServer)((_, res) => {
|
|
1051
|
+
hits++;
|
|
1052
|
+
res.writeHead(200, { "Content-Type": "text/plain" });
|
|
1053
|
+
res.write("partial");
|
|
1054
|
+
});
|
|
1055
|
+
const port = await listen(server);
|
|
1056
|
+
try {
|
|
1057
|
+
const url = `http://127.0.0.1:${port}/`;
|
|
1058
|
+
const session = await require_public_fetch.openDirectFetch(url);
|
|
1059
|
+
(0, node_assert.deepStrictEqual)(session.url.href, url);
|
|
1060
|
+
await (0, node_assert.rejects)(() => session.fetch(`http://127.0.0.1:${port + 1}/`), TypeError);
|
|
1061
|
+
const response = await session.fetch(`${url}path`);
|
|
1062
|
+
(0, node_assert.deepStrictEqual)(response.status, 200);
|
|
1063
|
+
(0, node_assert.ok)(!response.bodyUsed);
|
|
1064
|
+
await session.close();
|
|
1065
|
+
(0, node_assert.ok)(response.bodyUsed);
|
|
1066
|
+
await (0, node_assert.rejects)(() => session.fetch(url), TypeError);
|
|
1067
|
+
await session.close();
|
|
1068
|
+
(0, node_assert.deepStrictEqual)(hits, 1);
|
|
1069
|
+
} finally {
|
|
1070
|
+
await closeServer(server);
|
|
1071
|
+
}
|
|
1072
|
+
});
|
|
1073
|
+
//#endregion
|