@fedify/vocab-runtime 2.1.0 → 2.1.1
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 +30 -13
- package/dist/mod.js +30 -13
- package/dist/tests/decimal.test.cjs +2 -2
- package/dist/tests/decimal.test.js +2 -2
- package/dist/tests/{docloader-U31begIa.js → docloader-DIUeF-5W.js} +30 -13
- package/dist/tests/{docloader-D3nu2LmR.cjs → docloader-RQoMNF99.cjs} +30 -13
- package/dist/tests/docloader.test.cjs +49 -2
- package/dist/tests/docloader.test.js +49 -2
- package/dist/tests/{request-BH_NlxCL.js → request-D6fgLaP4.js} +1 -1
- package/dist/tests/{request-DyrEDYQ-.cjs → request-kJU0S4zp.cjs} +1 -1
- package/dist/tests/request.test.cjs +1 -1
- package/dist/tests/request.test.js +1 -1
- package/package.json +1 -1
- package/src/docloader.test.ts +67 -0
- package/src/docloader.ts +43 -11
package/deno.json
CHANGED
package/dist/mod.cjs
CHANGED
|
@@ -4376,7 +4376,7 @@ var contexts_default = preloadedContexts;
|
|
|
4376
4376
|
//#endregion
|
|
4377
4377
|
//#region deno.json
|
|
4378
4378
|
var name = "@fedify/vocab-runtime";
|
|
4379
|
-
var version = "2.1.
|
|
4379
|
+
var version = "2.1.1";
|
|
4380
4380
|
var license = "MIT";
|
|
4381
4381
|
var exports$1 = {
|
|
4382
4382
|
".": "./src/mod.ts",
|
|
@@ -4735,6 +4735,7 @@ const logger = (0, __logtape_logtape.getLogger)([
|
|
|
4735
4735
|
"runtime",
|
|
4736
4736
|
"docloader"
|
|
4737
4737
|
]);
|
|
4738
|
+
const DEFAULT_MAX_REDIRECTION = 20;
|
|
4738
4739
|
/**
|
|
4739
4740
|
* Gets a {@link RemoteDocument} from the given response.
|
|
4740
4741
|
* @param url The URL of the document to load.
|
|
@@ -4853,31 +4854,33 @@ async function getRemoteDocument(url, response, fetch$1) {
|
|
|
4853
4854
|
function getDocumentLoader({ allowPrivateAddress, skipPreloadedContexts, userAgent } = {}) {
|
|
4854
4855
|
const tracerProvider = __opentelemetry_api.trace.getTracerProvider();
|
|
4855
4856
|
const tracer = tracerProvider.getTracer(deno_default.name, deno_default.version);
|
|
4856
|
-
async function load(url, options) {
|
|
4857
|
+
async function load(url, options, redirected = 0, visited = /* @__PURE__ */ new Set()) {
|
|
4857
4858
|
options?.signal?.throwIfAborted();
|
|
4858
|
-
|
|
4859
|
-
|
|
4859
|
+
const currentUrl = new URL(url).href;
|
|
4860
|
+
if (!skipPreloadedContexts && currentUrl in contexts_default) {
|
|
4861
|
+
logger.debug("Using preloaded context: {url}.", { url: currentUrl });
|
|
4860
4862
|
return {
|
|
4861
4863
|
contextUrl: null,
|
|
4862
|
-
document: contexts_default[
|
|
4863
|
-
documentUrl:
|
|
4864
|
+
document: contexts_default[currentUrl],
|
|
4865
|
+
documentUrl: currentUrl
|
|
4864
4866
|
};
|
|
4865
4867
|
}
|
|
4866
4868
|
if (!allowPrivateAddress) try {
|
|
4867
|
-
await validatePublicUrl(
|
|
4869
|
+
await validatePublicUrl(currentUrl);
|
|
4868
4870
|
} catch (error) {
|
|
4869
4871
|
if (error instanceof UrlError) logger.error("Disallowed private URL: {url}", {
|
|
4870
|
-
url,
|
|
4872
|
+
url: currentUrl,
|
|
4871
4873
|
error
|
|
4872
4874
|
});
|
|
4873
4875
|
throw error;
|
|
4874
4876
|
}
|
|
4877
|
+
visited.add(currentUrl);
|
|
4875
4878
|
return await tracer.startActiveSpan("activitypub.fetch_document", {
|
|
4876
4879
|
kind: __opentelemetry_api.SpanKind.CLIENT,
|
|
4877
|
-
attributes: { "url.full":
|
|
4880
|
+
attributes: { "url.full": currentUrl }
|
|
4878
4881
|
}, async (span) => {
|
|
4879
4882
|
try {
|
|
4880
|
-
const request = createActivityPubRequest(
|
|
4883
|
+
const request = createActivityPubRequest(currentUrl, { userAgent });
|
|
4881
4884
|
logRequest(logger, request);
|
|
4882
4885
|
const response = await fetch(request, {
|
|
4883
4886
|
redirect: "manual",
|
|
@@ -4885,11 +4888,25 @@ function getDocumentLoader({ allowPrivateAddress, skipPreloadedContexts, userAge
|
|
|
4885
4888
|
});
|
|
4886
4889
|
span.setAttribute("http.response.status_code", response.status);
|
|
4887
4890
|
if (response.status >= 300 && response.status < 400 && response.headers.has("Location")) {
|
|
4888
|
-
|
|
4891
|
+
if (redirected >= DEFAULT_MAX_REDIRECTION) {
|
|
4892
|
+
logger.error("Too many redirections ({redirections}) while fetching document.", {
|
|
4893
|
+
redirections: redirected + 1,
|
|
4894
|
+
url: currentUrl
|
|
4895
|
+
});
|
|
4896
|
+
throw new FetchError(currentUrl, `Too many redirections (${redirected + 1})`);
|
|
4897
|
+
}
|
|
4898
|
+
const redirectUrl = new URL(response.headers.get("Location"), response.url === "" ? currentUrl : response.url).href;
|
|
4889
4899
|
span.setAttribute("http.redirect.url", redirectUrl);
|
|
4890
|
-
|
|
4900
|
+
if (visited.has(redirectUrl)) {
|
|
4901
|
+
logger.error("Detected a redirect loop while fetching document: {url} -> {redirectUrl}", {
|
|
4902
|
+
url: currentUrl,
|
|
4903
|
+
redirectUrl
|
|
4904
|
+
});
|
|
4905
|
+
throw new FetchError(currentUrl, `Redirect loop detected: ${redirectUrl}`);
|
|
4906
|
+
}
|
|
4907
|
+
return await load(redirectUrl, options, redirected + 1, visited);
|
|
4891
4908
|
}
|
|
4892
|
-
const result = await getRemoteDocument(
|
|
4909
|
+
const result = await getRemoteDocument(currentUrl, response, load);
|
|
4893
4910
|
span.setAttribute("docloader.document_url", result.documentUrl);
|
|
4894
4911
|
if (result.contextUrl != null) span.setAttribute("docloader.context_url", result.contextUrl);
|
|
4895
4912
|
return result;
|
package/dist/mod.js
CHANGED
|
@@ -4375,7 +4375,7 @@ var contexts_default = preloadedContexts;
|
|
|
4375
4375
|
//#endregion
|
|
4376
4376
|
//#region deno.json
|
|
4377
4377
|
var name = "@fedify/vocab-runtime";
|
|
4378
|
-
var version = "2.1.
|
|
4378
|
+
var version = "2.1.1";
|
|
4379
4379
|
var license = "MIT";
|
|
4380
4380
|
var exports = {
|
|
4381
4381
|
".": "./src/mod.ts",
|
|
@@ -4734,6 +4734,7 @@ const logger = getLogger([
|
|
|
4734
4734
|
"runtime",
|
|
4735
4735
|
"docloader"
|
|
4736
4736
|
]);
|
|
4737
|
+
const DEFAULT_MAX_REDIRECTION = 20;
|
|
4737
4738
|
/**
|
|
4738
4739
|
* Gets a {@link RemoteDocument} from the given response.
|
|
4739
4740
|
* @param url The URL of the document to load.
|
|
@@ -4852,31 +4853,33 @@ async function getRemoteDocument(url, response, fetch$1) {
|
|
|
4852
4853
|
function getDocumentLoader({ allowPrivateAddress, skipPreloadedContexts, userAgent } = {}) {
|
|
4853
4854
|
const tracerProvider = trace.getTracerProvider();
|
|
4854
4855
|
const tracer = tracerProvider.getTracer(deno_default.name, deno_default.version);
|
|
4855
|
-
async function load(url, options) {
|
|
4856
|
+
async function load(url, options, redirected = 0, visited = /* @__PURE__ */ new Set()) {
|
|
4856
4857
|
options?.signal?.throwIfAborted();
|
|
4857
|
-
|
|
4858
|
-
|
|
4858
|
+
const currentUrl = new URL(url).href;
|
|
4859
|
+
if (!skipPreloadedContexts && currentUrl in contexts_default) {
|
|
4860
|
+
logger.debug("Using preloaded context: {url}.", { url: currentUrl });
|
|
4859
4861
|
return {
|
|
4860
4862
|
contextUrl: null,
|
|
4861
|
-
document: contexts_default[
|
|
4862
|
-
documentUrl:
|
|
4863
|
+
document: contexts_default[currentUrl],
|
|
4864
|
+
documentUrl: currentUrl
|
|
4863
4865
|
};
|
|
4864
4866
|
}
|
|
4865
4867
|
if (!allowPrivateAddress) try {
|
|
4866
|
-
await validatePublicUrl(
|
|
4868
|
+
await validatePublicUrl(currentUrl);
|
|
4867
4869
|
} catch (error) {
|
|
4868
4870
|
if (error instanceof UrlError) logger.error("Disallowed private URL: {url}", {
|
|
4869
|
-
url,
|
|
4871
|
+
url: currentUrl,
|
|
4870
4872
|
error
|
|
4871
4873
|
});
|
|
4872
4874
|
throw error;
|
|
4873
4875
|
}
|
|
4876
|
+
visited.add(currentUrl);
|
|
4874
4877
|
return await tracer.startActiveSpan("activitypub.fetch_document", {
|
|
4875
4878
|
kind: SpanKind.CLIENT,
|
|
4876
|
-
attributes: { "url.full":
|
|
4879
|
+
attributes: { "url.full": currentUrl }
|
|
4877
4880
|
}, async (span) => {
|
|
4878
4881
|
try {
|
|
4879
|
-
const request = createActivityPubRequest(
|
|
4882
|
+
const request = createActivityPubRequest(currentUrl, { userAgent });
|
|
4880
4883
|
logRequest(logger, request);
|
|
4881
4884
|
const response = await fetch(request, {
|
|
4882
4885
|
redirect: "manual",
|
|
@@ -4884,11 +4887,25 @@ function getDocumentLoader({ allowPrivateAddress, skipPreloadedContexts, userAge
|
|
|
4884
4887
|
});
|
|
4885
4888
|
span.setAttribute("http.response.status_code", response.status);
|
|
4886
4889
|
if (response.status >= 300 && response.status < 400 && response.headers.has("Location")) {
|
|
4887
|
-
|
|
4890
|
+
if (redirected >= DEFAULT_MAX_REDIRECTION) {
|
|
4891
|
+
logger.error("Too many redirections ({redirections}) while fetching document.", {
|
|
4892
|
+
redirections: redirected + 1,
|
|
4893
|
+
url: currentUrl
|
|
4894
|
+
});
|
|
4895
|
+
throw new FetchError(currentUrl, `Too many redirections (${redirected + 1})`);
|
|
4896
|
+
}
|
|
4897
|
+
const redirectUrl = new URL(response.headers.get("Location"), response.url === "" ? currentUrl : response.url).href;
|
|
4888
4898
|
span.setAttribute("http.redirect.url", redirectUrl);
|
|
4889
|
-
|
|
4899
|
+
if (visited.has(redirectUrl)) {
|
|
4900
|
+
logger.error("Detected a redirect loop while fetching document: {url} -> {redirectUrl}", {
|
|
4901
|
+
url: currentUrl,
|
|
4902
|
+
redirectUrl
|
|
4903
|
+
});
|
|
4904
|
+
throw new FetchError(currentUrl, `Redirect loop detected: ${redirectUrl}`);
|
|
4905
|
+
}
|
|
4906
|
+
return await load(redirectUrl, options, redirected + 1, visited);
|
|
4890
4907
|
}
|
|
4891
|
-
const result = await getRemoteDocument(
|
|
4908
|
+
const result = await getRemoteDocument(currentUrl, response, load);
|
|
4892
4909
|
span.setAttribute("docloader.document_url", result.documentUrl);
|
|
4893
4910
|
if (result.contextUrl != null) span.setAttribute("docloader.context_url", result.contextUrl);
|
|
4894
4911
|
return result;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
const require_chunk = require('./chunk-DWy1uDak.cjs');
|
|
2
|
-
require('./docloader-
|
|
3
|
-
require('./request-
|
|
2
|
+
require('./docloader-RQoMNF99.cjs');
|
|
3
|
+
require('./request-kJU0S4zp.cjs');
|
|
4
4
|
require('./link-DYNFAdNu.cjs');
|
|
5
5
|
require('./url-DIjOdK8Q.cjs');
|
|
6
6
|
require('./multicodec--6hQ74zI.cjs');
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { FetchError, createActivityPubRequest, deno_default, logRequest } from "./request-
|
|
1
|
+
import { FetchError, createActivityPubRequest, deno_default, logRequest } from "./request-D6fgLaP4.js";
|
|
2
2
|
import { HttpHeaderLink } from "./link-C3q2TC2G.js";
|
|
3
3
|
import { UrlError, validatePublicUrl } from "./url-CWEP9Zs9.js";
|
|
4
4
|
import { getLogger } from "@logtape/logtape";
|
|
@@ -4373,6 +4373,7 @@ const logger = getLogger([
|
|
|
4373
4373
|
"runtime",
|
|
4374
4374
|
"docloader"
|
|
4375
4375
|
]);
|
|
4376
|
+
const DEFAULT_MAX_REDIRECTION = 20;
|
|
4376
4377
|
/**
|
|
4377
4378
|
* Gets a {@link RemoteDocument} from the given response.
|
|
4378
4379
|
* @param url The URL of the document to load.
|
|
@@ -4491,31 +4492,33 @@ async function getRemoteDocument(url, response, fetch$1) {
|
|
|
4491
4492
|
function getDocumentLoader({ allowPrivateAddress, skipPreloadedContexts, userAgent } = {}) {
|
|
4492
4493
|
const tracerProvider = trace.getTracerProvider();
|
|
4493
4494
|
const tracer = tracerProvider.getTracer(deno_default.name, deno_default.version);
|
|
4494
|
-
async function load(url, options) {
|
|
4495
|
+
async function load(url, options, redirected = 0, visited = /* @__PURE__ */ new Set()) {
|
|
4495
4496
|
options?.signal?.throwIfAborted();
|
|
4496
|
-
|
|
4497
|
-
|
|
4497
|
+
const currentUrl = new URL(url).href;
|
|
4498
|
+
if (!skipPreloadedContexts && currentUrl in contexts_default) {
|
|
4499
|
+
logger.debug("Using preloaded context: {url}.", { url: currentUrl });
|
|
4498
4500
|
return {
|
|
4499
4501
|
contextUrl: null,
|
|
4500
|
-
document: contexts_default[
|
|
4501
|
-
documentUrl:
|
|
4502
|
+
document: contexts_default[currentUrl],
|
|
4503
|
+
documentUrl: currentUrl
|
|
4502
4504
|
};
|
|
4503
4505
|
}
|
|
4504
4506
|
if (!allowPrivateAddress) try {
|
|
4505
|
-
await validatePublicUrl(
|
|
4507
|
+
await validatePublicUrl(currentUrl);
|
|
4506
4508
|
} catch (error) {
|
|
4507
4509
|
if (error instanceof UrlError) logger.error("Disallowed private URL: {url}", {
|
|
4508
|
-
url,
|
|
4510
|
+
url: currentUrl,
|
|
4509
4511
|
error
|
|
4510
4512
|
});
|
|
4511
4513
|
throw error;
|
|
4512
4514
|
}
|
|
4515
|
+
visited.add(currentUrl);
|
|
4513
4516
|
return await tracer.startActiveSpan("activitypub.fetch_document", {
|
|
4514
4517
|
kind: SpanKind.CLIENT,
|
|
4515
|
-
attributes: { "url.full":
|
|
4518
|
+
attributes: { "url.full": currentUrl }
|
|
4516
4519
|
}, async (span) => {
|
|
4517
4520
|
try {
|
|
4518
|
-
const request = createActivityPubRequest(
|
|
4521
|
+
const request = createActivityPubRequest(currentUrl, { userAgent });
|
|
4519
4522
|
logRequest(logger, request);
|
|
4520
4523
|
const response = await fetch(request, {
|
|
4521
4524
|
redirect: "manual",
|
|
@@ -4523,11 +4526,25 @@ function getDocumentLoader({ allowPrivateAddress, skipPreloadedContexts, userAge
|
|
|
4523
4526
|
});
|
|
4524
4527
|
span.setAttribute("http.response.status_code", response.status);
|
|
4525
4528
|
if (response.status >= 300 && response.status < 400 && response.headers.has("Location")) {
|
|
4526
|
-
|
|
4529
|
+
if (redirected >= DEFAULT_MAX_REDIRECTION) {
|
|
4530
|
+
logger.error("Too many redirections ({redirections}) while fetching document.", {
|
|
4531
|
+
redirections: redirected + 1,
|
|
4532
|
+
url: currentUrl
|
|
4533
|
+
});
|
|
4534
|
+
throw new FetchError(currentUrl, `Too many redirections (${redirected + 1})`);
|
|
4535
|
+
}
|
|
4536
|
+
const redirectUrl = new URL(response.headers.get("Location"), response.url === "" ? currentUrl : response.url).href;
|
|
4527
4537
|
span.setAttribute("http.redirect.url", redirectUrl);
|
|
4528
|
-
|
|
4538
|
+
if (visited.has(redirectUrl)) {
|
|
4539
|
+
logger.error("Detected a redirect loop while fetching document: {url} -> {redirectUrl}", {
|
|
4540
|
+
url: currentUrl,
|
|
4541
|
+
redirectUrl
|
|
4542
|
+
});
|
|
4543
|
+
throw new FetchError(currentUrl, `Redirect loop detected: ${redirectUrl}`);
|
|
4544
|
+
}
|
|
4545
|
+
return await load(redirectUrl, options, redirected + 1, visited);
|
|
4529
4546
|
}
|
|
4530
|
-
const result = await getRemoteDocument(
|
|
4547
|
+
const result = await getRemoteDocument(currentUrl, response, load);
|
|
4531
4548
|
span.setAttribute("docloader.document_url", result.documentUrl);
|
|
4532
4549
|
if (result.contextUrl != null) span.setAttribute("docloader.context_url", result.contextUrl);
|
|
4533
4550
|
return result;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
const require_chunk = require('./chunk-DWy1uDak.cjs');
|
|
2
|
-
const require_request = require('./request-
|
|
2
|
+
const require_request = require('./request-kJU0S4zp.cjs');
|
|
3
3
|
const require_link = require('./link-DYNFAdNu.cjs');
|
|
4
4
|
const require_url = require('./url-DIjOdK8Q.cjs');
|
|
5
5
|
const __logtape_logtape = require_chunk.__toESM(require("@logtape/logtape"));
|
|
@@ -4374,6 +4374,7 @@ const logger = (0, __logtape_logtape.getLogger)([
|
|
|
4374
4374
|
"runtime",
|
|
4375
4375
|
"docloader"
|
|
4376
4376
|
]);
|
|
4377
|
+
const DEFAULT_MAX_REDIRECTION = 20;
|
|
4377
4378
|
/**
|
|
4378
4379
|
* Gets a {@link RemoteDocument} from the given response.
|
|
4379
4380
|
* @param url The URL of the document to load.
|
|
@@ -4492,31 +4493,33 @@ async function getRemoteDocument(url, response, fetch$1) {
|
|
|
4492
4493
|
function getDocumentLoader({ allowPrivateAddress, skipPreloadedContexts, userAgent } = {}) {
|
|
4493
4494
|
const tracerProvider = __opentelemetry_api.trace.getTracerProvider();
|
|
4494
4495
|
const tracer = tracerProvider.getTracer(require_request.deno_default.name, require_request.deno_default.version);
|
|
4495
|
-
async function load(url, options) {
|
|
4496
|
+
async function load(url, options, redirected = 0, visited = /* @__PURE__ */ new Set()) {
|
|
4496
4497
|
options?.signal?.throwIfAborted();
|
|
4497
|
-
|
|
4498
|
-
|
|
4498
|
+
const currentUrl = new URL(url).href;
|
|
4499
|
+
if (!skipPreloadedContexts && currentUrl in contexts_default) {
|
|
4500
|
+
logger.debug("Using preloaded context: {url}.", { url: currentUrl });
|
|
4499
4501
|
return {
|
|
4500
4502
|
contextUrl: null,
|
|
4501
|
-
document: contexts_default[
|
|
4502
|
-
documentUrl:
|
|
4503
|
+
document: contexts_default[currentUrl],
|
|
4504
|
+
documentUrl: currentUrl
|
|
4503
4505
|
};
|
|
4504
4506
|
}
|
|
4505
4507
|
if (!allowPrivateAddress) try {
|
|
4506
|
-
await require_url.validatePublicUrl(
|
|
4508
|
+
await require_url.validatePublicUrl(currentUrl);
|
|
4507
4509
|
} catch (error) {
|
|
4508
4510
|
if (error instanceof require_url.UrlError) logger.error("Disallowed private URL: {url}", {
|
|
4509
|
-
url,
|
|
4511
|
+
url: currentUrl,
|
|
4510
4512
|
error
|
|
4511
4513
|
});
|
|
4512
4514
|
throw error;
|
|
4513
4515
|
}
|
|
4516
|
+
visited.add(currentUrl);
|
|
4514
4517
|
return await tracer.startActiveSpan("activitypub.fetch_document", {
|
|
4515
4518
|
kind: __opentelemetry_api.SpanKind.CLIENT,
|
|
4516
|
-
attributes: { "url.full":
|
|
4519
|
+
attributes: { "url.full": currentUrl }
|
|
4517
4520
|
}, async (span) => {
|
|
4518
4521
|
try {
|
|
4519
|
-
const request = require_request.createActivityPubRequest(
|
|
4522
|
+
const request = require_request.createActivityPubRequest(currentUrl, { userAgent });
|
|
4520
4523
|
require_request.logRequest(logger, request);
|
|
4521
4524
|
const response = await fetch(request, {
|
|
4522
4525
|
redirect: "manual",
|
|
@@ -4524,11 +4527,25 @@ function getDocumentLoader({ allowPrivateAddress, skipPreloadedContexts, userAge
|
|
|
4524
4527
|
});
|
|
4525
4528
|
span.setAttribute("http.response.status_code", response.status);
|
|
4526
4529
|
if (response.status >= 300 && response.status < 400 && response.headers.has("Location")) {
|
|
4527
|
-
|
|
4530
|
+
if (redirected >= DEFAULT_MAX_REDIRECTION) {
|
|
4531
|
+
logger.error("Too many redirections ({redirections}) while fetching document.", {
|
|
4532
|
+
redirections: redirected + 1,
|
|
4533
|
+
url: currentUrl
|
|
4534
|
+
});
|
|
4535
|
+
throw new require_request.FetchError(currentUrl, `Too many redirections (${redirected + 1})`);
|
|
4536
|
+
}
|
|
4537
|
+
const redirectUrl = new URL(response.headers.get("Location"), response.url === "" ? currentUrl : response.url).href;
|
|
4528
4538
|
span.setAttribute("http.redirect.url", redirectUrl);
|
|
4529
|
-
|
|
4539
|
+
if (visited.has(redirectUrl)) {
|
|
4540
|
+
logger.error("Detected a redirect loop while fetching document: {url} -> {redirectUrl}", {
|
|
4541
|
+
url: currentUrl,
|
|
4542
|
+
redirectUrl
|
|
4543
|
+
});
|
|
4544
|
+
throw new require_request.FetchError(currentUrl, `Redirect loop detected: ${redirectUrl}`);
|
|
4545
|
+
}
|
|
4546
|
+
return await load(redirectUrl, options, redirected + 1, visited);
|
|
4530
4547
|
}
|
|
4531
|
-
const result = await getRemoteDocument(
|
|
4548
|
+
const result = await getRemoteDocument(currentUrl, response, load);
|
|
4532
4549
|
span.setAttribute("docloader.document_url", result.documentUrl);
|
|
4533
4550
|
if (result.contextUrl != null) span.setAttribute("docloader.context_url", result.contextUrl);
|
|
4534
4551
|
return result;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
const require_chunk = require('./chunk-DWy1uDak.cjs');
|
|
2
|
-
const require_docloader = require('./docloader-
|
|
3
|
-
const require_request = require('./request-
|
|
2
|
+
const require_docloader = require('./docloader-RQoMNF99.cjs');
|
|
3
|
+
const require_request = require('./request-kJU0S4zp.cjs');
|
|
4
4
|
require('./link-DYNFAdNu.cjs');
|
|
5
5
|
const require_url = require('./url-DIjOdK8Q.cjs');
|
|
6
6
|
const node_assert = require_chunk.__toESM(require("node:assert"));
|
|
@@ -1512,6 +1512,53 @@ var esm_default = FetchMock_default;
|
|
|
1512
1512
|
(0, node_assert.deepStrictEqual)(await fetchDocumentLoader2("https://example.com/localhost-redirect"), expected);
|
|
1513
1513
|
(0, node_assert.deepStrictEqual)(await fetchDocumentLoader2("https://example.com/localhost-link"), expected);
|
|
1514
1514
|
});
|
|
1515
|
+
let redirectAttempts = 0;
|
|
1516
|
+
esm_default.get("begin:https://example.com/too-many-redirects/", (cl) => {
|
|
1517
|
+
redirectAttempts++;
|
|
1518
|
+
const index = Number(cl.url.split("/").at(-1));
|
|
1519
|
+
return {
|
|
1520
|
+
status: 302,
|
|
1521
|
+
headers: { Location: `https://example.com/too-many-redirects/${index + 1}` }
|
|
1522
|
+
};
|
|
1523
|
+
});
|
|
1524
|
+
await t.test("too many redirects", async () => {
|
|
1525
|
+
redirectAttempts = 0;
|
|
1526
|
+
await (0, node_assert.rejects)(() => fetchDocumentLoader("https://example.com/too-many-redirects/0"), require_request.FetchError, "Too many redirections");
|
|
1527
|
+
(0, node_assert.deepStrictEqual)(redirectAttempts, 21);
|
|
1528
|
+
});
|
|
1529
|
+
let loopAttempts = 0;
|
|
1530
|
+
esm_default.get("https://example.com/redirect-loop-a", () => {
|
|
1531
|
+
loopAttempts++;
|
|
1532
|
+
return {
|
|
1533
|
+
status: 302,
|
|
1534
|
+
headers: { Location: "https://example.com/redirect-loop-b" }
|
|
1535
|
+
};
|
|
1536
|
+
});
|
|
1537
|
+
esm_default.get("https://example.com/redirect-loop-b", () => {
|
|
1538
|
+
loopAttempts++;
|
|
1539
|
+
return {
|
|
1540
|
+
status: 302,
|
|
1541
|
+
headers: { Location: "https://example.com/redirect-loop-a" }
|
|
1542
|
+
};
|
|
1543
|
+
});
|
|
1544
|
+
await t.test("redirect loop", async () => {
|
|
1545
|
+
loopAttempts = 0;
|
|
1546
|
+
await (0, node_assert.rejects)(() => fetchDocumentLoader("https://example.com/redirect-loop-a"), require_request.FetchError, "Redirect loop detected");
|
|
1547
|
+
(0, node_assert.deepStrictEqual)(loopAttempts, 2);
|
|
1548
|
+
});
|
|
1549
|
+
let relativeLoopAttempts = 0;
|
|
1550
|
+
esm_default.get("https://example.com/redirect-loop-relative", () => {
|
|
1551
|
+
relativeLoopAttempts++;
|
|
1552
|
+
return {
|
|
1553
|
+
status: 302,
|
|
1554
|
+
headers: { Location: "/redirect-loop-relative" }
|
|
1555
|
+
};
|
|
1556
|
+
});
|
|
1557
|
+
await t.test("redirect loop with relative location", async () => {
|
|
1558
|
+
relativeLoopAttempts = 0;
|
|
1559
|
+
await (0, node_assert.rejects)(() => fetchDocumentLoader("https://example.com/redirect-loop-relative"), require_request.FetchError, "Redirect loop detected");
|
|
1560
|
+
(0, node_assert.deepStrictEqual)(relativeLoopAttempts, 1);
|
|
1561
|
+
});
|
|
1515
1562
|
const maliciousPayload = "<a" + " a=\"b\"".repeat(30) + " ";
|
|
1516
1563
|
esm_default.get("https://example.com/redos", {
|
|
1517
1564
|
body: maliciousPayload,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { contexts_default, getDocumentLoader } from "./docloader-
|
|
2
|
-
import { FetchError } from "./request-
|
|
1
|
+
import { contexts_default, getDocumentLoader } from "./docloader-DIUeF-5W.js";
|
|
2
|
+
import { FetchError } from "./request-D6fgLaP4.js";
|
|
3
3
|
import "./link-C3q2TC2G.js";
|
|
4
4
|
import { UrlError } from "./url-CWEP9Zs9.js";
|
|
5
5
|
import "node:module";
|
|
@@ -1538,6 +1538,53 @@ test("getDocumentLoader()", async (t) => {
|
|
|
1538
1538
|
deepStrictEqual(await fetchDocumentLoader2("https://example.com/localhost-redirect"), expected);
|
|
1539
1539
|
deepStrictEqual(await fetchDocumentLoader2("https://example.com/localhost-link"), expected);
|
|
1540
1540
|
});
|
|
1541
|
+
let redirectAttempts = 0;
|
|
1542
|
+
esm_default.get("begin:https://example.com/too-many-redirects/", (cl) => {
|
|
1543
|
+
redirectAttempts++;
|
|
1544
|
+
const index = Number(cl.url.split("/").at(-1));
|
|
1545
|
+
return {
|
|
1546
|
+
status: 302,
|
|
1547
|
+
headers: { Location: `https://example.com/too-many-redirects/${index + 1}` }
|
|
1548
|
+
};
|
|
1549
|
+
});
|
|
1550
|
+
await t.test("too many redirects", async () => {
|
|
1551
|
+
redirectAttempts = 0;
|
|
1552
|
+
await rejects(() => fetchDocumentLoader("https://example.com/too-many-redirects/0"), FetchError, "Too many redirections");
|
|
1553
|
+
deepStrictEqual(redirectAttempts, 21);
|
|
1554
|
+
});
|
|
1555
|
+
let loopAttempts = 0;
|
|
1556
|
+
esm_default.get("https://example.com/redirect-loop-a", () => {
|
|
1557
|
+
loopAttempts++;
|
|
1558
|
+
return {
|
|
1559
|
+
status: 302,
|
|
1560
|
+
headers: { Location: "https://example.com/redirect-loop-b" }
|
|
1561
|
+
};
|
|
1562
|
+
});
|
|
1563
|
+
esm_default.get("https://example.com/redirect-loop-b", () => {
|
|
1564
|
+
loopAttempts++;
|
|
1565
|
+
return {
|
|
1566
|
+
status: 302,
|
|
1567
|
+
headers: { Location: "https://example.com/redirect-loop-a" }
|
|
1568
|
+
};
|
|
1569
|
+
});
|
|
1570
|
+
await t.test("redirect loop", async () => {
|
|
1571
|
+
loopAttempts = 0;
|
|
1572
|
+
await rejects(() => fetchDocumentLoader("https://example.com/redirect-loop-a"), FetchError, "Redirect loop detected");
|
|
1573
|
+
deepStrictEqual(loopAttempts, 2);
|
|
1574
|
+
});
|
|
1575
|
+
let relativeLoopAttempts = 0;
|
|
1576
|
+
esm_default.get("https://example.com/redirect-loop-relative", () => {
|
|
1577
|
+
relativeLoopAttempts++;
|
|
1578
|
+
return {
|
|
1579
|
+
status: 302,
|
|
1580
|
+
headers: { Location: "/redirect-loop-relative" }
|
|
1581
|
+
};
|
|
1582
|
+
});
|
|
1583
|
+
await t.test("redirect loop with relative location", async () => {
|
|
1584
|
+
relativeLoopAttempts = 0;
|
|
1585
|
+
await rejects(() => fetchDocumentLoader("https://example.com/redirect-loop-relative"), FetchError, "Redirect loop detected");
|
|
1586
|
+
deepStrictEqual(relativeLoopAttempts, 1);
|
|
1587
|
+
});
|
|
1541
1588
|
const maliciousPayload = "<a" + " a=\"b\"".repeat(30) + " ";
|
|
1542
1589
|
esm_default.get("https://example.com/redos", {
|
|
1543
1590
|
body: maliciousPayload,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
const require_chunk = require('./chunk-DWy1uDak.cjs');
|
|
2
|
-
const require_request = require('./request-
|
|
2
|
+
const require_request = require('./request-kJU0S4zp.cjs');
|
|
3
3
|
const node_assert = require_chunk.__toESM(require("node:assert"));
|
|
4
4
|
const node_test = require_chunk.__toESM(require("node:test"));
|
|
5
5
|
const node_process = require_chunk.__toESM(require("node:process"));
|
package/package.json
CHANGED
package/src/docloader.test.ts
CHANGED
|
@@ -369,6 +369,73 @@ test("getDocumentLoader()", async (t) => {
|
|
|
369
369
|
);
|
|
370
370
|
});
|
|
371
371
|
|
|
372
|
+
let redirectAttempts = 0;
|
|
373
|
+
fetchMock.get("begin:https://example.com/too-many-redirects/", (cl) => {
|
|
374
|
+
redirectAttempts++;
|
|
375
|
+
const index = Number(cl.url.split("/").at(-1));
|
|
376
|
+
return {
|
|
377
|
+
status: 302,
|
|
378
|
+
headers: {
|
|
379
|
+
Location: `https://example.com/too-many-redirects/${index + 1}`,
|
|
380
|
+
},
|
|
381
|
+
};
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
await t.test("too many redirects", async () => {
|
|
385
|
+
redirectAttempts = 0;
|
|
386
|
+
await rejects(
|
|
387
|
+
() => fetchDocumentLoader("https://example.com/too-many-redirects/0"),
|
|
388
|
+
FetchError,
|
|
389
|
+
"Too many redirections",
|
|
390
|
+
);
|
|
391
|
+
deepStrictEqual(redirectAttempts, 21);
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
let loopAttempts = 0;
|
|
395
|
+
fetchMock.get("https://example.com/redirect-loop-a", () => {
|
|
396
|
+
loopAttempts++;
|
|
397
|
+
return {
|
|
398
|
+
status: 302,
|
|
399
|
+
headers: { Location: "https://example.com/redirect-loop-b" },
|
|
400
|
+
};
|
|
401
|
+
});
|
|
402
|
+
fetchMock.get("https://example.com/redirect-loop-b", () => {
|
|
403
|
+
loopAttempts++;
|
|
404
|
+
return {
|
|
405
|
+
status: 302,
|
|
406
|
+
headers: { Location: "https://example.com/redirect-loop-a" },
|
|
407
|
+
};
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
await t.test("redirect loop", async () => {
|
|
411
|
+
loopAttempts = 0;
|
|
412
|
+
await rejects(
|
|
413
|
+
() => fetchDocumentLoader("https://example.com/redirect-loop-a"),
|
|
414
|
+
FetchError,
|
|
415
|
+
"Redirect loop detected",
|
|
416
|
+
);
|
|
417
|
+
deepStrictEqual(loopAttempts, 2);
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
let relativeLoopAttempts = 0;
|
|
421
|
+
fetchMock.get("https://example.com/redirect-loop-relative", () => {
|
|
422
|
+
relativeLoopAttempts++;
|
|
423
|
+
return {
|
|
424
|
+
status: 302,
|
|
425
|
+
headers: { Location: "/redirect-loop-relative" },
|
|
426
|
+
};
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
await t.test("redirect loop with relative location", async () => {
|
|
430
|
+
relativeLoopAttempts = 0;
|
|
431
|
+
await rejects(
|
|
432
|
+
() => fetchDocumentLoader("https://example.com/redirect-loop-relative"),
|
|
433
|
+
FetchError,
|
|
434
|
+
"Redirect loop detected",
|
|
435
|
+
);
|
|
436
|
+
deepStrictEqual(relativeLoopAttempts, 1);
|
|
437
|
+
});
|
|
438
|
+
|
|
372
439
|
// Regression test for ReDoS vulnerability (CVE-2025-68475)
|
|
373
440
|
// Malicious HTML payload: <a a="b" a="b" ... (unclosed tag)
|
|
374
441
|
// With the vulnerable regex, this causes catastrophic backtracking
|
package/src/docloader.ts
CHANGED
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
import { UrlError, validatePublicUrl } from "./url.ts";
|
|
13
13
|
|
|
14
14
|
const logger = getLogger(["fedify", "runtime", "docloader"]);
|
|
15
|
+
const DEFAULT_MAX_REDIRECTION = 20;
|
|
15
16
|
|
|
16
17
|
/**
|
|
17
18
|
* A remote JSON-LD document and its context fetched by
|
|
@@ -293,38 +294,45 @@ export function getDocumentLoader(
|
|
|
293
294
|
async function load(
|
|
294
295
|
url: string,
|
|
295
296
|
options?: DocumentLoaderOptions,
|
|
297
|
+
redirected = 0,
|
|
298
|
+
visited = new Set<string>(),
|
|
296
299
|
): Promise<RemoteDocument> {
|
|
297
300
|
options?.signal?.throwIfAborted();
|
|
298
|
-
|
|
299
|
-
|
|
301
|
+
const currentUrl = new URL(url).href;
|
|
302
|
+
if (!skipPreloadedContexts && currentUrl in preloadedContexts) {
|
|
303
|
+
logger.debug("Using preloaded context: {url}.", { url: currentUrl });
|
|
300
304
|
return {
|
|
301
305
|
contextUrl: null,
|
|
302
|
-
document: preloadedContexts[
|
|
303
|
-
documentUrl:
|
|
306
|
+
document: preloadedContexts[currentUrl],
|
|
307
|
+
documentUrl: currentUrl,
|
|
304
308
|
};
|
|
305
309
|
}
|
|
306
310
|
if (!allowPrivateAddress) {
|
|
307
311
|
try {
|
|
308
|
-
await validatePublicUrl(
|
|
312
|
+
await validatePublicUrl(currentUrl);
|
|
309
313
|
} catch (error) {
|
|
310
314
|
if (error instanceof UrlError) {
|
|
311
|
-
logger.error("Disallowed private URL: {url}", {
|
|
315
|
+
logger.error("Disallowed private URL: {url}", {
|
|
316
|
+
url: currentUrl,
|
|
317
|
+
error,
|
|
318
|
+
});
|
|
312
319
|
}
|
|
313
320
|
throw error;
|
|
314
321
|
}
|
|
315
322
|
}
|
|
323
|
+
visited.add(currentUrl);
|
|
316
324
|
|
|
317
325
|
return await tracer.startActiveSpan(
|
|
318
326
|
"activitypub.fetch_document",
|
|
319
327
|
{
|
|
320
328
|
kind: SpanKind.CLIENT,
|
|
321
329
|
attributes: {
|
|
322
|
-
"url.full":
|
|
330
|
+
"url.full": currentUrl,
|
|
323
331
|
},
|
|
324
332
|
},
|
|
325
333
|
async (span) => {
|
|
326
334
|
try {
|
|
327
|
-
const request = createActivityPubRequest(
|
|
335
|
+
const request = createActivityPubRequest(currentUrl, { userAgent });
|
|
328
336
|
logRequest(logger, request);
|
|
329
337
|
const response = await fetch(request, {
|
|
330
338
|
// Since Bun has a bug that ignores the `Request.redirect` option,
|
|
@@ -340,12 +348,36 @@ export function getDocumentLoader(
|
|
|
340
348
|
response.status >= 300 && response.status < 400 &&
|
|
341
349
|
response.headers.has("Location")
|
|
342
350
|
) {
|
|
343
|
-
|
|
351
|
+
if (redirected >= DEFAULT_MAX_REDIRECTION) {
|
|
352
|
+
logger.error(
|
|
353
|
+
"Too many redirections ({redirections}) while fetching document.",
|
|
354
|
+
{ redirections: redirected + 1, url: currentUrl },
|
|
355
|
+
);
|
|
356
|
+
throw new FetchError(
|
|
357
|
+
currentUrl,
|
|
358
|
+
`Too many redirections (${redirected + 1})`,
|
|
359
|
+
);
|
|
360
|
+
}
|
|
361
|
+
const redirectUrl = new URL(
|
|
362
|
+
response.headers.get("Location")!,
|
|
363
|
+
response.url === "" ? currentUrl : response.url,
|
|
364
|
+
).href;
|
|
344
365
|
span.setAttribute("http.redirect.url", redirectUrl);
|
|
345
|
-
|
|
366
|
+
if (visited.has(redirectUrl)) {
|
|
367
|
+
logger.error(
|
|
368
|
+
"Detected a redirect loop while fetching document: {url} -> " +
|
|
369
|
+
"{redirectUrl}",
|
|
370
|
+
{ url: currentUrl, redirectUrl },
|
|
371
|
+
);
|
|
372
|
+
throw new FetchError(
|
|
373
|
+
currentUrl,
|
|
374
|
+
`Redirect loop detected: ${redirectUrl}`,
|
|
375
|
+
);
|
|
376
|
+
}
|
|
377
|
+
return await load(redirectUrl, options, redirected + 1, visited);
|
|
346
378
|
}
|
|
347
379
|
|
|
348
|
-
const result = await getRemoteDocument(
|
|
380
|
+
const result = await getRemoteDocument(currentUrl, response, load);
|
|
349
381
|
span.setAttribute("docloader.document_url", result.documentUrl);
|
|
350
382
|
if (result.contextUrl != null) {
|
|
351
383
|
span.setAttribute("docloader.context_url", result.contextUrl);
|