@fedify/vocab-runtime 2.4.0-dev.1964 → 2.4.0-dev.1988
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 +1 -1
- package/dist/mod.cjs +176 -152
- package/dist/mod.js +176 -152
- package/dist/tests/body-Cg5W_IdS.cjs +94 -0
- package/dist/tests/body-DRVriaJO.mjs +77 -0
- package/dist/tests/body.test.cjs +89 -0
- package/dist/tests/body.test.d.cts +1 -0
- package/dist/tests/body.test.d.mts +1 -0
- package/dist/tests/body.test.mjs +90 -0
- package/dist/tests/decimal.test.cjs +2 -2
- package/dist/tests/decimal.test.mjs +2 -2
- package/dist/tests/digest.test.cjs +1 -1
- package/dist/tests/digest.test.mjs +1 -1
- package/dist/tests/{docloader-BexVAGvr.mjs → docloader-ZSduXDDg.mjs} +33 -82
- package/dist/tests/{docloader-B3auVdRI.cjs → docloader-j_N6ZEZx.cjs} +33 -82
- package/dist/tests/docloader.test.cjs +84 -5
- package/dist/tests/docloader.test.mjs +84 -5
- package/dist/tests/internal/multicodec.test.cjs +1 -1
- package/dist/tests/internal/multicodec.test.mjs +1 -1
- package/dist/tests/jsonld-cache.test.cjs +1 -1
- package/dist/tests/jsonld-cache.test.mjs +1 -1
- package/dist/tests/key.test.cjs +1 -1
- package/dist/tests/key.test.mjs +1 -1
- package/dist/tests/langstr.test.cjs +1 -1
- package/dist/tests/langstr.test.mjs +1 -1
- package/dist/tests/link.test.cjs +1 -1
- package/dist/tests/link.test.mjs +1 -1
- package/dist/tests/multibase/multibase.test.cjs +1 -1
- package/dist/tests/multibase/multibase.test.mjs +1 -1
- package/dist/tests/{request-CRO9h2AO.cjs → request-B3YkikPO.cjs} +1 -1
- package/dist/tests/{request-C8BUtPj4.mjs → request-Bo1iqGDW.mjs} +1 -1
- package/dist/tests/request.test.cjs +2 -2
- package/dist/tests/request.test.mjs +2 -2
- package/dist/tests/temporal.test.cjs +1 -1
- package/dist/tests/temporal.test.mjs +1 -1
- package/dist/tests/url.test.cjs +1 -1
- package/dist/tests/url.test.mjs +1 -1
- package/package.json +1 -1
- package/src/body.test.ts +125 -0
- package/src/body.ts +95 -0
- package/src/docloader.test.ts +128 -3
- package/src/docloader.ts +52 -93
package/src/docloader.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { getLogger } from "@logtape/logtape";
|
|
2
2
|
import { SpanKind, SpanStatusCode, trace } from "@opentelemetry/api";
|
|
3
3
|
import metadata from "../deno.json" with { type: "json" };
|
|
4
|
+
import { BodyTooLargeError, MAX_BODY_SIZE, readBoundedText } from "./body.ts";
|
|
4
5
|
import preloadedContexts from "./contexts.ts";
|
|
5
6
|
import { HttpHeaderLink } from "./link.ts";
|
|
6
7
|
import {
|
|
@@ -121,51 +122,6 @@ function createResponseMetadata(response: Response): Response {
|
|
|
121
122
|
});
|
|
122
123
|
}
|
|
123
124
|
|
|
124
|
-
async function cancelResponseBody(response: Response): Promise<void> {
|
|
125
|
-
if (response.body != null) {
|
|
126
|
-
await response.body.cancel();
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
async function readBoundedText(
|
|
131
|
-
response: Response,
|
|
132
|
-
maxBytes: number,
|
|
133
|
-
): Promise<{ text: string; size: number; tooLarge: boolean }> {
|
|
134
|
-
const contentLength = response.headers.get("Content-Length");
|
|
135
|
-
if (contentLength != null) {
|
|
136
|
-
const size = Number(contentLength);
|
|
137
|
-
if (size > maxBytes) {
|
|
138
|
-
await cancelResponseBody(response);
|
|
139
|
-
return { text: "", size, tooLarge: true };
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
if (response.body == null) return { text: "", size: 0, tooLarge: false };
|
|
144
|
-
|
|
145
|
-
const reader = response.body.getReader();
|
|
146
|
-
const decoder = new TextDecoder();
|
|
147
|
-
let text = "";
|
|
148
|
-
let size = 0;
|
|
149
|
-
try {
|
|
150
|
-
while (true) {
|
|
151
|
-
const result = await reader.read();
|
|
152
|
-
if (result.done) break;
|
|
153
|
-
const chunkSize = result.value.byteLength;
|
|
154
|
-
if (size + chunkSize > maxBytes) {
|
|
155
|
-
size += chunkSize;
|
|
156
|
-
await reader.cancel();
|
|
157
|
-
return { text: "", size, tooLarge: true };
|
|
158
|
-
}
|
|
159
|
-
size += chunkSize;
|
|
160
|
-
text += decoder.decode(result.value, { stream: true });
|
|
161
|
-
}
|
|
162
|
-
text += decoder.decode();
|
|
163
|
-
return { text, size, tooLarge: false };
|
|
164
|
-
} finally {
|
|
165
|
-
reader.releaseLock();
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
|
|
169
125
|
/**
|
|
170
126
|
* Gets a {@link RemoteDocument} from the given response.
|
|
171
127
|
* @param url The URL of the document to load.
|
|
@@ -255,11 +211,14 @@ export async function getRemoteDocument(
|
|
|
255
211
|
contentType?.startsWith("application/xhtml+xml;"))
|
|
256
212
|
) {
|
|
257
213
|
const errorResponse = createResponseMetadata(response);
|
|
258
|
-
|
|
259
|
-
|
|
214
|
+
let html: string;
|
|
215
|
+
try {
|
|
216
|
+
html = await readBoundedText(response, MAX_HTML_SIZE, documentUrl);
|
|
217
|
+
} catch (error) {
|
|
218
|
+
if (!(error instanceof BodyTooLargeError)) throw error;
|
|
260
219
|
logger.warn(
|
|
261
220
|
"HTML response too large, skipping alternate link discovery: {url}",
|
|
262
|
-
{ url: documentUrl,
|
|
221
|
+
{ url: documentUrl, maxBytes: MAX_HTML_SIZE },
|
|
263
222
|
);
|
|
264
223
|
throw new FetchError(
|
|
265
224
|
documentUrl,
|
|
@@ -267,58 +226,58 @@ export async function getRemoteDocument(
|
|
|
267
226
|
`(Content-Type: ${contentType})`,
|
|
268
227
|
errorResponse,
|
|
269
228
|
);
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
/([a-z][a-z:_-]*)=(?:"([^"]*)"|'([^']*)'|([^\s>]+))/gi;
|
|
278
|
-
|
|
279
|
-
let tagMatch: RegExpExecArray | null;
|
|
280
|
-
while ((tagMatch = tagPattern.exec(html.text)) !== null) {
|
|
281
|
-
const tagContent = tagMatch[2];
|
|
282
|
-
let attrMatch: RegExpExecArray | null;
|
|
283
|
-
const attribs: Record<string, string> = {};
|
|
229
|
+
}
|
|
230
|
+
// Safe regex patterns without nested quantifiers to prevent ReDoS
|
|
231
|
+
// (CVE-2025-68475)
|
|
232
|
+
// Step 1: Extract <a ...> or <link ...> tags
|
|
233
|
+
const tagPattern = /<(a|link)\s+([^>]*?)\s*\/?>/gi;
|
|
234
|
+
// Step 2: Parse attributes
|
|
235
|
+
const attrPattern = /([a-z][a-z:_-]*)=(?:"([^"]*)"|'([^']*)'|([^\s>]+))/gi;
|
|
284
236
|
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
attribs[key] = value;
|
|
291
|
-
}
|
|
237
|
+
let tagMatch: RegExpExecArray | null;
|
|
238
|
+
while ((tagMatch = tagPattern.exec(html)) !== null) {
|
|
239
|
+
const tagContent = tagMatch[2];
|
|
240
|
+
let attrMatch: RegExpExecArray | null;
|
|
241
|
+
const attribs: Record<string, string> = {};
|
|
292
242
|
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
new URL(attribs.href, docUrl).href !== docUrl.href
|
|
300
|
-
) {
|
|
301
|
-
logger.debug(
|
|
302
|
-
"Found alternate document: {alternateUrl} from {url}",
|
|
303
|
-
{ alternateUrl: attribs.href, url: documentUrl },
|
|
304
|
-
);
|
|
305
|
-
return await fetch(new URL(attribs.href, docUrl).href);
|
|
306
|
-
}
|
|
243
|
+
// Reset regex state for attribute parsing
|
|
244
|
+
attrPattern.lastIndex = 0;
|
|
245
|
+
while ((attrMatch = attrPattern.exec(tagContent)) !== null) {
|
|
246
|
+
const key = attrMatch[1].toLowerCase();
|
|
247
|
+
const value = attrMatch[2] ?? attrMatch[3] ?? attrMatch[4] ?? "";
|
|
248
|
+
attribs[key] = value;
|
|
307
249
|
}
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
250
|
+
|
|
251
|
+
if (
|
|
252
|
+
attribs.rel === "alternate" && "type" in attribs && (
|
|
253
|
+
attribs.type === "application/activity+json" ||
|
|
254
|
+
attribs.type === "application/ld+json" ||
|
|
255
|
+
attribs.type.startsWith("application/ld+json;")
|
|
256
|
+
) && "href" in attribs &&
|
|
257
|
+
new URL(attribs.href, docUrl).href !== docUrl.href
|
|
258
|
+
) {
|
|
259
|
+
logger.debug(
|
|
260
|
+
"Found alternate document: {alternateUrl} from {url}",
|
|
261
|
+
{ alternateUrl: attribs.href, url: documentUrl },
|
|
317
262
|
);
|
|
263
|
+
return await fetch(new URL(attribs.href, docUrl).href);
|
|
318
264
|
}
|
|
319
265
|
}
|
|
266
|
+
try {
|
|
267
|
+
document = JSON.parse(html);
|
|
268
|
+
} catch (error) {
|
|
269
|
+
if (!(error instanceof SyntaxError)) throw error;
|
|
270
|
+
throw new FetchError(
|
|
271
|
+
documentUrl,
|
|
272
|
+
`HTML document has no ActivityPub alternate link ` +
|
|
273
|
+
`(Content-Type: ${contentType})`,
|
|
274
|
+
errorResponse,
|
|
275
|
+
);
|
|
276
|
+
}
|
|
320
277
|
} else {
|
|
321
|
-
document =
|
|
278
|
+
document = JSON.parse(
|
|
279
|
+
await readBoundedText(response, MAX_BODY_SIZE, documentUrl),
|
|
280
|
+
);
|
|
322
281
|
}
|
|
323
282
|
logger.debug(
|
|
324
283
|
"Fetched document: {status} {url} {headers}",
|