withdrawal replacements patch

This commit is contained in:
nelle 2024-06-17 13:02:29 -06:00
parent 50eb66e375
commit 98f3f55371
8 changed files with 110 additions and 62 deletions

View file

@ -14,6 +14,7 @@ import { StatusError } from "@/misc/fetch.js";
import { shouldSkipInstance } from "@/misc/skipped-instances.js"; import { shouldSkipInstance } from "@/misc/skipped-instances.js";
import type { DeliverJobData } from "@/queue/types.js"; import type { DeliverJobData } from "@/queue/types.js";
import type Bull from "bull"; import type Bull from "bull";
import { patchText, shouldPatchText } from "@/remote/activitypub/renderer/note.js";
const logger = new Logger("deliver"); const logger = new Logger("deliver");
@ -30,10 +31,33 @@ export default async (job: Bull.Job<DeliverJobData>) => {
logger.debug(`delivering ${latest}`); 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); await request(job.data.user, job.data.to, job.data.content);
(async () => {
if (i === undefined) {
i = await registerOrFetchInstanceDoc(host);
}
// Update stats // Update stats
registerOrFetchInstanceDoc(host).then((i) => {
Instances.update(i.id, { Instances.update(i.id, {
latestRequestSentAt: new Date(), latestRequestSentAt: new Date(),
latestStatus: 200, latestStatus: 200,
@ -46,7 +70,7 @@ export default async (job: Bull.Job<DeliverJobData>) => {
instanceChart.requestSent(i.host, true); instanceChart.requestSent(i.host, true);
apRequestChart.deliverSucc(); apRequestChart.deliverSucc();
federationChart.deliverd(i.host, true); federationChart.deliverd(i.host, true);
}); })();
return "Success"; return "Success";
} catch (res) { } catch (res) {

View file

@ -28,24 +28,24 @@ export async function hasSignature(req: IncomingMessage): Promise<string> {
return required ? "supplied" : "unneeded"; return required ? "supplied" : "unneeded";
} }
export async function checkFetch(req: IncomingMessage): Promise<number> { export async function checkFetch(req: IncomingMessage): Promise<{ status: number; host?: string }> {
const meta = await fetchMeta(); const meta = await fetchMeta();
if (meta.secureMode || meta.privateMode) { if (meta.secureMode || meta.privateMode) {
if (req.headers.host !== config.host) return 400; if (req.headers.host !== config.host) return { status: 400 };
let signature; let signature;
try { try {
signature = httpSignature.parseRequest(req, { headers: ["(request-target)", "host", "date"] }); signature = httpSignature.parseRequest(req, { headers: ["(request-target)", "host", "date"] });
} catch (e) { } catch (e) {
return 401; return { status: 401 };
} }
const keyId = new URL(signature.keyId); const keyId = new URL(signature.keyId);
const host = toPuny(keyId.hostname); const host = toPuny(keyId.hostname);
if (await shouldBlockInstance(host, meta)) { if (await shouldBlockInstance(host, meta)) {
return 403; return { status: 403 };
} }
if ( if (
@ -54,13 +54,13 @@ export async function checkFetch(req: IncomingMessage): Promise<number> {
host !== config.domain && host !== config.domain &&
!meta.allowedHosts.includes(host) !meta.allowedHosts.includes(host)
) { ) {
return 403; return { status: 403 };
} }
const keyIdLower = signature.keyId.toLowerCase(); const keyIdLower = signature.keyId.toLowerCase();
if (keyIdLower.startsWith("acct:")) { if (keyIdLower.startsWith("acct:")) {
// Old keyId is no longer supported. // Old keyId is no longer supported.
return 401; return { status: 401 };
} }
const dbResolver = new DbResolver(); const dbResolver = new DbResolver();
@ -77,23 +77,23 @@ export async function checkFetch(req: IncomingMessage): Promise<number> {
); );
} catch (e) { } catch (e) {
// できなければ駄目 // できなければ駄目
return 403; return { status: 403 };
} }
} }
// publicKey がなくても終了 // publicKey がなくても終了
if (authUser?.key == null) { if (authUser?.key == null) {
return 403; return { status: 403 };
} }
// Cannot authenticate against local user // Cannot authenticate against local user
if (authUser.user.uri === null || authUser.user.host === null) { if (authUser.user.uri === null || authUser.user.host === null) {
return 400; return { status: 400 };
} }
// Check if keyId hostname matches actor hostname // Check if keyId hostname matches actor hostname
if (toPuny(new URL(authUser.user.uri).hostname) !== host) { if (toPuny(new URL(authUser.user.uri).hostname) !== host) {
return 403; return { status: 403 };
} }
// HTTP-Signatureの検証 // HTTP-Signatureの検証
@ -107,7 +107,7 @@ export async function checkFetch(req: IncomingMessage): Promise<number> {
authUser.key = await dbResolver.refetchPublicKeyForApId(authUser.user); authUser.key = await dbResolver.refetchPublicKeyForApId(authUser.user);
if (authUser.key == null) { if (authUser.key == null) {
return 403; return { status: 403 };
} }
httpSignatureValidated = httpSignature.verifySignature( httpSignatureValidated = httpSignature.verifySignature(
@ -117,12 +117,12 @@ export async function checkFetch(req: IncomingMessage): Promise<number> {
} }
if (!httpSignatureValidated) { if (!httpSignatureValidated) {
return 403; return { status: 403 };
} }
return verifySignature(signature, authUser.key) ? 200 : 401; return verifySignature(signature, authUser.key) ? 200 : 401;
} }
return 200; return { status: 200 };
} }
export async function getSignatureUser(req: IncomingMessage): Promise<{ export async function getSignatureUser(req: IncomingMessage): Promise<{

View file

@ -10,11 +10,14 @@ import renderEmoji from "./emoji.js";
import renderMention from "./mention.js"; import renderMention from "./mention.js";
import renderHashtag from "./hashtag.js"; import renderHashtag from "./hashtag.js";
import renderDocument from "./document.js"; import renderDocument from "./document.js";
import { Instances } from "@/models/index.js";
import { Instance } from "@/models/entities/instance.js";
export default async function renderNote( export default async function renderNote(
note: Note, note: Note,
dive = true, dive = true,
isTalk = false, isTalk = false,
clientHost: string | undefined = undefined,
): Promise<Record<string, unknown>> { ): Promise<Record<string, unknown>> {
const getPromisedFiles = async (ids: string[]) => { const getPromisedFiles = async (ids: string[]) => {
if (!ids || ids.length === 0) return []; if (!ids || ids.length === 0) return [];
@ -93,13 +96,17 @@ export default async function renderNote(
const files = await getPromisedFiles(note.fileIds); const files = await getPromisedFiles(note.fileIds);
const text = note.text ?? ""; let text = note.text ?? "";
let poll: Poll | null = null; let poll: Poll | null = null;
if (note.hasPoll) { if (note.hasPoll) {
poll = await Polls.findOneBy({ noteId: note.id }); poll = await Polls.findOneBy({ noteId: note.id });
} }
if (clientHost && shouldPatchText(text)) {
text = patchText(text, await Instances.findOneBy({ host: clientHost }));
}
let apText = text; let apText = text;
if (quote) { if (quote) {
@ -186,3 +193,18 @@ export async function getEmojis(names: string[]): Promise<Emoji[]> {
return emojis.filter((emoji) => emoji != null) as Emoji[]; 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;
}

View file

@ -110,9 +110,9 @@ router.post("/users/:user/inbox", parseJsonBodyOrFail, inbox);
router.get("/notes/:note", async (ctx, next) => { router.get("/notes/:note", async (ctx, next) => {
if (!isActivityPubReq(ctx)) return await next(); if (!isActivityPubReq(ctx)) return await next();
const verify = await checkFetch(ctx.req); const { status, host } = await checkFetch(ctx.req);
if (verify !== 200) { if (status !== 200) {
ctx.status = verify; ctx.status = status;
return; return;
} }
@ -168,7 +168,7 @@ router.get("/notes/:note", async (ctx, next) => {
serverLogger.debug("Accepting: access criteria met"); 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(); const meta = await fetchMeta();
if (meta.secureMode || meta.privateMode) { if (meta.secureMode || meta.privateMode) {
@ -181,9 +181,9 @@ router.get("/notes/:note", async (ctx, next) => {
// note activity // note activity
router.get("/notes/:note/activity", async (ctx) => { router.get("/notes/:note/activity", async (ctx) => {
const verify = await checkFetch(ctx.req); const { status, host } = await checkFetch(ctx.req);
if (verify !== 200) { if (status !== 200) {
ctx.status = verify; ctx.status = status;
return; return;
} }
@ -199,7 +199,7 @@ router.get("/notes/:note/activity", async (ctx) => {
return; return;
} }
ctx.body = renderActivity(await packActivity(note)); ctx.body = renderActivity(await packActivity(note, host));
const meta = await fetchMeta(); const meta = await fetchMeta();
if (meta.secureMode || meta.privateMode) { if (meta.secureMode || meta.privateMode) {
ctx.set("Cache-Control", "private, max-age=0, must-revalidate"); ctx.set("Cache-Control", "private, max-age=0, must-revalidate");
@ -233,9 +233,9 @@ router.get("/users/:user/publickey", async (ctx) => {
return; return;
} }
const verify = await checkFetch(ctx.req); const { status } = await checkFetch(ctx.req);
if (verify !== 200) { if (status !== 200) {
ctx.status = verify; ctx.status = status;
return; return;
} }
@ -293,10 +293,9 @@ router.get("/users/:user", async (ctx, next) => {
return; return;
} }
const verify = await checkFetch(ctx.req); const { status } = await checkFetch(ctx.req);
if (verify !== 200) { if (status !== 200) {
ctx.status = verify; ctx.status = status;
return;
} }
const userId = ctx.params.user; const userId = ctx.params.user;
@ -319,9 +318,9 @@ router.get("/@:user", async (ctx, next) => {
return; return;
} }
const verify = await checkFetch(ctx.req); const { status } = await checkFetch(ctx.req);
if (verify !== 200) { if (status !== 200) {
ctx.status = verify; ctx.status = status;
return; return;
} }
@ -342,9 +341,9 @@ router.get("/actor", async (ctx, next) => {
// emoji // emoji
router.get("/emojis/:emoji", async (ctx) => { router.get("/emojis/:emoji", async (ctx) => {
const verify = await checkFetch(ctx.req); const { status } = await checkFetch(ctx.req);
if (verify !== 200) { if (status !== 200) {
ctx.status = verify; ctx.status = status;
return; return;
} }
@ -370,9 +369,9 @@ router.get("/emojis/:emoji", async (ctx) => {
// like // like
router.get("/likes/:like", async (ctx) => { router.get("/likes/:like", async (ctx) => {
const verify = await checkFetch(ctx.req); const { status } = await checkFetch(ctx.req);
if (verify !== 200) { if (status !== 200) {
ctx.status = verify; ctx.status = status;
return; return;
} }
@ -404,9 +403,9 @@ router.get("/likes/:like", async (ctx) => {
router.get( router.get(
"/follows/:follower/:followee", "/follows/:follower/:followee",
async (ctx: Router.RouterContext) => { async (ctx: Router.RouterContext) => {
const verify = await checkFetch(ctx.req); const { status } = await checkFetch(ctx.req);
if (verify !== 200) { if (status !== 200) {
ctx.status = verify; ctx.status = status;
return; return;
} }
// This may be used before the follow is completed, so we do not // This may be used before the follow is completed, so we do not
@ -441,9 +440,9 @@ router.get(
// follow request // follow request
router.get("/follows/:followRequestId", async (ctx: Router.RouterContext) => { router.get("/follows/:followRequestId", async (ctx: Router.RouterContext) => {
const verify = await checkFetch(ctx.req); const { status } = await checkFetch(ctx.req);
if (verify !== 200) { if (status !== 200) {
ctx.status = verify; ctx.status = status
return; return;
} }

View file

@ -10,9 +10,9 @@ import { setResponseType } from "../activitypub.js";
import type Router from "@koa/router"; import type Router from "@koa/router";
export default async (ctx: Router.RouterContext) => { export default async (ctx: Router.RouterContext) => {
const verify = await checkFetch(ctx.req); const { status } = await checkFetch(ctx.req);
if (verify !== 200) { if (status !== 200) {
ctx.status = verify; ctx.status = status;
return; return;
} }

View file

@ -14,9 +14,9 @@ import type { FindOptionsWhere } from "typeorm";
import type Router from "@koa/router"; import type Router from "@koa/router";
export default async (ctx: Router.RouterContext) => { export default async (ctx: Router.RouterContext) => {
const verify = await checkFetch(ctx.req); const { status } = await checkFetch(ctx.req);
if (verify !== 200) { if (status !== 200) {
ctx.status = verify; ctx.status = status;
return; return;
} }

View file

@ -14,9 +14,9 @@ import type { FindOptionsWhere } from "typeorm";
import type Router from "@koa/router"; import type Router from "@koa/router";
export default async (ctx: Router.RouterContext) => { export default async (ctx: Router.RouterContext) => {
const verify = await checkFetch(ctx.req); const { status } = await checkFetch(ctx.req);
if (verify !== 200) { if (status !== 200) {
ctx.status = verify; ctx.status = status;
return; return;
} }

View file

@ -17,9 +17,9 @@ import { setResponseType } from "../activitypub.js";
import type Router from "@koa/router"; import type Router from "@koa/router";
export default async (ctx: Router.RouterContext) => { export default async (ctx: Router.RouterContext) => {
const verify = await checkFetch(ctx.req); const { status, host } = await checkFetch(ctx.req);
if (verify !== 200) { if (status !== 200) {
ctx.status = verify; ctx.status = status;
return; return;
} }
@ -78,7 +78,7 @@ export default async (ctx: Router.RouterContext) => {
if (sinceId) notes.reverse(); if (sinceId) notes.reverse();
const activities = await Promise.all( const activities = await Promise.all(
notes.map((note) => packActivity(note)), notes.map((note) => packActivity(note, host)),
); );
const rendered = renderOrderedCollectionPage( const rendered = renderOrderedCollectionPage(
`${partOf}?${url.query({ `${partOf}?${url.query({
@ -129,7 +129,10 @@ export default async (ctx: Router.RouterContext) => {
* Pack Create<Note> or Announce Activity * Pack Create<Note> or Announce Activity
* @param note Note * @param note Note
*/ */
export async function packActivity(note: Note): Promise<any> { export async function packActivity(
note: Note,
clientHost: string | undefined = undefined
): Promise<any> {
if ( if (
note.renoteId && note.renoteId &&
note.text == null && note.text == null &&
@ -144,5 +147,5 @@ export async function packActivity(note: Note): Promise<any> {
); );
} }
return renderCreate(await renderNote(note, false), note); return renderCreate(await renderNote(note, false, false, clientHost), note);
} }