diff --git a/packages/backend/src/misc/fetch.ts b/packages/backend/src/misc/fetch.ts index 36027de50..d94c1f9a1 100644 --- a/packages/backend/src/misc/fetch.ts +++ b/packages/backend/src/misc/fetch.ts @@ -2,7 +2,7 @@ import * as http from "node:http"; import * as https from "node:https"; import type { URL } from "node:url"; import CacheableLookup from "cacheable-lookup"; -import fetch from "node-fetch"; +import fetch, { RequestRedirect } from "node-fetch"; import { HttpProxyAgent, HttpsProxyAgent } from "hpagent"; import config from "@/config/index.js"; import { isValidUrl } from "./is-valid-url.js"; @@ -58,6 +58,7 @@ export async function getResponse(args: { headers: Record; timeout?: number; size?: number; + redirect?: RequestRedirect; }) { if (!isValidUrl(args.url)) { throw new StatusError("Invalid URL", 400); @@ -78,8 +79,13 @@ export async function getResponse(args: { size: args.size || 10 * 1024 * 1024, agent: getAgentByUrl, signal: controller.signal, + redirect: args.redirect, }); + if (args.redirect === "manual" && [301, 302, 307, 308].includes(res.status)) { + return res; + } + if (!res.ok) { throw new StatusError( `${res.status} ${res.statusText}`, diff --git a/packages/backend/src/remote/activitypub/request.ts b/packages/backend/src/remote/activitypub/request.ts index 07ccbf4e8..3dbad8a97 100644 --- a/packages/backend/src/remote/activitypub/request.ts +++ b/packages/backend/src/remote/activitypub/request.ts @@ -6,6 +6,7 @@ import { createSignedPost, createSignedGet } from "./ap-request.js"; import type { Response } from "node-fetch"; import type { IObject } from "./type.js"; import { isValidUrl } from "@/misc/is-valid-url.js"; +import { apLogger } from "@/remote/activitypub/logger.js"; export default async (user: { id: User["id"] }, url: string, object: any) => { const body = JSON.stringify(object); @@ -34,10 +35,15 @@ export default async (user: { id: User["id"] }, url: string, object: any) => { /** * Get ActivityPub object - * @param user http-signature user * @param url URL to fetch + * @param user http-signature user + * @param redirects whether or not to accept redirects */ -export async function apGet(url: string, user?: ILocalUser): Promise { +export async function apGet( + url: string, + user?: ILocalUser, + redirects: boolean = true +): Promise { if (!isValidUrl(url)) { throw new StatusError("Invalid URL", 400); } @@ -61,7 +67,15 @@ export async function apGet(url: string, user?: ILocalUser): Promise { url, method: req.request.method, headers: req.request.headers, + redirect: redirects ? "manual" : "error", }); + + if (redirects && [301, 302, 307, 308].includes(res.status)) { + const newUrl = res.headers.get("location"); + if (newUrl == null) throw new Error("apGet got redirect but no target location"); + apLogger.debug(`apGet is redirecting to ${newUrl}`); + return apGet(newUrl, user, false); + } } else { res = await getResponse({ url, @@ -71,12 +85,20 @@ export async function apGet(url: string, user?: ILocalUser): Promise { 'application/activity+json, application/ld+json; profile="https://www.w3.org/ns/activitystreams"', "User-Agent": config.userAgent, }, + redirect: redirects ? "manual" : "error", }); + + if (redirects && [301, 302, 307, 308].includes(res.status)) { + const newUrl = res.headers.get("location"); + if (newUrl == null) throw new Error("apGet got redirect but no target location"); + apLogger.debug(`apGet is redirecting to ${newUrl}`); + return apGet(newUrl, undefined, false); + } } const contentType = res.headers.get("content-type"); if (contentType == null || !validateContentType(contentType)) { - throw new Error("Invalid Content Type"); + throw new Error(`apGet response had unexpected content-type: ${contentType}`); } if (res.body == null) throw new Error("body is null");