From 0f5dec02076841fa05ef69d0eb0ca7b986e7b0a3 Mon Sep 17 00:00:00 2001 From: Laura Hausmann Date: Wed, 27 Mar 2024 07:07:19 +0900 Subject: [PATCH] feat (backend): permit redirects for AP object lookups https://iceshrimp.dev/iceshrimp/iceshrimp/commit/8d7d95fd2344ffe2954821af167305f5231f3b28 Co-authored-by: naskya --- src/misc/fetch.ts | 8 +++++++- src/remote/activitypub/request.ts | 28 +++++++++++++++++++++++++--- 2 files changed, 32 insertions(+), 4 deletions(-) diff --git a/src/misc/fetch.ts b/src/misc/fetch.ts index 916e03652..e4c7adbca 100644 --- a/src/misc/fetch.ts +++ b/src/misc/fetch.ts @@ -1,7 +1,7 @@ import * as http from 'http'; import * as https from 'https'; 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'; import { URL } from 'url'; @@ -42,6 +42,7 @@ export async function getResponse(args: { headers: Record; timeout?: number; size?: number; + redirect?: RequestRedirect; }) { if (!isValidUrl(args.url)) { throw new StatusError('Invalid URL', 400); @@ -62,8 +63,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}`, res.status, res.statusText); } diff --git a/src/remote/activitypub/request.ts b/src/remote/activitypub/request.ts index acd83ec92..ac29d7faf 100644 --- a/src/remote/activitypub/request.ts +++ b/src/remote/activitypub/request.ts @@ -7,6 +7,7 @@ import { createSignedPost, createSignedGet } from './ap-request'; import type { Response } from 'node-fetch'; import { IObject } from './type'; import { isValidUrl } from '../../misc/is-valid-url'; +import { apLogger } from './logger'; export default async (user: ILocalUser, url: string, object: any) => { const body = JSON.stringify(object); @@ -37,8 +38,9 @@ export default async (user: ILocalUser, 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 signedGet(url: string, user: ILocalUser) { const keypair = await UserKeypairs.findOne({ @@ -65,7 +67,11 @@ export async function signedGet(url: string, user: ILocalUser) { return await res.json(); } -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); } @@ -91,7 +97,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, @@ -101,12 +115,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");