diff --git a/packages/backend/src/remote/activitypub/check-fetch.ts b/packages/backend/src/remote/activitypub/check-fetch.ts index a8bbe61b84..c885b4a199 100644 --- a/packages/backend/src/remote/activitypub/check-fetch.ts +++ b/packages/backend/src/remote/activitypub/check-fetch.ts @@ -7,6 +7,8 @@ import DbResolver from "@/remote/activitypub/db-resolver.js"; import { getApId } from "@/remote/activitypub/type.js"; import { shouldBlockInstance } from "@/misc/should-block-instance.js"; import type { IncomingMessage } from "http"; +import type { CacheableRemoteUser } from "@/models/entities/user.js"; +import type { UserPublickey } from "@/models/entities/user-publickey.js"; export async function hasSignature(req: IncomingMessage): Promise { const meta = await fetchMeta(); @@ -95,3 +97,22 @@ export async function checkFetch(req: IncomingMessage): Promise { } return 200; } + +export async function getSignatureUser(req: IncomingMessage): Promise<{ + user: CacheableRemoteUser; + key: UserPublickey | null; +} | null> { + const signature = httpSignature.parseRequest(req, { headers: [] }); + const keyId = new URL(signature.keyId); + const dbResolver = new DbResolver(); + + // Retrieve from DB by HTTP-Signature keyId + const authUser = await dbResolver.getAuthUserFromKeyId(signature.keyId); + if (authUser) { + return authUser; + } + + // Resolve if failed to retrieve by keyId + keyId.hash = ""; + return await dbResolver.getAuthUserFromApId(getApId(keyId.toString())); +} diff --git a/packages/backend/src/remote/activitypub/request.ts b/packages/backend/src/remote/activitypub/request.ts index ffb3e25a33..69c97a445d 100644 --- a/packages/backend/src/remote/activitypub/request.ts +++ b/packages/backend/src/remote/activitypub/request.ts @@ -3,6 +3,7 @@ import { getUserKeypair } from "@/misc/keypair-store.js"; import type { User } from "@/models/entities/user.js"; import { getResponse } from "../../misc/fetch.js"; import { createSignedPost, createSignedGet } from "./ap-request.js"; +import { apLogger } from "@/remote/activitypub/logger.js"; export default async (user: { id: User["id"] }, url: string, object: any) => { const body = JSON.stringify(object); @@ -35,6 +36,7 @@ export default async (user: { id: User["id"] }, url: string, object: any) => { * @param url URL to fetch */ export async function signedGet(url: string, user: { id: User["id"] }) { + apLogger.debug(`Running signedGet on url: ${url}`); const keypair = await getUserKeypair(user.id); const req = createSignedGet({ diff --git a/packages/backend/src/remote/activitypub/resolver.ts b/packages/backend/src/remote/activitypub/resolver.ts index 0547927609..608ca3e935 100644 --- a/packages/backend/src/remote/activitypub/resolver.ts +++ b/packages/backend/src/remote/activitypub/resolver.ts @@ -23,6 +23,7 @@ import renderCreate from "@/remote/activitypub/renderer/create.js"; import { renderActivity } from "@/remote/activitypub/renderer/index.js"; import renderFollow from "@/remote/activitypub/renderer/follow.js"; import { shouldBlockInstance } from "@/misc/should-block-instance.js"; +import { apLogger } from "@/remote/activitypub/logger.js"; export default class Resolver { private history: Set; @@ -34,6 +35,15 @@ export default class Resolver { this.recursionLimit = recursionLimit; } + public setUser(user) { + this.user = user; + } + + public reset(): Resolver { + this.history = new Set(); + return this; + } + public getHistory(): string[] { return Array.from(this.history); } @@ -56,15 +66,20 @@ export default class Resolver { } if (typeof value !== "string") { + apLogger.debug("Object to resolve is not a string"); if (typeof value.id !== "undefined") { const host = extractDbHost(getApId(value)); if (await shouldBlockInstance(host)) { throw new Error("instance is blocked"); } } + apLogger.debug("Returning existing object:"); + apLogger.debug(JSON.stringify(value, null, 2)); return value; } + apLogger.debug(`Resolving: ${value}`); + if (value.includes("#")) { // URLs with fragment parts cannot be resolved correctly because // the fragment part does not get transmitted over HTTP(S). @@ -102,6 +117,9 @@ export default class Resolver { this.user = await getInstanceActor(); } + apLogger.debug("Getting object from remote, authenticated as user:"); + apLogger.debug(JSON.stringify(this.user, null, 2)); + const object = ( this.user ? await signedGet(value, this.user) diff --git a/packages/backend/src/server/activitypub.ts b/packages/backend/src/server/activitypub.ts index 042ab446c7..f9d5eb99c3 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,37 @@ router.get("/notes/:note", async (ctx, next) => { return; } + if (note.visibility === "followers") { + serverLogger.debug( + "Responding to request for follower-only note, validating access...", + ); + const 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)); + + if (remoteUser == null) { + serverLogger.debug("Rejecting: no user"); + ctx.status = 401; + return; + } + + const 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(); diff --git a/packages/backend/src/server/api/endpoints/ap/show.ts b/packages/backend/src/server/api/endpoints/ap/show.ts index 0bd3414ee3..3dd168d718 100644 --- a/packages/backend/src/server/api/endpoints/ap/show.ts +++ b/packages/backend/src/server/api/endpoints/ap/show.ts @@ -127,6 +127,7 @@ async function fetchAny( // fetching Object once from remote const resolver = new Resolver(); + resolver.setUser(me); const object = await resolver.resolve(uri); // /@user If a URI other than the id is specified, @@ -144,8 +145,12 @@ async function fetchAny( return await mergePack( me, - isActor(object) ? await createPerson(getApId(object)) : null, - isPost(object) ? await createNote(getApId(object), undefined, true) : null, + isActor(object) + ? await createPerson(getApId(object), resolver.reset()) + : null, + isPost(object) + ? await createNote(getApId(object), resolver.reset(), true) + : null, ); }