From 98f3f553715fa81dab1e01dc57d1b7905c6f6a19 Mon Sep 17 00:00:00 2001 From: limepotato Date: Mon, 17 Jun 2024 13:02:29 -0600 Subject: [PATCH] withdrawal replacements patch --- .../backend/src/queue/processors/deliver.ts | 30 +++++++++- .../src/remote/activitypub/check-fetch.ts | 26 ++++---- .../src/remote/activitypub/renderer/note.ts | 24 +++++++- packages/backend/src/server/activitypub.ts | 59 +++++++++---------- .../src/server/activitypub/featured.ts | 6 +- .../src/server/activitypub/followers.ts | 6 +- .../src/server/activitypub/following.ts | 6 +- .../backend/src/server/activitypub/outbox.ts | 15 +++-- 8 files changed, 110 insertions(+), 62 deletions(-) diff --git a/packages/backend/src/queue/processors/deliver.ts b/packages/backend/src/queue/processors/deliver.ts index 65471a559..74dcc3abf 100644 --- a/packages/backend/src/queue/processors/deliver.ts +++ b/packages/backend/src/queue/processors/deliver.ts @@ -14,6 +14,7 @@ import { StatusError } from "@/misc/fetch.js"; import { shouldSkipInstance } from "@/misc/skipped-instances.js"; import type { DeliverJobData } from "@/queue/types.js"; import type Bull from "bull"; +import { patchText, shouldPatchText } from "@/remote/activitypub/renderer/note.js"; const logger = new Logger("deliver"); @@ -30,10 +31,33 @@ export default async (job: Bull.Job) => { logger.debug(`delivering ${latest}`); } + let i = undefined; + + if ( + ["Create", "Update"].includes(job.data.content.type) + && job.data.content.object.type === "Note" + ) { + const obj = job.data.content.object; + const patchSrcContent = shouldPatchText(obj.source.content); + if (patchSrcContent) { + i = await registerOrFetchInstanceDoc(host); + if (shouldPatchText(obj.content)) + obj.content = patchText(obj.content, i); + if (shouldPatchText(obj._misskey_content)) + obj._misskey_content = patchText(obj._misskey_content, i); + if (patchSrcContent) + obj.source.content = patchText(obj.source.content, i); + } + } + await request(job.data.user, job.data.to, job.data.content); - // Update stats - registerOrFetchInstanceDoc(host).then((i) => { + (async () => { + if (i === undefined) { + i = await registerOrFetchInstanceDoc(host); + } + + // Update stats Instances.update(i.id, { latestRequestSentAt: new Date(), latestStatus: 200, @@ -46,7 +70,7 @@ export default async (job: Bull.Job) => { instanceChart.requestSent(i.host, true); apRequestChart.deliverSucc(); federationChart.deliverd(i.host, true); - }); + })(); return "Success"; } catch (res) { diff --git a/packages/backend/src/remote/activitypub/check-fetch.ts b/packages/backend/src/remote/activitypub/check-fetch.ts index b7a4b4742..f299a1624 100644 --- a/packages/backend/src/remote/activitypub/check-fetch.ts +++ b/packages/backend/src/remote/activitypub/check-fetch.ts @@ -28,24 +28,24 @@ export async function hasSignature(req: IncomingMessage): Promise { return required ? "supplied" : "unneeded"; } -export async function checkFetch(req: IncomingMessage): Promise { +export async function checkFetch(req: IncomingMessage): Promise<{ status: number; host?: string }> { const meta = await fetchMeta(); if (meta.secureMode || meta.privateMode) { - if (req.headers.host !== config.host) return 400; + if (req.headers.host !== config.host) return { status: 400 }; let signature; try { signature = httpSignature.parseRequest(req, { headers: ["(request-target)", "host", "date"] }); } catch (e) { - return 401; + return { status: 401 }; } const keyId = new URL(signature.keyId); const host = toPuny(keyId.hostname); if (await shouldBlockInstance(host, meta)) { - return 403; + return { status: 403 }; } if ( @@ -54,13 +54,13 @@ export async function checkFetch(req: IncomingMessage): Promise { host !== config.domain && !meta.allowedHosts.includes(host) ) { - return 403; + return { status: 403 }; } const keyIdLower = signature.keyId.toLowerCase(); if (keyIdLower.startsWith("acct:")) { // Old keyId is no longer supported. - return 401; + return { status: 401 }; } const dbResolver = new DbResolver(); @@ -77,23 +77,23 @@ export async function checkFetch(req: IncomingMessage): Promise { ); } catch (e) { // できなければ駄目 - return 403; + return { status: 403 }; } } // publicKey がなくても終了 if (authUser?.key == null) { - return 403; + return { status: 403 }; } // Cannot authenticate against local user if (authUser.user.uri === null || authUser.user.host === null) { - return 400; + return { status: 400 }; } // Check if keyId hostname matches actor hostname if (toPuny(new URL(authUser.user.uri).hostname) !== host) { - return 403; + return { status: 403 }; } // HTTP-Signatureの検証 @@ -107,7 +107,7 @@ export async function checkFetch(req: IncomingMessage): Promise { authUser.key = await dbResolver.refetchPublicKeyForApId(authUser.user); if (authUser.key == null) { - return 403; + return { status: 403 }; } httpSignatureValidated = httpSignature.verifySignature( @@ -117,12 +117,12 @@ export async function checkFetch(req: IncomingMessage): Promise { } if (!httpSignatureValidated) { - return 403; + return { status: 403 }; } return verifySignature(signature, authUser.key) ? 200 : 401; } - return 200; + return { status: 200 }; } export async function getSignatureUser(req: IncomingMessage): Promise<{ diff --git a/packages/backend/src/remote/activitypub/renderer/note.ts b/packages/backend/src/remote/activitypub/renderer/note.ts index f6ea3c892..1820b3133 100644 --- a/packages/backend/src/remote/activitypub/renderer/note.ts +++ b/packages/backend/src/remote/activitypub/renderer/note.ts @@ -10,11 +10,14 @@ import renderEmoji from "./emoji.js"; import renderMention from "./mention.js"; import renderHashtag from "./hashtag.js"; import renderDocument from "./document.js"; +import { Instances } from "@/models/index.js"; +import { Instance } from "@/models/entities/instance.js"; export default async function renderNote( note: Note, dive = true, isTalk = false, + clientHost: string | undefined = undefined, ): Promise> { const getPromisedFiles = async (ids: string[]) => { if (!ids || ids.length === 0) return []; @@ -93,13 +96,17 @@ export default async function renderNote( const files = await getPromisedFiles(note.fileIds); - const text = note.text ?? ""; + let text = note.text ?? ""; let poll: Poll | null = null; if (note.hasPoll) { poll = await Polls.findOneBy({ noteId: note.id }); } + if (clientHost && shouldPatchText(text)) { + text = patchText(text, await Instances.findOneBy({ host: clientHost })); + } + let apText = text; if (quote) { @@ -186,3 +193,18 @@ export async function getEmojis(names: string[]): Promise { return emojis.filter((emoji) => emoji != null) as Emoji[]; } +export function shouldPatchText(text: string): boolean { + return text.match(/\$INSTANCE\$(?:host|softwareName|softwareVersion|name|description)\$/) !== null; +} + +export function patchText( + text: string, + target: Instance +): string { + text = text.replaceAll("$INSTANCE$host$", target.host); + text = text.replaceAll("$INSTANCE$softwareName$", target.softwareName || "softwareName"); + text = text.replaceAll("$INSTANCE$softwareVersion$", target.softwareVersion || "softwareVersion"); + text = text.replaceAll("$INSTANCE$name$", target.name || "name"); + text = text.replaceAll("$INSTANCE$description$", target.description || "description"); + return text; +} diff --git a/packages/backend/src/server/activitypub.ts b/packages/backend/src/server/activitypub.ts index 4c22a1cbe..347660a70 100644 --- a/packages/backend/src/server/activitypub.ts +++ b/packages/backend/src/server/activitypub.ts @@ -110,9 +110,9 @@ router.post("/users/:user/inbox", parseJsonBodyOrFail, inbox); router.get("/notes/:note", async (ctx, next) => { if (!isActivityPubReq(ctx)) return await next(); - const verify = await checkFetch(ctx.req); - if (verify !== 200) { - ctx.status = verify; + const { status, host } = await checkFetch(ctx.req); + if (status !== 200) { + ctx.status = status; return; } @@ -168,7 +168,7 @@ router.get("/notes/:note", async (ctx, next) => { serverLogger.debug("Accepting: access criteria met"); } - ctx.body = renderActivity(await renderNote(note, false)); + ctx.body = renderActivity(await renderNote(note, false, false, host)); const meta = await fetchMeta(); if (meta.secureMode || meta.privateMode) { @@ -181,9 +181,9 @@ router.get("/notes/:note", async (ctx, next) => { // note activity router.get("/notes/:note/activity", async (ctx) => { - const verify = await checkFetch(ctx.req); - if (verify !== 200) { - ctx.status = verify; + const { status, host } = await checkFetch(ctx.req); + if (status !== 200) { + ctx.status = status; return; } @@ -199,7 +199,7 @@ router.get("/notes/:note/activity", async (ctx) => { return; } - ctx.body = renderActivity(await packActivity(note)); + ctx.body = renderActivity(await packActivity(note, host)); const meta = await fetchMeta(); if (meta.secureMode || meta.privateMode) { ctx.set("Cache-Control", "private, max-age=0, must-revalidate"); @@ -233,9 +233,9 @@ router.get("/users/:user/publickey", async (ctx) => { return; } - const verify = await checkFetch(ctx.req); - if (verify !== 200) { - ctx.status = verify; + const { status } = await checkFetch(ctx.req); + if (status !== 200) { + ctx.status = status; return; } @@ -293,10 +293,9 @@ router.get("/users/:user", async (ctx, next) => { return; } - const verify = await checkFetch(ctx.req); - if (verify !== 200) { - ctx.status = verify; - return; + const { status } = await checkFetch(ctx.req); + if (status !== 200) { + ctx.status = status; } const userId = ctx.params.user; @@ -319,9 +318,9 @@ router.get("/@:user", async (ctx, next) => { return; } - const verify = await checkFetch(ctx.req); - if (verify !== 200) { - ctx.status = verify; + const { status } = await checkFetch(ctx.req); + if (status !== 200) { + ctx.status = status; return; } @@ -342,9 +341,9 @@ router.get("/actor", async (ctx, next) => { // emoji router.get("/emojis/:emoji", async (ctx) => { - const verify = await checkFetch(ctx.req); - if (verify !== 200) { - ctx.status = verify; + const { status } = await checkFetch(ctx.req); + if (status !== 200) { + ctx.status = status; return; } @@ -370,9 +369,9 @@ router.get("/emojis/:emoji", async (ctx) => { // like router.get("/likes/:like", async (ctx) => { - const verify = await checkFetch(ctx.req); - if (verify !== 200) { - ctx.status = verify; + const { status } = await checkFetch(ctx.req); + if (status !== 200) { + ctx.status = status; return; } @@ -404,9 +403,9 @@ router.get("/likes/:like", async (ctx) => { router.get( "/follows/:follower/:followee", async (ctx: Router.RouterContext) => { - const verify = await checkFetch(ctx.req); - if (verify !== 200) { - ctx.status = verify; + const { status } = await checkFetch(ctx.req); + if (status !== 200) { + ctx.status = status; return; } // This may be used before the follow is completed, so we do not @@ -441,9 +440,9 @@ router.get( // follow request router.get("/follows/:followRequestId", async (ctx: Router.RouterContext) => { - const verify = await checkFetch(ctx.req); - if (verify !== 200) { - ctx.status = verify; + const { status } = await checkFetch(ctx.req); + if (status !== 200) { + ctx.status = status return; } diff --git a/packages/backend/src/server/activitypub/featured.ts b/packages/backend/src/server/activitypub/featured.ts index 82bb19fa1..91bad07b2 100644 --- a/packages/backend/src/server/activitypub/featured.ts +++ b/packages/backend/src/server/activitypub/featured.ts @@ -10,9 +10,9 @@ import { setResponseType } from "../activitypub.js"; import type Router from "@koa/router"; export default async (ctx: Router.RouterContext) => { - const verify = await checkFetch(ctx.req); - if (verify !== 200) { - ctx.status = verify; + const { status } = await checkFetch(ctx.req); + if (status !== 200) { + ctx.status = status; return; } diff --git a/packages/backend/src/server/activitypub/followers.ts b/packages/backend/src/server/activitypub/followers.ts index 146ca5192..e3b92b315 100644 --- a/packages/backend/src/server/activitypub/followers.ts +++ b/packages/backend/src/server/activitypub/followers.ts @@ -14,9 +14,9 @@ import type { FindOptionsWhere } from "typeorm"; import type Router from "@koa/router"; export default async (ctx: Router.RouterContext) => { - const verify = await checkFetch(ctx.req); - if (verify !== 200) { - ctx.status = verify; + const { status } = await checkFetch(ctx.req); + if (status !== 200) { + ctx.status = status; return; } diff --git a/packages/backend/src/server/activitypub/following.ts b/packages/backend/src/server/activitypub/following.ts index eab513ce6..6fd4d6028 100644 --- a/packages/backend/src/server/activitypub/following.ts +++ b/packages/backend/src/server/activitypub/following.ts @@ -14,9 +14,9 @@ import type { FindOptionsWhere } from "typeorm"; import type Router from "@koa/router"; export default async (ctx: Router.RouterContext) => { - const verify = await checkFetch(ctx.req); - if (verify !== 200) { - ctx.status = verify; + const { status } = await checkFetch(ctx.req); + if (status !== 200) { + ctx.status = status; return; } diff --git a/packages/backend/src/server/activitypub/outbox.ts b/packages/backend/src/server/activitypub/outbox.ts index adc06798a..71d607d24 100644 --- a/packages/backend/src/server/activitypub/outbox.ts +++ b/packages/backend/src/server/activitypub/outbox.ts @@ -17,9 +17,9 @@ import { setResponseType } from "../activitypub.js"; import type Router from "@koa/router"; export default async (ctx: Router.RouterContext) => { - const verify = await checkFetch(ctx.req); - if (verify !== 200) { - ctx.status = verify; + const { status, host } = await checkFetch(ctx.req); + if (status !== 200) { + ctx.status = status; return; } @@ -78,7 +78,7 @@ export default async (ctx: Router.RouterContext) => { if (sinceId) notes.reverse(); const activities = await Promise.all( - notes.map((note) => packActivity(note)), + notes.map((note) => packActivity(note, host)), ); const rendered = renderOrderedCollectionPage( `${partOf}?${url.query({ @@ -129,7 +129,10 @@ export default async (ctx: Router.RouterContext) => { * Pack Create or Announce Activity * @param note Note */ -export async function packActivity(note: Note): Promise { +export async function packActivity( + note: Note, + clientHost: string | undefined = undefined + ): Promise { if ( note.renoteId && note.text == null && @@ -144,5 +147,5 @@ export async function packActivity(note: Note): Promise { ); } - return renderCreate(await renderNote(note, false), note); + return renderCreate(await renderNote(note, false, false, clientHost), note); }