@fedify/vocab-runtime 2.0.7 → 2.0.8
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/docloader.test.cjs +77 -13
- package/dist/tests/docloader.test.js +77 -13
- package/dist/tests/{request-BZixuWv5.js → request-Cqx2eUpt.js} +1 -1
- package/dist/tests/{request-78UEYyIx.cjs → request-D2-F2dMS.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
|
@@ -4263,7 +4263,7 @@ var contexts_default = preloadedContexts;
|
|
|
4263
4263
|
//#endregion
|
|
4264
4264
|
//#region deno.json
|
|
4265
4265
|
var name = "@fedify/vocab-runtime";
|
|
4266
|
-
var version = "2.0.
|
|
4266
|
+
var version = "2.0.8";
|
|
4267
4267
|
var license = "MIT";
|
|
4268
4268
|
var exports$1 = {
|
|
4269
4269
|
".": "./src/mod.ts",
|
|
@@ -4616,6 +4616,7 @@ const logger = (0, __logtape_logtape.getLogger)([
|
|
|
4616
4616
|
"runtime",
|
|
4617
4617
|
"docloader"
|
|
4618
4618
|
]);
|
|
4619
|
+
const DEFAULT_MAX_REDIRECTION = 20;
|
|
4619
4620
|
/**
|
|
4620
4621
|
* Gets a {@link RemoteDocument} from the given response.
|
|
4621
4622
|
* @param url The URL of the document to load.
|
|
@@ -4734,31 +4735,33 @@ async function getRemoteDocument(url, response, fetch$1) {
|
|
|
4734
4735
|
function getDocumentLoader({ allowPrivateAddress, skipPreloadedContexts, userAgent } = {}) {
|
|
4735
4736
|
const tracerProvider = __opentelemetry_api.trace.getTracerProvider();
|
|
4736
4737
|
const tracer = tracerProvider.getTracer(deno_default.name, deno_default.version);
|
|
4737
|
-
async function load(url, options) {
|
|
4738
|
+
async function load(url, options, redirected = 0, visited = /* @__PURE__ */ new Set()) {
|
|
4738
4739
|
options?.signal?.throwIfAborted();
|
|
4739
|
-
|
|
4740
|
-
|
|
4740
|
+
const currentUrl = new URL(url).href;
|
|
4741
|
+
if (!skipPreloadedContexts && currentUrl in contexts_default) {
|
|
4742
|
+
logger.debug("Using preloaded context: {url}.", { url: currentUrl });
|
|
4741
4743
|
return {
|
|
4742
4744
|
contextUrl: null,
|
|
4743
|
-
document: contexts_default[
|
|
4744
|
-
documentUrl:
|
|
4745
|
+
document: contexts_default[currentUrl],
|
|
4746
|
+
documentUrl: currentUrl
|
|
4745
4747
|
};
|
|
4746
4748
|
}
|
|
4747
4749
|
if (!allowPrivateAddress) try {
|
|
4748
|
-
await validatePublicUrl(
|
|
4750
|
+
await validatePublicUrl(currentUrl);
|
|
4749
4751
|
} catch (error) {
|
|
4750
4752
|
if (error instanceof UrlError) logger.error("Disallowed private URL: {url}", {
|
|
4751
|
-
url,
|
|
4753
|
+
url: currentUrl,
|
|
4752
4754
|
error
|
|
4753
4755
|
});
|
|
4754
4756
|
throw error;
|
|
4755
4757
|
}
|
|
4758
|
+
visited.add(currentUrl);
|
|
4756
4759
|
return await tracer.startActiveSpan("activitypub.fetch_document", {
|
|
4757
4760
|
kind: __opentelemetry_api.SpanKind.CLIENT,
|
|
4758
|
-
attributes: { "url.full":
|
|
4761
|
+
attributes: { "url.full": currentUrl }
|
|
4759
4762
|
}, async (span) => {
|
|
4760
4763
|
try {
|
|
4761
|
-
const request = createActivityPubRequest(
|
|
4764
|
+
const request = createActivityPubRequest(currentUrl, { userAgent });
|
|
4762
4765
|
logRequest(logger, request);
|
|
4763
4766
|
const response = await fetch(request, {
|
|
4764
4767
|
redirect: "manual",
|
|
@@ -4766,11 +4769,25 @@ function getDocumentLoader({ allowPrivateAddress, skipPreloadedContexts, userAge
|
|
|
4766
4769
|
});
|
|
4767
4770
|
span.setAttribute("http.response.status_code", response.status);
|
|
4768
4771
|
if (response.status >= 300 && response.status < 400 && response.headers.has("Location")) {
|
|
4769
|
-
|
|
4772
|
+
if (redirected >= DEFAULT_MAX_REDIRECTION) {
|
|
4773
|
+
logger.error("Too many redirections ({redirections}) while fetching document.", {
|
|
4774
|
+
redirections: redirected + 1,
|
|
4775
|
+
url: currentUrl
|
|
4776
|
+
});
|
|
4777
|
+
throw new FetchError(currentUrl, `Too many redirections (${redirected + 1})`);
|
|
4778
|
+
}
|
|
4779
|
+
const redirectUrl = new URL(response.headers.get("Location"), response.url === "" ? currentUrl : response.url).href;
|
|
4770
4780
|
span.setAttribute("http.redirect.url", redirectUrl);
|
|
4771
|
-
|
|
4781
|
+
if (visited.has(redirectUrl)) {
|
|
4782
|
+
logger.error("Detected a redirect loop while fetching document: {url} -> {redirectUrl}", {
|
|
4783
|
+
url: currentUrl,
|
|
4784
|
+
redirectUrl
|
|
4785
|
+
});
|
|
4786
|
+
throw new FetchError(currentUrl, `Redirect loop detected: ${redirectUrl}`);
|
|
4787
|
+
}
|
|
4788
|
+
return await load(redirectUrl, options, redirected + 1, visited);
|
|
4772
4789
|
}
|
|
4773
|
-
const result = await getRemoteDocument(
|
|
4790
|
+
const result = await getRemoteDocument(currentUrl, response, load);
|
|
4774
4791
|
span.setAttribute("docloader.document_url", result.documentUrl);
|
|
4775
4792
|
if (result.contextUrl != null) span.setAttribute("docloader.context_url", result.contextUrl);
|
|
4776
4793
|
return result;
|
package/dist/mod.js
CHANGED
|
@@ -4262,7 +4262,7 @@ var contexts_default = preloadedContexts;
|
|
|
4262
4262
|
//#endregion
|
|
4263
4263
|
//#region deno.json
|
|
4264
4264
|
var name = "@fedify/vocab-runtime";
|
|
4265
|
-
var version = "2.0.
|
|
4265
|
+
var version = "2.0.8";
|
|
4266
4266
|
var license = "MIT";
|
|
4267
4267
|
var exports = {
|
|
4268
4268
|
".": "./src/mod.ts",
|
|
@@ -4615,6 +4615,7 @@ const logger = getLogger([
|
|
|
4615
4615
|
"runtime",
|
|
4616
4616
|
"docloader"
|
|
4617
4617
|
]);
|
|
4618
|
+
const DEFAULT_MAX_REDIRECTION = 20;
|
|
4618
4619
|
/**
|
|
4619
4620
|
* Gets a {@link RemoteDocument} from the given response.
|
|
4620
4621
|
* @param url The URL of the document to load.
|
|
@@ -4733,31 +4734,33 @@ async function getRemoteDocument(url, response, fetch$1) {
|
|
|
4733
4734
|
function getDocumentLoader({ allowPrivateAddress, skipPreloadedContexts, userAgent } = {}) {
|
|
4734
4735
|
const tracerProvider = trace.getTracerProvider();
|
|
4735
4736
|
const tracer = tracerProvider.getTracer(deno_default.name, deno_default.version);
|
|
4736
|
-
async function load(url, options) {
|
|
4737
|
+
async function load(url, options, redirected = 0, visited = /* @__PURE__ */ new Set()) {
|
|
4737
4738
|
options?.signal?.throwIfAborted();
|
|
4738
|
-
|
|
4739
|
-
|
|
4739
|
+
const currentUrl = new URL(url).href;
|
|
4740
|
+
if (!skipPreloadedContexts && currentUrl in contexts_default) {
|
|
4741
|
+
logger.debug("Using preloaded context: {url}.", { url: currentUrl });
|
|
4740
4742
|
return {
|
|
4741
4743
|
contextUrl: null,
|
|
4742
|
-
document: contexts_default[
|
|
4743
|
-
documentUrl:
|
|
4744
|
+
document: contexts_default[currentUrl],
|
|
4745
|
+
documentUrl: currentUrl
|
|
4744
4746
|
};
|
|
4745
4747
|
}
|
|
4746
4748
|
if (!allowPrivateAddress) try {
|
|
4747
|
-
await validatePublicUrl(
|
|
4749
|
+
await validatePublicUrl(currentUrl);
|
|
4748
4750
|
} catch (error) {
|
|
4749
4751
|
if (error instanceof UrlError) logger.error("Disallowed private URL: {url}", {
|
|
4750
|
-
url,
|
|
4752
|
+
url: currentUrl,
|
|
4751
4753
|
error
|
|
4752
4754
|
});
|
|
4753
4755
|
throw error;
|
|
4754
4756
|
}
|
|
4757
|
+
visited.add(currentUrl);
|
|
4755
4758
|
return await tracer.startActiveSpan("activitypub.fetch_document", {
|
|
4756
4759
|
kind: SpanKind.CLIENT,
|
|
4757
|
-
attributes: { "url.full":
|
|
4760
|
+
attributes: { "url.full": currentUrl }
|
|
4758
4761
|
}, async (span) => {
|
|
4759
4762
|
try {
|
|
4760
|
-
const request = createActivityPubRequest(
|
|
4763
|
+
const request = createActivityPubRequest(currentUrl, { userAgent });
|
|
4761
4764
|
logRequest(logger, request);
|
|
4762
4765
|
const response = await fetch(request, {
|
|
4763
4766
|
redirect: "manual",
|
|
@@ -4765,11 +4768,25 @@ function getDocumentLoader({ allowPrivateAddress, skipPreloadedContexts, userAge
|
|
|
4765
4768
|
});
|
|
4766
4769
|
span.setAttribute("http.response.status_code", response.status);
|
|
4767
4770
|
if (response.status >= 300 && response.status < 400 && response.headers.has("Location")) {
|
|
4768
|
-
|
|
4771
|
+
if (redirected >= DEFAULT_MAX_REDIRECTION) {
|
|
4772
|
+
logger.error("Too many redirections ({redirections}) while fetching document.", {
|
|
4773
|
+
redirections: redirected + 1,
|
|
4774
|
+
url: currentUrl
|
|
4775
|
+
});
|
|
4776
|
+
throw new FetchError(currentUrl, `Too many redirections (${redirected + 1})`);
|
|
4777
|
+
}
|
|
4778
|
+
const redirectUrl = new URL(response.headers.get("Location"), response.url === "" ? currentUrl : response.url).href;
|
|
4769
4779
|
span.setAttribute("http.redirect.url", redirectUrl);
|
|
4770
|
-
|
|
4780
|
+
if (visited.has(redirectUrl)) {
|
|
4781
|
+
logger.error("Detected a redirect loop while fetching document: {url} -> {redirectUrl}", {
|
|
4782
|
+
url: currentUrl,
|
|
4783
|
+
redirectUrl
|
|
4784
|
+
});
|
|
4785
|
+
throw new FetchError(currentUrl, `Redirect loop detected: ${redirectUrl}`);
|
|
4786
|
+
}
|
|
4787
|
+
return await load(redirectUrl, options, redirected + 1, visited);
|
|
4771
4788
|
}
|
|
4772
|
-
const result = await getRemoteDocument(
|
|
4789
|
+
const result = await getRemoteDocument(currentUrl, response, load);
|
|
4773
4790
|
span.setAttribute("docloader.document_url", result.documentUrl);
|
|
4774
4791
|
if (result.contextUrl != null) span.setAttribute("docloader.context_url", result.contextUrl);
|
|
4775
4792
|
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-D2-F2dMS.cjs');
|
|
3
3
|
const require_link = require('./link-CdFPEo9O.cjs');
|
|
4
4
|
const require_url = require('./url-C5Vs9nYh.cjs');
|
|
5
5
|
const node_assert = require_chunk.__toESM(require("node:assert"));
|
|
@@ -5497,6 +5497,7 @@ const logger = (0, __logtape_logtape.getLogger)([
|
|
|
5497
5497
|
"runtime",
|
|
5498
5498
|
"docloader"
|
|
5499
5499
|
]);
|
|
5500
|
+
const DEFAULT_MAX_REDIRECTION = 20;
|
|
5500
5501
|
/**
|
|
5501
5502
|
* Gets a {@link RemoteDocument} from the given response.
|
|
5502
5503
|
* @param url The URL of the document to load.
|
|
@@ -5615,31 +5616,33 @@ async function getRemoteDocument(url, response, fetch$1) {
|
|
|
5615
5616
|
function getDocumentLoader({ allowPrivateAddress, skipPreloadedContexts, userAgent } = {}) {
|
|
5616
5617
|
const tracerProvider = __opentelemetry_api.trace.getTracerProvider();
|
|
5617
5618
|
const tracer = tracerProvider.getTracer(require_request.deno_default.name, require_request.deno_default.version);
|
|
5618
|
-
async function load(url, options) {
|
|
5619
|
+
async function load(url, options, redirected = 0, visited = /* @__PURE__ */ new Set()) {
|
|
5619
5620
|
options?.signal?.throwIfAborted();
|
|
5620
|
-
|
|
5621
|
-
|
|
5621
|
+
const currentUrl = new URL(url).href;
|
|
5622
|
+
if (!skipPreloadedContexts && currentUrl in contexts_default) {
|
|
5623
|
+
logger.debug("Using preloaded context: {url}.", { url: currentUrl });
|
|
5622
5624
|
return {
|
|
5623
5625
|
contextUrl: null,
|
|
5624
|
-
document: contexts_default[
|
|
5625
|
-
documentUrl:
|
|
5626
|
+
document: contexts_default[currentUrl],
|
|
5627
|
+
documentUrl: currentUrl
|
|
5626
5628
|
};
|
|
5627
5629
|
}
|
|
5628
5630
|
if (!allowPrivateAddress) try {
|
|
5629
|
-
await require_url.validatePublicUrl(
|
|
5631
|
+
await require_url.validatePublicUrl(currentUrl);
|
|
5630
5632
|
} catch (error) {
|
|
5631
5633
|
if (error instanceof require_url.UrlError) logger.error("Disallowed private URL: {url}", {
|
|
5632
|
-
url,
|
|
5634
|
+
url: currentUrl,
|
|
5633
5635
|
error
|
|
5634
5636
|
});
|
|
5635
5637
|
throw error;
|
|
5636
5638
|
}
|
|
5639
|
+
visited.add(currentUrl);
|
|
5637
5640
|
return await tracer.startActiveSpan("activitypub.fetch_document", {
|
|
5638
5641
|
kind: __opentelemetry_api.SpanKind.CLIENT,
|
|
5639
|
-
attributes: { "url.full":
|
|
5642
|
+
attributes: { "url.full": currentUrl }
|
|
5640
5643
|
}, async (span) => {
|
|
5641
5644
|
try {
|
|
5642
|
-
const request = require_request.createActivityPubRequest(
|
|
5645
|
+
const request = require_request.createActivityPubRequest(currentUrl, { userAgent });
|
|
5643
5646
|
require_request.logRequest(logger, request);
|
|
5644
5647
|
const response = await fetch(request, {
|
|
5645
5648
|
redirect: "manual",
|
|
@@ -5647,11 +5650,25 @@ function getDocumentLoader({ allowPrivateAddress, skipPreloadedContexts, userAge
|
|
|
5647
5650
|
});
|
|
5648
5651
|
span.setAttribute("http.response.status_code", response.status);
|
|
5649
5652
|
if (response.status >= 300 && response.status < 400 && response.headers.has("Location")) {
|
|
5650
|
-
|
|
5653
|
+
if (redirected >= DEFAULT_MAX_REDIRECTION) {
|
|
5654
|
+
logger.error("Too many redirections ({redirections}) while fetching document.", {
|
|
5655
|
+
redirections: redirected + 1,
|
|
5656
|
+
url: currentUrl
|
|
5657
|
+
});
|
|
5658
|
+
throw new require_request.FetchError(currentUrl, `Too many redirections (${redirected + 1})`);
|
|
5659
|
+
}
|
|
5660
|
+
const redirectUrl = new URL(response.headers.get("Location"), response.url === "" ? currentUrl : response.url).href;
|
|
5651
5661
|
span.setAttribute("http.redirect.url", redirectUrl);
|
|
5652
|
-
|
|
5662
|
+
if (visited.has(redirectUrl)) {
|
|
5663
|
+
logger.error("Detected a redirect loop while fetching document: {url} -> {redirectUrl}", {
|
|
5664
|
+
url: currentUrl,
|
|
5665
|
+
redirectUrl
|
|
5666
|
+
});
|
|
5667
|
+
throw new require_request.FetchError(currentUrl, `Redirect loop detected: ${redirectUrl}`);
|
|
5668
|
+
}
|
|
5669
|
+
return await load(redirectUrl, options, redirected + 1, visited);
|
|
5653
5670
|
}
|
|
5654
|
-
const result = await getRemoteDocument(
|
|
5671
|
+
const result = await getRemoteDocument(currentUrl, response, load);
|
|
5655
5672
|
span.setAttribute("docloader.document_url", result.documentUrl);
|
|
5656
5673
|
if (result.contextUrl != null) span.setAttribute("docloader.context_url", result.contextUrl);
|
|
5657
5674
|
return result;
|
|
@@ -5939,6 +5956,53 @@ function getDocumentLoader({ allowPrivateAddress, skipPreloadedContexts, userAge
|
|
|
5939
5956
|
(0, node_assert.deepStrictEqual)(await fetchDocumentLoader2("https://example.com/localhost-redirect"), expected);
|
|
5940
5957
|
(0, node_assert.deepStrictEqual)(await fetchDocumentLoader2("https://example.com/localhost-link"), expected);
|
|
5941
5958
|
});
|
|
5959
|
+
let redirectAttempts = 0;
|
|
5960
|
+
esm_default.get("begin:https://example.com/too-many-redirects/", (cl) => {
|
|
5961
|
+
redirectAttempts++;
|
|
5962
|
+
const index = Number(cl.url.split("/").at(-1));
|
|
5963
|
+
return {
|
|
5964
|
+
status: 302,
|
|
5965
|
+
headers: { Location: `https://example.com/too-many-redirects/${index + 1}` }
|
|
5966
|
+
};
|
|
5967
|
+
});
|
|
5968
|
+
await t.test("too many redirects", async () => {
|
|
5969
|
+
redirectAttempts = 0;
|
|
5970
|
+
await (0, node_assert.rejects)(() => fetchDocumentLoader("https://example.com/too-many-redirects/0"), require_request.FetchError, "Too many redirections");
|
|
5971
|
+
(0, node_assert.deepStrictEqual)(redirectAttempts, 21);
|
|
5972
|
+
});
|
|
5973
|
+
let loopAttempts = 0;
|
|
5974
|
+
esm_default.get("https://example.com/redirect-loop-a", () => {
|
|
5975
|
+
loopAttempts++;
|
|
5976
|
+
return {
|
|
5977
|
+
status: 302,
|
|
5978
|
+
headers: { Location: "https://example.com/redirect-loop-b" }
|
|
5979
|
+
};
|
|
5980
|
+
});
|
|
5981
|
+
esm_default.get("https://example.com/redirect-loop-b", () => {
|
|
5982
|
+
loopAttempts++;
|
|
5983
|
+
return {
|
|
5984
|
+
status: 302,
|
|
5985
|
+
headers: { Location: "https://example.com/redirect-loop-a" }
|
|
5986
|
+
};
|
|
5987
|
+
});
|
|
5988
|
+
await t.test("redirect loop", async () => {
|
|
5989
|
+
loopAttempts = 0;
|
|
5990
|
+
await (0, node_assert.rejects)(() => fetchDocumentLoader("https://example.com/redirect-loop-a"), require_request.FetchError, "Redirect loop detected");
|
|
5991
|
+
(0, node_assert.deepStrictEqual)(loopAttempts, 2);
|
|
5992
|
+
});
|
|
5993
|
+
let relativeLoopAttempts = 0;
|
|
5994
|
+
esm_default.get("https://example.com/redirect-loop-relative", () => {
|
|
5995
|
+
relativeLoopAttempts++;
|
|
5996
|
+
return {
|
|
5997
|
+
status: 302,
|
|
5998
|
+
headers: { Location: "/redirect-loop-relative" }
|
|
5999
|
+
};
|
|
6000
|
+
});
|
|
6001
|
+
await t.test("redirect loop with relative location", async () => {
|
|
6002
|
+
relativeLoopAttempts = 0;
|
|
6003
|
+
await (0, node_assert.rejects)(() => fetchDocumentLoader("https://example.com/redirect-loop-relative"), require_request.FetchError, "Redirect loop detected");
|
|
6004
|
+
(0, node_assert.deepStrictEqual)(relativeLoopAttempts, 1);
|
|
6005
|
+
});
|
|
5942
6006
|
const maliciousPayload = "<a" + " a=\"b\"".repeat(30) + " ";
|
|
5943
6007
|
esm_default.get("https://example.com/redos", {
|
|
5944
6008
|
body: maliciousPayload,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { FetchError, createActivityPubRequest, deno_default, logRequest } from "./request-
|
|
1
|
+
import { FetchError, createActivityPubRequest, deno_default, logRequest } from "./request-Cqx2eUpt.js";
|
|
2
2
|
import { HttpHeaderLink } from "./link-Ck2yj4dH.js";
|
|
3
3
|
import { UrlError, validatePublicUrl } from "./url-fW_DHbih.js";
|
|
4
4
|
import "node:module";
|
|
@@ -5523,6 +5523,7 @@ const logger = getLogger([
|
|
|
5523
5523
|
"runtime",
|
|
5524
5524
|
"docloader"
|
|
5525
5525
|
]);
|
|
5526
|
+
const DEFAULT_MAX_REDIRECTION = 20;
|
|
5526
5527
|
/**
|
|
5527
5528
|
* Gets a {@link RemoteDocument} from the given response.
|
|
5528
5529
|
* @param url The URL of the document to load.
|
|
@@ -5641,31 +5642,33 @@ async function getRemoteDocument(url, response, fetch$1) {
|
|
|
5641
5642
|
function getDocumentLoader({ allowPrivateAddress, skipPreloadedContexts, userAgent } = {}) {
|
|
5642
5643
|
const tracerProvider = trace.getTracerProvider();
|
|
5643
5644
|
const tracer = tracerProvider.getTracer(deno_default.name, deno_default.version);
|
|
5644
|
-
async function load(url, options) {
|
|
5645
|
+
async function load(url, options, redirected = 0, visited = /* @__PURE__ */ new Set()) {
|
|
5645
5646
|
options?.signal?.throwIfAborted();
|
|
5646
|
-
|
|
5647
|
-
|
|
5647
|
+
const currentUrl = new URL(url).href;
|
|
5648
|
+
if (!skipPreloadedContexts && currentUrl in contexts_default) {
|
|
5649
|
+
logger.debug("Using preloaded context: {url}.", { url: currentUrl });
|
|
5648
5650
|
return {
|
|
5649
5651
|
contextUrl: null,
|
|
5650
|
-
document: contexts_default[
|
|
5651
|
-
documentUrl:
|
|
5652
|
+
document: contexts_default[currentUrl],
|
|
5653
|
+
documentUrl: currentUrl
|
|
5652
5654
|
};
|
|
5653
5655
|
}
|
|
5654
5656
|
if (!allowPrivateAddress) try {
|
|
5655
|
-
await validatePublicUrl(
|
|
5657
|
+
await validatePublicUrl(currentUrl);
|
|
5656
5658
|
} catch (error) {
|
|
5657
5659
|
if (error instanceof UrlError) logger.error("Disallowed private URL: {url}", {
|
|
5658
|
-
url,
|
|
5660
|
+
url: currentUrl,
|
|
5659
5661
|
error
|
|
5660
5662
|
});
|
|
5661
5663
|
throw error;
|
|
5662
5664
|
}
|
|
5665
|
+
visited.add(currentUrl);
|
|
5663
5666
|
return await tracer.startActiveSpan("activitypub.fetch_document", {
|
|
5664
5667
|
kind: SpanKind.CLIENT,
|
|
5665
|
-
attributes: { "url.full":
|
|
5668
|
+
attributes: { "url.full": currentUrl }
|
|
5666
5669
|
}, async (span) => {
|
|
5667
5670
|
try {
|
|
5668
|
-
const request = createActivityPubRequest(
|
|
5671
|
+
const request = createActivityPubRequest(currentUrl, { userAgent });
|
|
5669
5672
|
logRequest(logger, request);
|
|
5670
5673
|
const response = await fetch(request, {
|
|
5671
5674
|
redirect: "manual",
|
|
@@ -5673,11 +5676,25 @@ function getDocumentLoader({ allowPrivateAddress, skipPreloadedContexts, userAge
|
|
|
5673
5676
|
});
|
|
5674
5677
|
span.setAttribute("http.response.status_code", response.status);
|
|
5675
5678
|
if (response.status >= 300 && response.status < 400 && response.headers.has("Location")) {
|
|
5676
|
-
|
|
5679
|
+
if (redirected >= DEFAULT_MAX_REDIRECTION) {
|
|
5680
|
+
logger.error("Too many redirections ({redirections}) while fetching document.", {
|
|
5681
|
+
redirections: redirected + 1,
|
|
5682
|
+
url: currentUrl
|
|
5683
|
+
});
|
|
5684
|
+
throw new FetchError(currentUrl, `Too many redirections (${redirected + 1})`);
|
|
5685
|
+
}
|
|
5686
|
+
const redirectUrl = new URL(response.headers.get("Location"), response.url === "" ? currentUrl : response.url).href;
|
|
5677
5687
|
span.setAttribute("http.redirect.url", redirectUrl);
|
|
5678
|
-
|
|
5688
|
+
if (visited.has(redirectUrl)) {
|
|
5689
|
+
logger.error("Detected a redirect loop while fetching document: {url} -> {redirectUrl}", {
|
|
5690
|
+
url: currentUrl,
|
|
5691
|
+
redirectUrl
|
|
5692
|
+
});
|
|
5693
|
+
throw new FetchError(currentUrl, `Redirect loop detected: ${redirectUrl}`);
|
|
5694
|
+
}
|
|
5695
|
+
return await load(redirectUrl, options, redirected + 1, visited);
|
|
5679
5696
|
}
|
|
5680
|
-
const result = await getRemoteDocument(
|
|
5697
|
+
const result = await getRemoteDocument(currentUrl, response, load);
|
|
5681
5698
|
span.setAttribute("docloader.document_url", result.documentUrl);
|
|
5682
5699
|
if (result.contextUrl != null) span.setAttribute("docloader.context_url", result.contextUrl);
|
|
5683
5700
|
return result;
|
|
@@ -5965,6 +5982,53 @@ test("getDocumentLoader()", async (t) => {
|
|
|
5965
5982
|
deepStrictEqual(await fetchDocumentLoader2("https://example.com/localhost-redirect"), expected);
|
|
5966
5983
|
deepStrictEqual(await fetchDocumentLoader2("https://example.com/localhost-link"), expected);
|
|
5967
5984
|
});
|
|
5985
|
+
let redirectAttempts = 0;
|
|
5986
|
+
esm_default.get("begin:https://example.com/too-many-redirects/", (cl) => {
|
|
5987
|
+
redirectAttempts++;
|
|
5988
|
+
const index = Number(cl.url.split("/").at(-1));
|
|
5989
|
+
return {
|
|
5990
|
+
status: 302,
|
|
5991
|
+
headers: { Location: `https://example.com/too-many-redirects/${index + 1}` }
|
|
5992
|
+
};
|
|
5993
|
+
});
|
|
5994
|
+
await t.test("too many redirects", async () => {
|
|
5995
|
+
redirectAttempts = 0;
|
|
5996
|
+
await rejects(() => fetchDocumentLoader("https://example.com/too-many-redirects/0"), FetchError, "Too many redirections");
|
|
5997
|
+
deepStrictEqual(redirectAttempts, 21);
|
|
5998
|
+
});
|
|
5999
|
+
let loopAttempts = 0;
|
|
6000
|
+
esm_default.get("https://example.com/redirect-loop-a", () => {
|
|
6001
|
+
loopAttempts++;
|
|
6002
|
+
return {
|
|
6003
|
+
status: 302,
|
|
6004
|
+
headers: { Location: "https://example.com/redirect-loop-b" }
|
|
6005
|
+
};
|
|
6006
|
+
});
|
|
6007
|
+
esm_default.get("https://example.com/redirect-loop-b", () => {
|
|
6008
|
+
loopAttempts++;
|
|
6009
|
+
return {
|
|
6010
|
+
status: 302,
|
|
6011
|
+
headers: { Location: "https://example.com/redirect-loop-a" }
|
|
6012
|
+
};
|
|
6013
|
+
});
|
|
6014
|
+
await t.test("redirect loop", async () => {
|
|
6015
|
+
loopAttempts = 0;
|
|
6016
|
+
await rejects(() => fetchDocumentLoader("https://example.com/redirect-loop-a"), FetchError, "Redirect loop detected");
|
|
6017
|
+
deepStrictEqual(loopAttempts, 2);
|
|
6018
|
+
});
|
|
6019
|
+
let relativeLoopAttempts = 0;
|
|
6020
|
+
esm_default.get("https://example.com/redirect-loop-relative", () => {
|
|
6021
|
+
relativeLoopAttempts++;
|
|
6022
|
+
return {
|
|
6023
|
+
status: 302,
|
|
6024
|
+
headers: { Location: "/redirect-loop-relative" }
|
|
6025
|
+
};
|
|
6026
|
+
});
|
|
6027
|
+
await t.test("redirect loop with relative location", async () => {
|
|
6028
|
+
relativeLoopAttempts = 0;
|
|
6029
|
+
await rejects(() => fetchDocumentLoader("https://example.com/redirect-loop-relative"), FetchError, "Redirect loop detected");
|
|
6030
|
+
deepStrictEqual(relativeLoopAttempts, 1);
|
|
6031
|
+
});
|
|
5968
6032
|
const maliciousPayload = "<a" + " a=\"b\"".repeat(30) + " ";
|
|
5969
6033
|
esm_default.get("https://example.com/redos", {
|
|
5970
6034
|
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-D2-F2dMS.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
|
@@ -361,6 +361,73 @@ test("getDocumentLoader()", async (t) => {
|
|
|
361
361
|
);
|
|
362
362
|
});
|
|
363
363
|
|
|
364
|
+
let redirectAttempts = 0;
|
|
365
|
+
fetchMock.get("begin:https://example.com/too-many-redirects/", (cl) => {
|
|
366
|
+
redirectAttempts++;
|
|
367
|
+
const index = Number(cl.url.split("/").at(-1));
|
|
368
|
+
return {
|
|
369
|
+
status: 302,
|
|
370
|
+
headers: {
|
|
371
|
+
Location: `https://example.com/too-many-redirects/${index + 1}`,
|
|
372
|
+
},
|
|
373
|
+
};
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
await t.test("too many redirects", async () => {
|
|
377
|
+
redirectAttempts = 0;
|
|
378
|
+
await rejects(
|
|
379
|
+
() => fetchDocumentLoader("https://example.com/too-many-redirects/0"),
|
|
380
|
+
FetchError,
|
|
381
|
+
"Too many redirections",
|
|
382
|
+
);
|
|
383
|
+
deepStrictEqual(redirectAttempts, 21);
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
let loopAttempts = 0;
|
|
387
|
+
fetchMock.get("https://example.com/redirect-loop-a", () => {
|
|
388
|
+
loopAttempts++;
|
|
389
|
+
return {
|
|
390
|
+
status: 302,
|
|
391
|
+
headers: { Location: "https://example.com/redirect-loop-b" },
|
|
392
|
+
};
|
|
393
|
+
});
|
|
394
|
+
fetchMock.get("https://example.com/redirect-loop-b", () => {
|
|
395
|
+
loopAttempts++;
|
|
396
|
+
return {
|
|
397
|
+
status: 302,
|
|
398
|
+
headers: { Location: "https://example.com/redirect-loop-a" },
|
|
399
|
+
};
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
await t.test("redirect loop", async () => {
|
|
403
|
+
loopAttempts = 0;
|
|
404
|
+
await rejects(
|
|
405
|
+
() => fetchDocumentLoader("https://example.com/redirect-loop-a"),
|
|
406
|
+
FetchError,
|
|
407
|
+
"Redirect loop detected",
|
|
408
|
+
);
|
|
409
|
+
deepStrictEqual(loopAttempts, 2);
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
let relativeLoopAttempts = 0;
|
|
413
|
+
fetchMock.get("https://example.com/redirect-loop-relative", () => {
|
|
414
|
+
relativeLoopAttempts++;
|
|
415
|
+
return {
|
|
416
|
+
status: 302,
|
|
417
|
+
headers: { Location: "/redirect-loop-relative" },
|
|
418
|
+
};
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
await t.test("redirect loop with relative location", async () => {
|
|
422
|
+
relativeLoopAttempts = 0;
|
|
423
|
+
await rejects(
|
|
424
|
+
() => fetchDocumentLoader("https://example.com/redirect-loop-relative"),
|
|
425
|
+
FetchError,
|
|
426
|
+
"Redirect loop detected",
|
|
427
|
+
);
|
|
428
|
+
deepStrictEqual(relativeLoopAttempts, 1);
|
|
429
|
+
});
|
|
430
|
+
|
|
364
431
|
// Regression test for ReDoS vulnerability (CVE-2025-68475)
|
|
365
432
|
// Malicious HTML payload: <a a="b" a="b" ... (unclosed tag)
|
|
366
433
|
// 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
|
|
@@ -292,38 +293,45 @@ export function getDocumentLoader(
|
|
|
292
293
|
async function load(
|
|
293
294
|
url: string,
|
|
294
295
|
options?: DocumentLoaderOptions,
|
|
296
|
+
redirected = 0,
|
|
297
|
+
visited = new Set<string>(),
|
|
295
298
|
): Promise<RemoteDocument> {
|
|
296
299
|
options?.signal?.throwIfAborted();
|
|
297
|
-
|
|
298
|
-
|
|
300
|
+
const currentUrl = new URL(url).href;
|
|
301
|
+
if (!skipPreloadedContexts && currentUrl in preloadedContexts) {
|
|
302
|
+
logger.debug("Using preloaded context: {url}.", { url: currentUrl });
|
|
299
303
|
return {
|
|
300
304
|
contextUrl: null,
|
|
301
|
-
document: preloadedContexts[
|
|
302
|
-
documentUrl:
|
|
305
|
+
document: preloadedContexts[currentUrl],
|
|
306
|
+
documentUrl: currentUrl,
|
|
303
307
|
};
|
|
304
308
|
}
|
|
305
309
|
if (!allowPrivateAddress) {
|
|
306
310
|
try {
|
|
307
|
-
await validatePublicUrl(
|
|
311
|
+
await validatePublicUrl(currentUrl);
|
|
308
312
|
} catch (error) {
|
|
309
313
|
if (error instanceof UrlError) {
|
|
310
|
-
logger.error("Disallowed private URL: {url}", {
|
|
314
|
+
logger.error("Disallowed private URL: {url}", {
|
|
315
|
+
url: currentUrl,
|
|
316
|
+
error,
|
|
317
|
+
});
|
|
311
318
|
}
|
|
312
319
|
throw error;
|
|
313
320
|
}
|
|
314
321
|
}
|
|
322
|
+
visited.add(currentUrl);
|
|
315
323
|
|
|
316
324
|
return await tracer.startActiveSpan(
|
|
317
325
|
"activitypub.fetch_document",
|
|
318
326
|
{
|
|
319
327
|
kind: SpanKind.CLIENT,
|
|
320
328
|
attributes: {
|
|
321
|
-
"url.full":
|
|
329
|
+
"url.full": currentUrl,
|
|
322
330
|
},
|
|
323
331
|
},
|
|
324
332
|
async (span) => {
|
|
325
333
|
try {
|
|
326
|
-
const request = createActivityPubRequest(
|
|
334
|
+
const request = createActivityPubRequest(currentUrl, { userAgent });
|
|
327
335
|
logRequest(logger, request);
|
|
328
336
|
const response = await fetch(request, {
|
|
329
337
|
// Since Bun has a bug that ignores the `Request.redirect` option,
|
|
@@ -339,12 +347,36 @@ export function getDocumentLoader(
|
|
|
339
347
|
response.status >= 300 && response.status < 400 &&
|
|
340
348
|
response.headers.has("Location")
|
|
341
349
|
) {
|
|
342
|
-
|
|
350
|
+
if (redirected >= DEFAULT_MAX_REDIRECTION) {
|
|
351
|
+
logger.error(
|
|
352
|
+
"Too many redirections ({redirections}) while fetching document.",
|
|
353
|
+
{ redirections: redirected + 1, url: currentUrl },
|
|
354
|
+
);
|
|
355
|
+
throw new FetchError(
|
|
356
|
+
currentUrl,
|
|
357
|
+
`Too many redirections (${redirected + 1})`,
|
|
358
|
+
);
|
|
359
|
+
}
|
|
360
|
+
const redirectUrl = new URL(
|
|
361
|
+
response.headers.get("Location")!,
|
|
362
|
+
response.url === "" ? currentUrl : response.url,
|
|
363
|
+
).href;
|
|
343
364
|
span.setAttribute("http.redirect.url", redirectUrl);
|
|
344
|
-
|
|
365
|
+
if (visited.has(redirectUrl)) {
|
|
366
|
+
logger.error(
|
|
367
|
+
"Detected a redirect loop while fetching document: {url} -> " +
|
|
368
|
+
"{redirectUrl}",
|
|
369
|
+
{ url: currentUrl, redirectUrl },
|
|
370
|
+
);
|
|
371
|
+
throw new FetchError(
|
|
372
|
+
currentUrl,
|
|
373
|
+
`Redirect loop detected: ${redirectUrl}`,
|
|
374
|
+
);
|
|
375
|
+
}
|
|
376
|
+
return await load(redirectUrl, options, redirected + 1, visited);
|
|
345
377
|
}
|
|
346
378
|
|
|
347
|
-
const result = await getRemoteDocument(
|
|
379
|
+
const result = await getRemoteDocument(currentUrl, response, load);
|
|
348
380
|
span.setAttribute("docloader.document_url", result.documentUrl);
|
|
349
381
|
if (result.contextUrl != null) {
|
|
350
382
|
span.setAttribute("docloader.context_url", result.contextUrl);
|