@fedify/vocab-runtime 2.5.0-dev.2448 → 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 +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-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-DP2u2wUX.cjs → body-C8d1PmDa.cjs} +1 -1
- package/dist/tests/{body-oV99TVdA.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-CwxZguqN.cjs → docloader-D1fNCGam.cjs} +111 -39
- package/dist/tests/{docloader-D3mA7aPJ.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-D7ClSjFO.cjs → portable-media-BM9zgj9g.cjs} +3 -3
- package/dist/tests/{portable-media-B-oCeotd.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-B77_oZnk.cjs → request-7q-Zfe1p.cjs} +2 -2
- package/dist/tests/{request-BlLYI4x3.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.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,343 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Helpers shared by the pinner tests in *../\*.test.ts*. They start local
|
|
3
|
+
* servers and proxies, and run child processes.
|
|
4
|
+
* @module
|
|
5
|
+
*/
|
|
6
|
+
import { execFile } from "node:child_process";
|
|
7
|
+
import { existsSync } from "node:fs";
|
|
8
|
+
import {
|
|
9
|
+
createServer,
|
|
10
|
+
type IncomingMessage,
|
|
11
|
+
type Server,
|
|
12
|
+
type ServerResponse,
|
|
13
|
+
} from "node:http";
|
|
14
|
+
import {
|
|
15
|
+
type AddressInfo,
|
|
16
|
+
connect,
|
|
17
|
+
createServer as createTcpServer,
|
|
18
|
+
type Server as TcpServer,
|
|
19
|
+
} from "node:net";
|
|
20
|
+
import { dirname, join } from "node:path";
|
|
21
|
+
import process from "node:process";
|
|
22
|
+
import { fileURLToPath } from "node:url";
|
|
23
|
+
import { type ResolvedAddress, UrlError } from "../../url.ts";
|
|
24
|
+
import { detectRuntime } from "../mod.ts";
|
|
25
|
+
import type { ConnectionPinnerOptions } from "../types.ts";
|
|
26
|
+
|
|
27
|
+
export const isBun: boolean = detectRuntime() === "bun";
|
|
28
|
+
export const isNode: boolean = detectRuntime() === "node";
|
|
29
|
+
|
|
30
|
+
/** Pinner options that allow connecting to any address, e.g., loopback. */
|
|
31
|
+
export const allowAll: ConnectionPinnerOptions = { verifyAddress() {} };
|
|
32
|
+
|
|
33
|
+
export const ipv4 = (address: string): ResolvedAddress => ({
|
|
34
|
+
address,
|
|
35
|
+
family: 4,
|
|
36
|
+
});
|
|
37
|
+
export const ipv6 = (address: string): ResolvedAddress => ({
|
|
38
|
+
address,
|
|
39
|
+
family: 6,
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Whether the error is the rejection of an address by `verifyAddress`.
|
|
44
|
+
* The Bun pinner throws the `UrlError` itself, and the Node.js pinner
|
|
45
|
+
* rejects with the `TypeError` of `fetch()`, caused by the `UrlError` that
|
|
46
|
+
* its lookup failed with.
|
|
47
|
+
*/
|
|
48
|
+
export function isAddressRejection(error: unknown): boolean {
|
|
49
|
+
return error instanceof UrlError ||
|
|
50
|
+
error instanceof TypeError && error.cause instanceof UrlError;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface ReceivedRequest {
|
|
54
|
+
readonly method: string;
|
|
55
|
+
readonly url: string;
|
|
56
|
+
readonly host: string | undefined;
|
|
57
|
+
readonly localAddress: string | undefined;
|
|
58
|
+
readonly body: string;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function readBody(req: IncomingMessage): Promise<string> {
|
|
62
|
+
return new Promise((resolve, reject) => {
|
|
63
|
+
let body = "";
|
|
64
|
+
req.setEncoding("utf8");
|
|
65
|
+
req.on("data", (chunk: string) => body += chunk);
|
|
66
|
+
req.on("end", () => resolve(body));
|
|
67
|
+
req.on("error", reject);
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export async function listen(
|
|
72
|
+
server: Server | TcpServer,
|
|
73
|
+
host = "127.0.0.1",
|
|
74
|
+
): Promise<number> {
|
|
75
|
+
await new Promise<void>((resolve, reject) => {
|
|
76
|
+
server.once("error", reject);
|
|
77
|
+
server.listen(0, host, () => {
|
|
78
|
+
server.off("error", reject);
|
|
79
|
+
resolve();
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
return (server.address() as AddressInfo).port;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export async function closeServer(server: Server | TcpServer): Promise<void> {
|
|
86
|
+
(server as Server).closeAllConnections?.();
|
|
87
|
+
await new Promise<void>((resolve) => server.close(() => resolve()));
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function canConnect(host: string, port: number): Promise<boolean> {
|
|
91
|
+
return new Promise((resolve) => {
|
|
92
|
+
const socket = connect({ host, port });
|
|
93
|
+
const done = (result: boolean) => {
|
|
94
|
+
socket.destroy();
|
|
95
|
+
resolve(result);
|
|
96
|
+
};
|
|
97
|
+
socket.setTimeout(2_000, () => done(false));
|
|
98
|
+
socket.once("connect", () => done(true));
|
|
99
|
+
socket.once("error", () => done(false));
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export interface LoopbackSupport {
|
|
104
|
+
/** Whether `::1` can be connected to. */
|
|
105
|
+
readonly ipv6: boolean;
|
|
106
|
+
/**
|
|
107
|
+
* Whether a server listening on `::` accepts connections to both 127.0.0.1
|
|
108
|
+
* and `::1`.
|
|
109
|
+
*/
|
|
110
|
+
readonly dualStack: boolean;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
let loopbackSupport: Promise<LoopbackSupport> | undefined;
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Checks which loopback addresses the tests can use. Only 127.0.0.1 is
|
|
117
|
+
* assumed: other addresses of 127.0.0.0/8 are not configured on macOS, and
|
|
118
|
+
* IPv6 can be disabled.
|
|
119
|
+
*/
|
|
120
|
+
export function getLoopbackSupport(): Promise<LoopbackSupport> {
|
|
121
|
+
loopbackSupport ??= (async () => {
|
|
122
|
+
const server = createTcpServer((socket) => socket.destroy());
|
|
123
|
+
let port: number;
|
|
124
|
+
try {
|
|
125
|
+
port = await listen(server, "::");
|
|
126
|
+
} catch {
|
|
127
|
+
return { ipv6: false, dualStack: false };
|
|
128
|
+
}
|
|
129
|
+
try {
|
|
130
|
+
const [v4, v6] = await Promise.all([
|
|
131
|
+
canConnect("127.0.0.1", port),
|
|
132
|
+
canConnect("::1", port),
|
|
133
|
+
]);
|
|
134
|
+
return { ipv6: v6, dualStack: v4 && v6 };
|
|
135
|
+
} finally {
|
|
136
|
+
await closeServer(server);
|
|
137
|
+
}
|
|
138
|
+
})();
|
|
139
|
+
return loopbackSupport;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Starts an HTTP server that records the requests it receives.
|
|
144
|
+
*/
|
|
145
|
+
export async function startRecordingServer(
|
|
146
|
+
host = "127.0.0.1",
|
|
147
|
+
respond: (req: IncomingMessage, res: ServerResponse) => void = (_, res) =>
|
|
148
|
+
res.end("pinned"),
|
|
149
|
+
) {
|
|
150
|
+
const requests: ReceivedRequest[] = [];
|
|
151
|
+
const server = createServer(async (req, res) => {
|
|
152
|
+
const body = await readBody(req);
|
|
153
|
+
requests.push({
|
|
154
|
+
method: req.method ?? "",
|
|
155
|
+
url: req.url ?? "",
|
|
156
|
+
host: req.headers.host,
|
|
157
|
+
localAddress: req.socket.localAddress,
|
|
158
|
+
body,
|
|
159
|
+
});
|
|
160
|
+
respond(req, res);
|
|
161
|
+
});
|
|
162
|
+
const port = await listen(server, host);
|
|
163
|
+
return { server, port, requests };
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Starts a TCP server that only counts the connections it accepts, and drops
|
|
168
|
+
* them right away. It stands for an HTTPS server without a certificate.
|
|
169
|
+
*/
|
|
170
|
+
export async function startCountingServer() {
|
|
171
|
+
const counter = { connections: 0 };
|
|
172
|
+
const server = createTcpServer((socket) => {
|
|
173
|
+
counter.connections++;
|
|
174
|
+
socket.destroy();
|
|
175
|
+
});
|
|
176
|
+
const port = await listen(server);
|
|
177
|
+
return { server, port, counter };
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Starts a proxy that records the request targets it is asked for, and never
|
|
182
|
+
* forwards them. It answers a request in absolute form with `via-proxy`,
|
|
183
|
+
* and drops tunnels (`CONNECT`). It parses requests by itself since
|
|
184
|
+
* the `connect` event of `node:http` servers is not available on older Bun
|
|
185
|
+
* versions.
|
|
186
|
+
* @param options.refuseTunnels Whether to refuse tunnels with a `403`
|
|
187
|
+
* response instead of dropping them. Some
|
|
188
|
+
* clients retry dropped tunnels without end
|
|
189
|
+
* (e.g., the `EnvHttpProxyAgent` of Node.js),
|
|
190
|
+
* and some take the response of a refused
|
|
191
|
+
* tunnel for the response of the request
|
|
192
|
+
* (e.g., Bun 1.3.14).
|
|
193
|
+
*/
|
|
194
|
+
export async function startRecordingProxy(
|
|
195
|
+
{ refuseTunnels = false }: { readonly refuseTunnels?: boolean } = {},
|
|
196
|
+
) {
|
|
197
|
+
const targets: string[] = [];
|
|
198
|
+
const server = createTcpServer((socket) => {
|
|
199
|
+
let head = "";
|
|
200
|
+
socket.setEncoding("latin1");
|
|
201
|
+
socket.on("error", () => {});
|
|
202
|
+
socket.on("data", (chunk: string) => {
|
|
203
|
+
if (head.includes("\r\n\r\n")) return;
|
|
204
|
+
head += chunk;
|
|
205
|
+
if (!head.includes("\r\n\r\n")) return;
|
|
206
|
+
const [method, target] = head.slice(0, head.indexOf("\r\n")).split(" ");
|
|
207
|
+
targets.push(`${method} ${target}`);
|
|
208
|
+
if (method !== "CONNECT") {
|
|
209
|
+
socket.end(
|
|
210
|
+
"HTTP/1.1 200 OK\r\nContent-Length: 9\r\nConnection: close\r\n" +
|
|
211
|
+
"\r\nvia-proxy",
|
|
212
|
+
);
|
|
213
|
+
} else if (refuseTunnels) {
|
|
214
|
+
socket.end(
|
|
215
|
+
"HTTP/1.1 403 Forbidden\r\nContent-Length: 0\r\n" +
|
|
216
|
+
"Connection: close\r\n\r\n",
|
|
217
|
+
);
|
|
218
|
+
} else {
|
|
219
|
+
socket.destroy();
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
});
|
|
223
|
+
const port = await listen(server);
|
|
224
|
+
return { server, port, targets };
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Returns the first of the given paths that exists.
|
|
229
|
+
*/
|
|
230
|
+
function findFile(name: string, candidates: readonly string[]): string {
|
|
231
|
+
const found = candidates.find((path) => existsSync(path));
|
|
232
|
+
if (found == null) {
|
|
233
|
+
throw new Error(`Cannot find ${name} in ${candidates.join(", ")}`);
|
|
234
|
+
}
|
|
235
|
+
return found;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function getModuleDirectory(moduleUrl: string | undefined): string | null {
|
|
239
|
+
return typeof moduleUrl === "string" && moduleUrl.startsWith("file:")
|
|
240
|
+
? dirname(fileURLToPath(moduleUrl))
|
|
241
|
+
: null;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const TESTDATA = ["src", "pinners", "testdata"] as const;
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Finds a source file in *testdata/* (e.g., a child script that Bun runs
|
|
248
|
+
* directly). When a test runs from the compiled *dist/tests/pinners/*
|
|
249
|
+
* instead of *src/pinners/*, the test data is not compiled with it, so this
|
|
250
|
+
* falls back to the files in the package: first relative to the test module,
|
|
251
|
+
* then relative to the working directory (the package root, where the Bun
|
|
252
|
+
* test script runs), in case `import.meta.url` is unavailable in a CommonJS
|
|
253
|
+
* build.
|
|
254
|
+
* @param name The file name.
|
|
255
|
+
* @param moduleUrl The `import.meta.url` of the test module.
|
|
256
|
+
*/
|
|
257
|
+
export function resolveTestdata(
|
|
258
|
+
name: string,
|
|
259
|
+
moduleUrl: string | undefined,
|
|
260
|
+
): string {
|
|
261
|
+
const candidates: string[] = [];
|
|
262
|
+
const here = getModuleDirectory(moduleUrl);
|
|
263
|
+
if (here != null) {
|
|
264
|
+
candidates.push(
|
|
265
|
+
join(here, TESTDATA[2], name),
|
|
266
|
+
join(here, "..", "..", "..", ...TESTDATA, name),
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
candidates.push(join(process.cwd(), ...TESTDATA, name));
|
|
270
|
+
return findFile(name, candidates);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Finds a child script of *testdata/* compiled into
|
|
275
|
+
* *dist/tests/pinners/testdata/* by *tsdown.config.ts*, for Node.js, which
|
|
276
|
+
* cannot run TypeScript sources without flags before 22.6.0. Node.js tests
|
|
277
|
+
* run only from *dist/tests/* (`cd dist/tests && node --test`), so this looks
|
|
278
|
+
* relative to the test module, then relative to the working directory, in
|
|
279
|
+
* case `import.meta.url` is unavailable in a CommonJS build.
|
|
280
|
+
* @param name The compiled file name, e.g., `node-child.mjs`.
|
|
281
|
+
* @param moduleUrl The `import.meta.url` of the test module.
|
|
282
|
+
*/
|
|
283
|
+
export function resolveCompiledTestdata(
|
|
284
|
+
name: string,
|
|
285
|
+
moduleUrl: string | undefined,
|
|
286
|
+
): string {
|
|
287
|
+
const candidates: string[] = [];
|
|
288
|
+
const here = getModuleDirectory(moduleUrl);
|
|
289
|
+
if (here != null) candidates.push(join(here, TESTDATA[2], name));
|
|
290
|
+
candidates.push(
|
|
291
|
+
join(process.cwd(), ...TESTDATA.slice(1), name),
|
|
292
|
+
join(process.cwd(), "dist", "tests", ...TESTDATA.slice(1), name),
|
|
293
|
+
);
|
|
294
|
+
return findFile(name, candidates);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const PROXY_ENV_NAMES = [
|
|
298
|
+
"HTTP_PROXY",
|
|
299
|
+
"HTTPS_PROXY",
|
|
300
|
+
"ALL_PROXY",
|
|
301
|
+
"NO_PROXY",
|
|
302
|
+
] as const;
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Runs a child script in a new process of the current runtime, without proxy
|
|
306
|
+
* environment variables (and `NODE_USE_ENV_PROXY`) but with the given ones,
|
|
307
|
+
* and parses the last line of its output as JSON.
|
|
308
|
+
* @param script The path of the script.
|
|
309
|
+
* @param args The arguments to the script.
|
|
310
|
+
* @param extraEnv The environment variables to set.
|
|
311
|
+
*/
|
|
312
|
+
export function runChild<T>(
|
|
313
|
+
script: string,
|
|
314
|
+
args: readonly string[],
|
|
315
|
+
extraEnv: Record<string, string>,
|
|
316
|
+
): Promise<T> {
|
|
317
|
+
const env: Record<string, string | undefined> = { ...process.env };
|
|
318
|
+
for (const name of PROXY_ENV_NAMES) {
|
|
319
|
+
delete env[name];
|
|
320
|
+
delete env[name.toLowerCase()];
|
|
321
|
+
}
|
|
322
|
+
delete env.NODE_USE_ENV_PROXY;
|
|
323
|
+
Object.assign(env, extraEnv);
|
|
324
|
+
return new Promise((resolve, reject) => {
|
|
325
|
+
execFile(
|
|
326
|
+
process.execPath,
|
|
327
|
+
[script, ...args],
|
|
328
|
+
{ env, timeout: 30_000 },
|
|
329
|
+
(error, stdout, stderr) => {
|
|
330
|
+
if (error != null) {
|
|
331
|
+
reject(new Error(`${script} failed: ${stderr}`, { cause: error }));
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
const line = stdout.trim().split("\n").at(-1) ?? "";
|
|
335
|
+
try {
|
|
336
|
+
resolve(JSON.parse(line));
|
|
337
|
+
} catch (cause) {
|
|
338
|
+
reject(new Error(`Unexpected output: ${stdout}${stderr}`, { cause }));
|
|
339
|
+
}
|
|
340
|
+
},
|
|
341
|
+
);
|
|
342
|
+
});
|
|
343
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
-----BEGIN CERTIFICATE-----
|
|
2
|
+
MIIBzTCCAXOgAwIBAgIUfeiNSVLuqF1qsVMEOsR7YW1pmVgwCgYIKoZIzj0EAwIw
|
|
3
|
+
HjEcMBoGA1UEAwwTcGlubmVkLWhvc3QuaW52YWxpZDAgFw0yNjEwMjAxODI2MDVa
|
|
4
|
+
GA8yMTI2MDkyNjE4MjYwNVowHjEcMBoGA1UEAwwTcGlubmVkLWhvc3QuaW52YWxp
|
|
5
|
+
ZDBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABMiOCZmTSR6q86Up9QUH69Lf6o3G
|
|
6
|
+
/Y2T/NY3h3eE349EmITY/3TtcY18bG7dz+vkDw/umptvZ3fuE2uYmhoeKPWjgYww
|
|
7
|
+
gYkwHQYDVR0OBBYEFLHn+Zc6QAPrvKuTJv7E7ogNvqctMB8GA1UdIwQYMBaAFLHn
|
|
8
|
+
+Zc6QAPrvKuTJv7E7ogNvqctMA8GA1UdEwEB/wQFMAMBAf8wNgYDVR0RBC8wLYIT
|
|
9
|
+
cGlubmVkLWhvc3QuaW52YWxpZIIWYWx0ZXJuYXRlLWhvc3QuaW52YWxpZDAKBggq
|
|
10
|
+
hkjOPQQDAgNIADBFAiB4LmqgIz2JxWyls6cZdcLC5pGS9jgKVTPizpvXg/iE6gIh
|
|
11
|
+
AIYLzlFL9KlC5rc8dbkDWbNSEidECoc6UhUaO6QygAGc
|
|
12
|
+
-----END CERTIFICATE-----
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A child process for the certificate verification regression test of the
|
|
3
|
+
* Bun pinner in *../bun.test.ts*. Bun reads `NODE_EXTRA_CA_CERTS` only at
|
|
4
|
+
* startup, so the test runs this script in a new Bun process with it pointing
|
|
5
|
+
* to *tls-cert.pem*, a self-signed certificate for `pinned-host.invalid` and
|
|
6
|
+
* `alternate-host.invalid`.
|
|
7
|
+
*
|
|
8
|
+
* Usage: `bun tls-child.ts <https-port>`
|
|
9
|
+
*
|
|
10
|
+
* It requests `https://<hostname>:<https-port>/<case>` through a pinner that
|
|
11
|
+
* allows loopback addresses, with every hostname pinned to 127.0.0.1, in this
|
|
12
|
+
* order:
|
|
13
|
+
*
|
|
14
|
+
* 1. `pinned`: `pinned-host.invalid`, which the certificate covers.
|
|
15
|
+
* 2. `unlisted`: `unlisted-host.invalid`, which the certificate does not
|
|
16
|
+
* cover.
|
|
17
|
+
* 3. `unlisted-again`: the same as `unlisted`, so that a connection kept
|
|
18
|
+
* after a failed check would be reused.
|
|
19
|
+
* 4. `alternate`: `alternate-host.invalid`, the second name of
|
|
20
|
+
* the certificate.
|
|
21
|
+
* 5. `signals`: `pinned-host.invalid` with signals both in the request and
|
|
22
|
+
* as the separate argument.
|
|
23
|
+
*
|
|
24
|
+
* It prints the results as a JSON line.
|
|
25
|
+
* @module
|
|
26
|
+
*/
|
|
27
|
+
import process from "node:process";
|
|
28
|
+
import { issuePinGrant } from "../../pin-grant.ts";
|
|
29
|
+
import { createBunPinner } from "../bun.ts";
|
|
30
|
+
|
|
31
|
+
type TlsChildResult =
|
|
32
|
+
| { readonly case: string; readonly status: number; readonly body: string }
|
|
33
|
+
| { readonly case: string; readonly error: string };
|
|
34
|
+
|
|
35
|
+
const port = Number(process.argv[2]);
|
|
36
|
+
if (!Number.isInteger(port)) {
|
|
37
|
+
throw new TypeError("Usage: tls-child.ts <https-port>");
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const pinner = createBunPinner({ verifyAddress() {} });
|
|
41
|
+
const loopback = [{ address: "127.0.0.1", family: 4 }] as const;
|
|
42
|
+
|
|
43
|
+
async function attempt(
|
|
44
|
+
name: string,
|
|
45
|
+
hostname: string,
|
|
46
|
+
withSignals = false,
|
|
47
|
+
): Promise<TlsChildResult> {
|
|
48
|
+
const url = new URL(`https://${hostname}:${port}/${name}`);
|
|
49
|
+
const grant = issuePinGrant(url, loopback);
|
|
50
|
+
try {
|
|
51
|
+
const response = withSignals
|
|
52
|
+
? await pinner.fetch(
|
|
53
|
+
grant,
|
|
54
|
+
new Request(url, { signal: new AbortController().signal }),
|
|
55
|
+
new AbortController().signal,
|
|
56
|
+
)
|
|
57
|
+
: await pinner.fetch(grant, new Request(url));
|
|
58
|
+
return { case: name, status: response.status, body: await response.text() };
|
|
59
|
+
} catch (error) {
|
|
60
|
+
const code = (error as { code?: unknown } | null)?.code;
|
|
61
|
+
return {
|
|
62
|
+
case: name,
|
|
63
|
+
error: typeof code === "string" ? code : String(error),
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const results: TlsChildResult[] = [];
|
|
69
|
+
for (
|
|
70
|
+
const [name, hostname] of [
|
|
71
|
+
["pinned", "pinned-host.invalid"],
|
|
72
|
+
["unlisted", "unlisted-host.invalid"],
|
|
73
|
+
["unlisted-again", "unlisted-host.invalid"],
|
|
74
|
+
["alternate", "alternate-host.invalid"],
|
|
75
|
+
] as const
|
|
76
|
+
) {
|
|
77
|
+
results.push(await attempt(name, hostname));
|
|
78
|
+
}
|
|
79
|
+
results.push(await attempt("signals", "pinned-host.invalid", true));
|
|
80
|
+
|
|
81
|
+
const output = JSON.stringify({
|
|
82
|
+
version: (globalThis as { Bun?: { version: string } }).Bun?.version,
|
|
83
|
+
results,
|
|
84
|
+
});
|
|
85
|
+
await new Promise<void>((resolve) =>
|
|
86
|
+
process.stdout.write(`${output}\n`, () => resolve())
|
|
87
|
+
);
|
|
88
|
+
process.exit(0);
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { PinGrant } from "../pin-grant.ts";
|
|
2
|
+
import type { ResolvedAddress } from "../url.ts";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Connects requests only to the addresses a {@link PinGrant} stands for.
|
|
6
|
+
* Pinners are created by factories and never exported from any entry point.
|
|
7
|
+
*/
|
|
8
|
+
export interface ConnectionPinner {
|
|
9
|
+
/**
|
|
10
|
+
* Whether this pinner can pin requests to the given URL. This is decided
|
|
11
|
+
* per protocol.
|
|
12
|
+
*/
|
|
13
|
+
supports(url: URL): boolean;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Sends the request to one of the addresses of the grant. The grant is
|
|
17
|
+
* redeemed for every request, and every address is checked again right
|
|
18
|
+
* before connecting.
|
|
19
|
+
* @param grant The grant for the request's origin.
|
|
20
|
+
* @param request The request to send. Its redirect mode is ignored;
|
|
21
|
+
* redirects are never followed.
|
|
22
|
+
* @param signal An optional signal to abort the request.
|
|
23
|
+
* @returns The response.
|
|
24
|
+
*/
|
|
25
|
+
fetch(
|
|
26
|
+
grant: PinGrant,
|
|
27
|
+
request: Request,
|
|
28
|
+
signal?: AbortSignal,
|
|
29
|
+
): Promise<Response>;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Releases the resources held for the grant. Called once when the session
|
|
33
|
+
* that owns the grant is closed.
|
|
34
|
+
*/
|
|
35
|
+
release(grant: PinGrant): void | Promise<void>;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Options for the pinner factories.
|
|
40
|
+
*/
|
|
41
|
+
export interface ConnectionPinnerOptions {
|
|
42
|
+
/**
|
|
43
|
+
* Checks an address right before connecting to it. The production pinner
|
|
44
|
+
* rejects non-public addresses here.
|
|
45
|
+
* @throws {UrlError} If the address must not be connected to.
|
|
46
|
+
*/
|
|
47
|
+
readonly verifyAddress: (address: ResolvedAddress) => void;
|
|
48
|
+
}
|