diff --git a/packages/backend/src/remote/activitypub/check-fetch.ts b/packages/backend/src/remote/activitypub/check-fetch.ts index a8bbe61b8..3e52575a9 100644 --- a/packages/backend/src/remote/activitypub/check-fetch.ts +++ b/packages/backend/src/remote/activitypub/check-fetch.ts @@ -95,3 +95,79 @@ export async function checkFetch(req: IncomingMessage): Promise { } return 200; } + +export async function getSignatureUser( + req: IncomingMessage, +): Promise { + let authUser; + const meta = await fetchMeta(); + if (meta.secureMode || meta.privateMode) { + let signature; + + try { + signature = httpSignature.parseRequest(req, { headers: [] }); + } catch (e) { + return null; + } + + const keyId = new URL(signature.keyId); + const host = toPuny(keyId.hostname); + + if (await shouldBlockInstance(host, meta)) { + return 403; + } + + if ( + meta.privateMode && + host !== config.host && + !meta.allowedHosts.includes(host) + ) { + return null; + } + + const keyIdLower = signature.keyId.toLowerCase(); + if (keyIdLower.startsWith("acct:")) { + // Old keyId is no longer supported. + return null; + } + + const dbResolver = new DbResolver(); + + // HTTP-Signature keyIdを元にDBから取得 + authUser = await dbResolver.getAuthUserFromKeyId(signature.keyId); + + // keyIdでわからなければ、resolveしてみる + if (authUser == null) { + try { + keyId.hash = ""; + authUser = await dbResolver.getAuthUserFromApId( + getApId(keyId.toString()), + ); + } catch (e) { + // できなければ駄目 + return null; + } + } + + // publicKey がなくても終了 + if (authUser?.key == null) { + return null; + } + + // もう一回チェック + if (authUser.user.host !== host) { + return null; + } + + // HTTP-Signatureの検証 + const httpSignatureValidated = httpSignature.verifySignature( + signature, + authUser.key.keyPem, + ); + + if (!httpSignatureValidated) { + return null; + } + } + return authUser; +} diff --git a/packages/backend/src/server/activitypub.ts b/packages/backend/src/server/activitypub.ts index 042ab446c..548aafdd2 100644 --- a/packages/backend/src/server/activitypub.ts +++ b/packages/backend/src/server/activitypub.ts @@ -20,7 +20,11 @@ import { import type { ILocalUser, User } from "@/models/entities/user.js"; import { renderLike } from "@/remote/activitypub/renderer/like.js"; import { getUserKeypair } from "@/misc/keypair-store.js"; -import { checkFetch, hasSignature } from "@/remote/activitypub/check-fetch.js"; +import { + checkFetch, + hasSignature, + getSignatureUser, +} from "@/remote/activitypub/check-fetch.js"; import { getInstanceActor } from "@/services/instance-actor.js"; import { fetchMeta } from "@/misc/fetch-meta.js"; import renderFollow from "@/remote/activitypub/renderer/follow.js"; @@ -28,6 +32,7 @@ import Featured from "./activitypub/featured.js"; import Following from "./activitypub/following.js"; import Followers from "./activitypub/followers.js"; import Outbox, { packActivity } from "./activitypub/outbox.js"; +import { serverLogger } from "./index.js"; // Init router const router = new Router(); @@ -84,7 +89,7 @@ router.get("/notes/:note", async (ctx, next) => { const note = await Notes.findOneBy({ id: ctx.params.note, - visibility: In(["public" as const, "home" as const]), + visibility: In(["public" as const, "home" as const, "followers" as const]), localOnly: false, }); @@ -103,6 +108,31 @@ router.get("/notes/:note", async (ctx, next) => { return; } + if (note.visibility == "followers") { + serverLogger.debug( + "Responding to request for follower-only note, validating access...", + ); + let remoteUser = await getSignatureUser(ctx.req); + serverLogger.debug("Local note author user:"); + serverLogger.debug(JSON.stringify(note, null, 2)); + serverLogger.debug("Authenticated remote user:"); + serverLogger.debug(JSON.stringify(remoteUser, null, 2)); + + let relation = await Users.getRelation(remoteUser.user.id, note.userId); + serverLogger.debug("Relation:"); + serverLogger.debug(JSON.stringify(relation, null, 2)); + + if (!relation.isFollowing || relation.isBlocked) { + serverLogger.debug( + "Rejecting: authenticated user is not following us or was blocked by us", + ); + ctx.status = 403; + return; + } + + serverLogger.debug("Accepting: access criteria met"); + } + ctx.body = renderActivity(await renderNote(note, false)); const meta = await fetchMeta();