@fedify/vocab-runtime 2.0.0-dev.100

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.
@@ -0,0 +1,393 @@
1
+ import fetchMock from "fetch-mock";
2
+ import { deepStrictEqual, ok, rejects } from "node:assert";
3
+ import { test } from "node:test";
4
+ import preloadedContexts from "./contexts.ts";
5
+ import { getDocumentLoader } from "./docloader.ts";
6
+ import { FetchError } from "./request.ts";
7
+ import { UrlError } from "./url.ts";
8
+
9
+ test("new FetchError()", () => {
10
+ const e = new FetchError("https://example.com/", "An error message.");
11
+ deepStrictEqual(e.name, "FetchError");
12
+ deepStrictEqual(e.url, new URL("https://example.com/"));
13
+ deepStrictEqual(e.message, "https://example.com/: An error message.");
14
+
15
+ const e2 = new FetchError(new URL("https://example.org/"));
16
+ deepStrictEqual(e2.url, new URL("https://example.org/"));
17
+ deepStrictEqual(e2.message, "https://example.org/");
18
+ });
19
+
20
+ test("getDocumentLoader()", async (t) => {
21
+ const fetchDocumentLoader = getDocumentLoader();
22
+
23
+ fetchMock.spyGlobal();
24
+
25
+ fetchMock.get("https://example.com/object", {
26
+ body: {
27
+ "@context": "https://www.w3.org/ns/activitystreams",
28
+ id: "https://example.com/object",
29
+ name: "Fetched object",
30
+ type: "Object",
31
+ },
32
+ });
33
+
34
+ await t.test("ok", async () => {
35
+ deepStrictEqual(await fetchDocumentLoader("https://example.com/object"), {
36
+ contextUrl: null,
37
+ documentUrl: "https://example.com/object",
38
+ document: {
39
+ "@context": "https://www.w3.org/ns/activitystreams",
40
+ id: "https://example.com/object",
41
+ name: "Fetched object",
42
+ type: "Object",
43
+ },
44
+ });
45
+ });
46
+
47
+ fetchMock.get("https://example.com/link-ctx", {
48
+ body: {
49
+ id: "https://example.com/link-ctx",
50
+ name: "Fetched object",
51
+ type: "Object",
52
+ },
53
+ headers: {
54
+ "Content-Type": "application/activity+json",
55
+ Link: "<https://www.w3.org/ns/activitystreams>; " +
56
+ 'rel="http://www.w3.org/ns/json-ld#context"; ' +
57
+ 'type="application/ld+json"',
58
+ },
59
+ });
60
+
61
+ fetchMock.get("https://example.com/link-obj", {
62
+ headers: {
63
+ "Content-Type": "text/html; charset=utf-8",
64
+ Link: '<https://example.com/object>; rel="alternate"; ' +
65
+ 'type="application/activity+json"',
66
+ },
67
+ });
68
+
69
+ fetchMock.get("https://example.com/link-obj-relative", {
70
+ headers: {
71
+ "Content-Type": "text/html; charset=utf-8",
72
+ Link: '</object>; rel="alternate"; ' +
73
+ 'type="application/activity+json"',
74
+ },
75
+ });
76
+
77
+ fetchMock.get("https://example.com/obj-w-wrong-link", {
78
+ body: {
79
+ "@context": "https://www.w3.org/ns/activitystreams",
80
+ id: "https://example.com/obj-w-wrong-link",
81
+ name: "Fetched object",
82
+ type: "Object",
83
+ },
84
+ headers: {
85
+ "Content-Type": "text/html; charset=utf-8",
86
+ Link: '<https://example.com/object>; rel="alternate"; ' +
87
+ 'type="application/ld+json; profile="https://www.w3.org/ns/activitystreams""',
88
+ },
89
+ });
90
+
91
+ await t.test("Link header", async () => {
92
+ deepStrictEqual(await fetchDocumentLoader("https://example.com/link-ctx"), {
93
+ contextUrl: "https://www.w3.org/ns/activitystreams",
94
+ documentUrl: "https://example.com/link-ctx",
95
+ document: {
96
+ id: "https://example.com/link-ctx",
97
+ name: "Fetched object",
98
+ type: "Object",
99
+ },
100
+ });
101
+
102
+ deepStrictEqual(await fetchDocumentLoader("https://example.com/link-obj"), {
103
+ contextUrl: null,
104
+ documentUrl: "https://example.com/object",
105
+ document: {
106
+ "@context": "https://www.w3.org/ns/activitystreams",
107
+ id: "https://example.com/object",
108
+ name: "Fetched object",
109
+ type: "Object",
110
+ },
111
+ });
112
+ });
113
+
114
+ await t.test("Link header relative url", async () => {
115
+ deepStrictEqual(await fetchDocumentLoader("https://example.com/link-ctx"), {
116
+ contextUrl: "https://www.w3.org/ns/activitystreams",
117
+ documentUrl: "https://example.com/link-ctx",
118
+ document: {
119
+ id: "https://example.com/link-ctx",
120
+ name: "Fetched object",
121
+ type: "Object",
122
+ },
123
+ });
124
+
125
+ deepStrictEqual(
126
+ await fetchDocumentLoader("https://example.com/link-obj-relative"),
127
+ {
128
+ contextUrl: null,
129
+ documentUrl: "https://example.com/object",
130
+ document: {
131
+ "@context": "https://www.w3.org/ns/activitystreams",
132
+ id: "https://example.com/object",
133
+ name: "Fetched object",
134
+ type: "Object",
135
+ },
136
+ },
137
+ );
138
+ });
139
+
140
+ await t.test("wrong Link header syntax", async () => {
141
+ deepStrictEqual(
142
+ await fetchDocumentLoader("https://example.com/obj-w-wrong-link"),
143
+ {
144
+ contextUrl: null,
145
+ documentUrl: "https://example.com/obj-w-wrong-link",
146
+ document: {
147
+ "@context": "https://www.w3.org/ns/activitystreams",
148
+ id: "https://example.com/obj-w-wrong-link",
149
+ name: "Fetched object",
150
+ type: "Object",
151
+ },
152
+ },
153
+ );
154
+ });
155
+
156
+ fetchMock.get("https://example.com/html-link", {
157
+ body: `<html>
158
+ <head>
159
+ <meta charset=utf-8>
160
+ <link
161
+ rel=alternate
162
+ type='application/activity+json'
163
+ href="https://example.com/object">
164
+ </head>
165
+ </html>`,
166
+ headers: { "Content-Type": "text/html; charset=utf-8" },
167
+ });
168
+
169
+ await t.test("HTML <link>", async () => {
170
+ deepStrictEqual(
171
+ await fetchDocumentLoader("https://example.com/html-link"),
172
+ {
173
+ contextUrl: null,
174
+ documentUrl: "https://example.com/object",
175
+ document: {
176
+ "@context": "https://www.w3.org/ns/activitystreams",
177
+ id: "https://example.com/object",
178
+ name: "Fetched object",
179
+ type: "Object",
180
+ },
181
+ },
182
+ );
183
+ });
184
+
185
+ fetchMock.get("https://example.com/xhtml-link", {
186
+ body: `<html>
187
+ <head>
188
+ <meta charset="utf-8" />
189
+ <link
190
+ rel=alternate
191
+ type="application/activity+json"
192
+ href="https://example.com/object" />
193
+ </head>
194
+ </html>`,
195
+ headers: { "Content-Type": "application/xhtml+xml; charset=utf-8" },
196
+ });
197
+
198
+ await t.test("XHTML <link>", async () => {
199
+ deepStrictEqual(
200
+ await fetchDocumentLoader("https://example.com/xhtml-link"),
201
+ {
202
+ contextUrl: null,
203
+ documentUrl: "https://example.com/object",
204
+ document: {
205
+ "@context": "https://www.w3.org/ns/activitystreams",
206
+ id: "https://example.com/object",
207
+ name: "Fetched object",
208
+ type: "Object",
209
+ },
210
+ },
211
+ );
212
+ });
213
+
214
+ fetchMock.get("https://example.com/html-a", {
215
+ body: `<html>
216
+ <head>
217
+ <meta charset=utf-8>
218
+ </head>
219
+ <body>
220
+ <a
221
+ rel=alternate
222
+ type=application/activity+json
223
+ href=https://example.com/object>test</a>
224
+ </body>
225
+ </html>`,
226
+ headers: { "Content-Type": "text/html; charset=utf-8" },
227
+ });
228
+
229
+ await t.test("HTML <a>", async () => {
230
+ deepStrictEqual(await fetchDocumentLoader("https://example.com/html-a"), {
231
+ contextUrl: null,
232
+ documentUrl: "https://example.com/object",
233
+ document: {
234
+ "@context": "https://www.w3.org/ns/activitystreams",
235
+ id: "https://example.com/object",
236
+ name: "Fetched object",
237
+ type: "Object",
238
+ },
239
+ });
240
+ });
241
+
242
+ fetchMock.get("https://example.com/wrong-content-type", {
243
+ body: {
244
+ "@context": "https://www.w3.org/ns/activitystreams",
245
+ id: "https://example.com/wrong-content-type",
246
+ name: "Fetched object",
247
+ type: "Object",
248
+ },
249
+ headers: { "Content-Type": "text/html; charset=utf-8" },
250
+ });
251
+
252
+ await t.test("Wrong Content-Type", async () => {
253
+ deepStrictEqual(
254
+ await fetchDocumentLoader("https://example.com/wrong-content-type"),
255
+ {
256
+ contextUrl: null,
257
+ documentUrl: "https://example.com/wrong-content-type",
258
+ document: {
259
+ "@context": "https://www.w3.org/ns/activitystreams",
260
+ id: "https://example.com/wrong-content-type",
261
+ name: "Fetched object",
262
+ type: "Object",
263
+ },
264
+ },
265
+ );
266
+ });
267
+
268
+ fetchMock.get("https://example.com/404", { status: 404 });
269
+
270
+ await t.test("not ok", async () => {
271
+ await rejects(
272
+ () => fetchDocumentLoader("https://example.com/404"),
273
+ FetchError,
274
+ "HTTP 404: https://example.com/404",
275
+ );
276
+ });
277
+
278
+ await t.test("preloaded contexts", async () => {
279
+ for (const [url, document] of Object.entries(preloadedContexts)) {
280
+ deepStrictEqual(await fetchDocumentLoader(url), {
281
+ contextUrl: null,
282
+ documentUrl: url,
283
+ document,
284
+ });
285
+ }
286
+ });
287
+
288
+ await t.test("deny non-HTTP/HTTPS", async () => {
289
+ await rejects(
290
+ () => fetchDocumentLoader("ftp://localhost"),
291
+ UrlError,
292
+ );
293
+ });
294
+
295
+ fetchMock.get("https://example.com/localhost-redirect", {
296
+ status: 302,
297
+ headers: { Location: "https://localhost/object" },
298
+ });
299
+
300
+ fetchMock.get("https://example.com/localhost-link", {
301
+ body: `<html>
302
+ <head>
303
+ <meta charset=utf-8>
304
+ <link
305
+ rel=alternate
306
+ type='application/activity+json'
307
+ href="https://localhost/object">
308
+ </head>
309
+ </html>`,
310
+ headers: { "Content-Type": "text/html; charset=utf-8" },
311
+ });
312
+
313
+ fetchMock.get("https://localhost/object", {
314
+ body: {
315
+ "@context": "https://www.w3.org/ns/activitystreams",
316
+ id: "https://localhost/object",
317
+ name: "Fetched object",
318
+ type: "Object",
319
+ },
320
+ });
321
+
322
+ await t.test("allowPrivateAddress: false", async () => {
323
+ await rejects(
324
+ () => fetchDocumentLoader("https://localhost/object"),
325
+ UrlError,
326
+ );
327
+ await rejects(
328
+ () => fetchDocumentLoader("https://example.com/localhost-redirect"),
329
+ UrlError,
330
+ );
331
+ await rejects(
332
+ () => fetchDocumentLoader("https://example.com/localhost-link"),
333
+ UrlError,
334
+ );
335
+ });
336
+
337
+ const fetchDocumentLoader2 = getDocumentLoader({ allowPrivateAddress: true });
338
+
339
+ await t.test("allowPrivateAddress: true", async () => {
340
+ const expected = {
341
+ contextUrl: null,
342
+ documentUrl: "https://localhost/object",
343
+ document: {
344
+ "@context": "https://www.w3.org/ns/activitystreams",
345
+ id: "https://localhost/object",
346
+ name: "Fetched object",
347
+ type: "Object",
348
+ },
349
+ };
350
+ deepStrictEqual(
351
+ await fetchDocumentLoader2("https://localhost/object"),
352
+ expected,
353
+ );
354
+ deepStrictEqual(
355
+ await fetchDocumentLoader2("https://example.com/localhost-redirect"),
356
+ expected,
357
+ );
358
+ deepStrictEqual(
359
+ await fetchDocumentLoader2("https://example.com/localhost-link"),
360
+ expected,
361
+ );
362
+ });
363
+
364
+ // Regression test for ReDoS vulnerability (CVE-2025-68475)
365
+ // Malicious HTML payload: <a a="b" a="b" ... (unclosed tag)
366
+ // With the vulnerable regex, this causes catastrophic backtracking
367
+ const maliciousPayload = "<a" + ' a="b"'.repeat(30) + " ";
368
+
369
+ fetchMock.get("https://example.com/redos", {
370
+ body: maliciousPayload,
371
+ headers: { "Content-Type": "text/html; charset=utf-8" },
372
+ });
373
+
374
+ await t.test("ReDoS resistance (CVE-2025-68475)", async () => {
375
+ const start = performance.now();
376
+ // The malicious HTML will fail JSON parsing, but the important thing is
377
+ // that it should complete quickly (not hang due to ReDoS)
378
+ await rejects(
379
+ () => fetchDocumentLoader("https://example.com/redos"),
380
+ SyntaxError,
381
+ );
382
+ const elapsed = performance.now() - start;
383
+
384
+ // Should complete in under 1 second. With the vulnerable regex,
385
+ // this would take 14+ seconds for 30 repetitions.
386
+ ok(
387
+ elapsed < 1000,
388
+ `Potential ReDoS vulnerability detected: ${elapsed}ms (expected < 1000ms)`,
389
+ );
390
+ });
391
+
392
+ fetchMock.hardReset();
393
+ });