summaryrefslogtreecommitdiff
path: root/packages/backend/src/server/ActivityPubServerService.ts
diff options
context:
space:
mode:
authorJulia <julia@insertdomain.name>2025-05-29 00:07:38 +0000
committerJulia <julia@insertdomain.name>2025-05-29 00:07:38 +0000
commit6b554c178b81f13f83a69b19d44b72b282a0c119 (patch)
treef5537f1a56323a4dd57ba150b3cb84a2d8b5dc63 /packages/backend/src/server/ActivityPubServerService.ts
parentmerge: Security fixes (!970) (diff)
parentbump version for release (diff)
downloadsharkey-6b554c178b81f13f83a69b19d44b72b282a0c119.tar.gz
sharkey-6b554c178b81f13f83a69b19d44b72b282a0c119.tar.bz2
sharkey-6b554c178b81f13f83a69b19d44b72b282a0c119.zip
merge: release 2025.4.2 (!1051)
View MR for information: https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/1051 Approved-by: Hazelnoot <acomputerdog@gmail.com> Approved-by: Marie <github@yuugi.dev> Approved-by: Julia <julia@insertdomain.name>
Diffstat (limited to 'packages/backend/src/server/ActivityPubServerService.ts')
-rw-r--r--packages/backend/src/server/ActivityPubServerService.ts418
1 files changed, 316 insertions, 102 deletions
diff --git a/packages/backend/src/server/ActivityPubServerService.ts b/packages/backend/src/server/ActivityPubServerService.ts
index 10dba1660f..41beadb56d 100644
--- a/packages/backend/src/server/ActivityPubServerService.ts
+++ b/packages/backend/src/server/ActivityPubServerService.ts
@@ -14,7 +14,7 @@ import accepts from 'accepts';
import vary from 'vary';
import secureJson from 'secure-json-parse';
import { DI } from '@/di-symbols.js';
-import type { FollowingsRepository, NotesRepository, EmojisRepository, NoteReactionsRepository, UserProfilesRepository, UserNotePiningsRepository, UsersRepository, FollowRequestsRepository } from '@/models/_.js';
+import type { FollowingsRepository, NotesRepository, EmojisRepository, NoteReactionsRepository, UserProfilesRepository, UserNotePiningsRepository, UsersRepository, FollowRequestsRepository, MiMeta } from '@/models/_.js';
import * as url from '@/misc/prelude/url.js';
import type { Config } from '@/config.js';
import { ApRendererService } from '@/core/activitypub/ApRendererService.js';
@@ -22,7 +22,6 @@ import { ApDbResolverService } from '@/core/activitypub/ApDbResolverService.js';
import { QueueService } from '@/core/QueueService.js';
import type { MiLocalUser, MiRemoteUser, MiUser } from '@/models/User.js';
import { UserKeypairService } from '@/core/UserKeypairService.js';
-import { InstanceActorService } from '@/core/InstanceActorService.js';
import type { MiUserPublickey } from '@/models/UserPublickey.js';
import type { MiFollowing } from '@/models/Following.js';
import { countIf } from '@/misc/prelude/array.js';
@@ -33,11 +32,13 @@ import { UserEntityService } from '@/core/entities/UserEntityService.js';
import type Logger from '@/logger.js';
import { LoggerService } from '@/core/LoggerService.js';
import { bindThis } from '@/decorators.js';
-import { IActivity } from '@/core/activitypub/type.js';
+import { IActivity, IAnnounce, ICreate } from '@/core/activitypub/type.js';
import { isQuote, isRenote } from '@/misc/is-renote.js';
import * as Acct from '@/misc/acct.js';
+import { CacheService } from '@/core/CacheService.js';
import type { FastifyInstance, FastifyRequest, FastifyReply, FastifyPluginOptions, FastifyBodyParser } from 'fastify';
import type { FindOptionsWhere } from 'typeorm';
+import { FanoutTimelineEndpointService } from '@/core/FanoutTimelineEndpointService.js';
const ACTIVITY_JSON = 'application/activity+json; charset=utf-8';
const LD_JSON = 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"; charset=utf-8';
@@ -51,6 +52,9 @@ export class ActivityPubServerService {
@Inject(DI.config)
private config: Config,
+ @Inject(DI.meta)
+ private meta: MiMeta,
+
@Inject(DI.usersRepository)
private usersRepository: UsersRepository,
@@ -77,13 +81,14 @@ export class ActivityPubServerService {
private utilityService: UtilityService,
private userEntityService: UserEntityService,
- private instanceActorService: InstanceActorService,
private apRendererService: ApRendererService,
private apDbResolverService: ApDbResolverService,
private queueService: QueueService,
private userKeypairService: UserKeypairService,
private queryService: QueryService,
+ private fanoutTimelineEndpointService: FanoutTimelineEndpointService,
private loggerService: LoggerService,
+ private readonly cacheService: CacheService,
) {
//this.createServer = this.createServer.bind(this);
this.logger = this.loggerService.getLogger('apserv', 'pink');
@@ -106,7 +111,7 @@ export class ActivityPubServerService {
* @param author Author of the note
*/
@bindThis
- private async packActivity(note: MiNote, author: MiUser): Promise<any> {
+ private async packActivity(note: MiNote, author: MiUser): Promise<ICreate | IAnnounce> {
if (isRenote(note) && !isQuote(note)) {
const renote = await this.notesRepository.findOneByOrFail({ id: note.renoteId });
return this.apRendererService.renderAnnounce(renote.uri ? renote.uri : `${this.config.url}/notes/${renote.id}`, note);
@@ -115,10 +120,55 @@ export class ActivityPubServerService {
return this.apRendererService.renderCreate(await this.apRendererService.renderNote(note, author, false), note);
}
- @bindThis
- private async shouldRefuseGetRequest(request: FastifyRequest, reply: FastifyReply, userId: string | undefined = undefined): Promise<boolean> {
- if (!this.config.checkActivityPubGetSignature) return false;
+ /**
+ * Checks Authorized Fetch.
+ * Returns an object with two properties:
+ * * reject - true if the request should be ignored by the caller, false if it should be processed.
+ * * redact - true if the caller should redact response data, false if it should return full data.
+ * When "reject" is true, the HTTP status code will be automatically set to 401 unauthorized.
+ */
+ private async checkAuthorizedFetch(
+ request: FastifyRequest,
+ reply: FastifyReply,
+ userId?: string,
+ essential?: boolean,
+ ): Promise<{ reject: boolean, redact: boolean }> {
+ // Federation disabled => reject
+ if (this.meta.federation === 'none') {
+ reply.code(401);
+ return { reject: true, redact: true };
+ }
+
+ // Auth fetch disabled => accept
+ const allowUnsignedFetch = await this.getUnsignedFetchAllowance(userId);
+ if (allowUnsignedFetch === 'always') {
+ return { reject: false, redact: false };
+ }
+ // Valid signature => accept
+ const error = await this.checkSignature(request);
+ if (!error) {
+ return { reject: false, redact: false };
+ }
+
+ // Unsigned, but essential => accept redacted
+ if (allowUnsignedFetch === 'essential' && essential) {
+ return { reject: false, redact: true };
+ }
+
+ // Unsigned, not essential => reject
+ this.authlogger.warn(error);
+ reply.code(401);
+ return { reject: true, redact: true };
+ }
+
+ /**
+ * Verifies HTTP Signatures for a request.
+ * Returns null of success (valid signature).
+ * Returns a string error on validation failure.
+ */
+ @bindThis
+ private async checkSignature(request: FastifyRequest): Promise<string | null> {
/* this code is inspired from the `inbox` function below, and
`queue/processors/InboxProcessorService`
@@ -129,59 +179,33 @@ export class ActivityPubServerService {
this is also inspired by FireFish's `checkFetch`
*/
- /* tell any caching proxy that they should not cache these
- responses: we wouldn't want the proxy to return a 403 to
- someone presenting a valid signature, or return a cached
- response body to someone we've blocked!
- */
- reply.header('Cache-Control', 'private, max-age=0, must-revalidate');
-
- /* we always allow requests about our instance actor, because when
- a remote instance needs to check our signature on a request we
- sent, it will need to fetch information about the user that
- signed it (which is our instance actor), and if we try to check
- their signature on *that* request, we'll fetch *their* instance
- actor... leading to an infinite recursion */
- if (userId) {
- const instanceActor = await this.instanceActorService.getInstanceActor();
-
- if (userId === instanceActor.id || userId === instanceActor.username) {
- this.authlogger.debug(`${request.id} ${request.url} request to instance.actor, letting through`);
- return false;
- }
- }
-
- let signature;
+ let signature: httpSignature.IParsedSignature;
try {
- signature = httpSignature.parseRequest(request.raw, { 'headers': ['(request-target)', 'host', 'date'], authorizationHeaderName: 'signature' });
+ signature = httpSignature.parseRequest(request.raw, {
+ headers: ['(request-target)', 'host', 'date'],
+ authorizationHeaderName: 'signature',
+ });
} catch (e) {
// not signed, or malformed signature: refuse
- this.authlogger.warn(`${request.id} ${request.url} not signed, or malformed signature: refuse`);
- reply.code(401);
- return true;
+ return `${request.id} ${request.url} not signed, or malformed signature: refuse`;
}
const keyId = new URL(signature.keyId);
const keyHost = this.utilityService.toPuny(keyId.hostname);
- const logPrefix = `${request.id} ${request.url} (by ${request.headers['user-agent']}) apparently from ${keyHost}:`;
+ const logPrefix = `${request.id} ${request.url} (by ${request.headers['user-agent']}) claims to be from ${keyHost}:`;
- if (signature.params.headers.indexOf('host') === -1
- || request.headers.host !== this.config.host) {
+ if (signature.params.headers.indexOf('host') === -1 || request.headers.host !== this.config.host) {
// no destination host, or not us: refuse
- this.authlogger.warn(`${logPrefix} no destination host, or not us: refuse`);
- reply.code(401);
- return true;
+ return `${logPrefix} no destination host, or not us: refuse`;
}
if (!this.utilityService.isFederationAllowedHost(keyHost)) {
/* blocked instance: refuse (we don't care if the signature is
good, if they even pretend to be from a blocked instance,
they're out) */
- this.authlogger.warn(`${logPrefix} instance is blocked: refuse`);
- reply.code(401);
- return true;
+ return `${logPrefix} instance is blocked: refuse`;
}
// do we know the signer already?
@@ -200,34 +224,64 @@ export class ActivityPubServerService {
if (authUser?.key == null) {
// we can't figure out who the signer is, or we can't get their key: refuse
- this.authlogger.warn(`${logPrefix} we can't figure out who the signer is, or we can't get their key: refuse`);
- reply.code(401);
- return true;
+ return `${logPrefix} we can't figure out who the signer is, or we can't get their key: refuse`;
}
- let httpSignatureValidated = httpSignature.verifySignature(signature, authUser.key.keyPem);
+ if (authUser.user.isSuspended) {
+ // Signer is suspended locally
+ return `${logPrefix} signer is suspended: refuse`;
+ }
+
+ // some fedi implementations include the query (`?foo=bar`) in the
+ // signature, some don't, so we have to handle both cases
+ function verifyWithOrWithoutQuery() {
+ const httpSignatureValidated = httpSignature.verifySignature(signature, authUser!.key!.keyPem);
+ if (httpSignatureValidated) return true;
+
+ const requestUrl = new URL(`http://whatever${request.raw.url}`);
+ if (! requestUrl.search) return false;
+
+ // verification failed, the request URL contained a query, let's try without
+ const semiRawRequest = request.raw;
+ semiRawRequest.url = requestUrl.pathname;
+
+ // no need for try/catch, if the original request parsed, this
+ // one will, too
+ const signatureWithoutQuery = httpSignature.parseRequest(semiRawRequest, {
+ headers: ['(request-target)', 'host', 'date'],
+ authorizationHeaderName: 'signature',
+ });
+
+ return httpSignature.verifySignature(signatureWithoutQuery, authUser!.key!.keyPem);
+ }
+
+ let httpSignatureValidated = verifyWithOrWithoutQuery();
// maybe they changed their key? refetch it
+ // TODO rate-limit this using lastFetchedAt
if (!httpSignatureValidated) {
authUser.key = await this.apDbResolverService.refetchPublicKeyForApId(authUser.user);
if (authUser.key != null) {
- httpSignatureValidated = httpSignature.verifySignature(signature, authUser.key.keyPem);
+ httpSignatureValidated = verifyWithOrWithoutQuery();
}
}
if (!httpSignatureValidated) {
// bad signature: refuse
- this.authlogger.info(`${logPrefix} failed to validate signature: refuse`);
- reply.code(401);
- return true;
+ return `${logPrefix} failed to validate signature: refuse`;
}
// all good, don't refuse
- return false;
+ return null;
}
@bindThis
private inbox(request: FastifyRequest, reply: FastifyReply) {
+ if (this.meta.federation === 'none') {
+ reply.code(403);
+ return;
+ }
+
let signature;
try {
@@ -299,7 +353,13 @@ export class ActivityPubServerService {
request: FastifyRequest<{ Params: { user: string; }; Querystring: { cursor?: string; page?: string; }; }>,
reply: FastifyReply,
) {
- if (await this.shouldRefuseGetRequest(request, reply, request.params.user)) return;
+ if (this.meta.federation === 'none') {
+ reply.code(403);
+ return;
+ }
+
+ const { reject } = await this.checkAuthorizedFetch(request, reply, request.params.user);
+ if (reject) return;
const userId = request.params.user;
@@ -326,11 +386,9 @@ export class ActivityPubServerService {
if (profile.followersVisibility === 'private') {
reply.code(403);
- if (!this.config.checkActivityPubGetSignature) reply.header('Cache-Control', 'public, max-age=30');
return;
} else if (profile.followersVisibility === 'followers') {
reply.code(403);
- if (!this.config.checkActivityPubGetSignature) reply.header('Cache-Control', 'public, max-age=30');
return;
}
//#endregion
@@ -382,7 +440,6 @@ export class ActivityPubServerService {
user.followersCount,
`${partOf}?page=true`,
);
- if (!this.config.checkActivityPubGetSignature) reply.header('Cache-Control', 'public, max-age=180');
this.setResponseType(request, reply);
return (this.apRendererService.addContext(rendered));
}
@@ -393,7 +450,13 @@ export class ActivityPubServerService {
request: FastifyRequest<{ Params: { user: string; }; Querystring: { cursor?: string; page?: string; }; }>,
reply: FastifyReply,
) {
- if (await this.shouldRefuseGetRequest(request, reply, request.params.user)) return;
+ if (this.meta.federation === 'none') {
+ reply.code(403);
+ return;
+ }
+
+ const { reject } = await this.checkAuthorizedFetch(request, reply, request.params.user);
+ if (reject) return;
const userId = request.params.user;
@@ -420,11 +483,9 @@ export class ActivityPubServerService {
if (profile.followingVisibility === 'private') {
reply.code(403);
- if (!this.config.checkActivityPubGetSignature) reply.header('Cache-Control', 'public, max-age=30');
return;
} else if (profile.followingVisibility === 'followers') {
reply.code(403);
- if (!this.config.checkActivityPubGetSignature) reply.header('Cache-Control', 'public, max-age=30');
return;
}
//#endregion
@@ -476,7 +537,6 @@ export class ActivityPubServerService {
user.followingCount,
`${partOf}?page=true`,
);
- if (!this.config.checkActivityPubGetSignature) reply.header('Cache-Control', 'public, max-age=180');
this.setResponseType(request, reply);
return (this.apRendererService.addContext(rendered));
}
@@ -484,7 +544,13 @@ export class ActivityPubServerService {
@bindThis
private async featured(request: FastifyRequest<{ Params: { user: string; }; }>, reply: FastifyReply) {
- if (await this.shouldRefuseGetRequest(request, reply, request.params.user)) return;
+ if (this.meta.federation === 'none') {
+ reply.code(403);
+ return;
+ }
+
+ const { reject } = await this.checkAuthorizedFetch(request, reply, request.params.user);
+ if (reject) return;
const userId = request.params.user;
@@ -517,7 +583,6 @@ export class ActivityPubServerService {
renderedNotes,
);
- if (!this.config.checkActivityPubGetSignature) reply.header('Cache-Control', 'public, max-age=180');
this.setResponseType(request, reply);
return (this.apRendererService.addContext(rendered));
}
@@ -530,7 +595,13 @@ export class ActivityPubServerService {
}>,
reply: FastifyReply,
) {
- if (await this.shouldRefuseGetRequest(request, reply, request.params.user)) return;
+ if (this.meta.federation === 'none') {
+ reply.code(403);
+ return;
+ }
+
+ const { reject } = await this.checkAuthorizedFetch(request, reply, request.params.user);
+ if (reject) return;
const userId = request.params.user;
@@ -567,16 +638,28 @@ export class ActivityPubServerService {
const partOf = `${this.config.url}/users/${userId}/outbox`;
if (page) {
- const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), sinceId, untilId)
- .andWhere('note.userId = :userId', { userId: user.id })
- .andWhere(new Brackets(qb => {
- qb
- .where('note.visibility = \'public\'')
- .orWhere('note.visibility = \'home\'');
- }))
- .andWhere('note.localOnly = FALSE');
-
- const notes = await query.limit(limit).getMany();
+ const notes = this.meta.enableFanoutTimeline ? await this.fanoutTimelineEndpointService.getMiNotes({
+ sinceId: sinceId ?? null,
+ untilId: untilId ?? null,
+ limit: limit,
+ allowPartial: false, // Possibly true? IDK it's OK for ordered collection.
+ me: null,
+ redisTimelines: [
+ `userTimeline:${user.id}`,
+ `userTimelineWithReplies:${user.id}`,
+ ],
+ useDbFallback: true,
+ ignoreAuthorFromMute: true,
+ excludePureRenotes: false,
+ noteFilter: (note) => {
+ if (note.visibility !== 'home' && note.visibility !== 'public') return false;
+ if (note.localOnly) return false;
+ return true;
+ },
+ dbFallback: async (untilId, sinceId, limit) => {
+ return await this.getUserNotesFromDb(sinceId, untilId, limit, user.id);
+ },
+ }) : await this.getUserNotesFromDb(sinceId ?? null, untilId ?? null, limit, user.id);
if (sinceId) notes.reverse();
@@ -608,14 +691,32 @@ export class ActivityPubServerService {
`${partOf}?page=true`,
`${partOf}?page=true&since_id=000000000000000000000000`,
);
- if (!this.config.checkActivityPubGetSignature) reply.header('Cache-Control', 'public, max-age=180');
this.setResponseType(request, reply);
return (this.apRendererService.addContext(rendered));
}
}
@bindThis
- private async userInfo(request: FastifyRequest, reply: FastifyReply, user: MiUser | null) {
+ private async getUserNotesFromDb(untilId: string | null, sinceId: string | null, limit: number, userId: MiUser['id']) {
+ return await this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), sinceId, untilId)
+ .andWhere('note.userId = :userId', { userId })
+ .andWhere(new Brackets(qb => {
+ qb
+ .where('note.visibility = \'public\'')
+ .orWhere('note.visibility = \'home\'');
+ }))
+ .andWhere('note.localOnly = FALSE')
+ .limit(limit)
+ .getMany();
+ }
+
+ @bindThis
+ private async userInfo(request: FastifyRequest, reply: FastifyReply, user: MiUser | null, redact = false) {
+ if (this.meta.federation === 'none') {
+ reply.code(403);
+ return;
+ }
+
if (user == null) {
reply.code(404);
return;
@@ -631,10 +732,12 @@ export class ActivityPubServerService {
return;
}
- if (!this.config.checkActivityPubGetSignature) reply.header('Cache-Control', 'public, max-age=180');
-
this.setResponseType(request, reply);
- return (this.apRendererService.addContext(await this.apRendererService.renderPerson(user as MiLocalUser)));
+
+ const person = redact
+ ? await this.apRendererService.renderPersonRedacted(user as MiLocalUser)
+ : await this.apRendererService.renderPerson(user as MiLocalUser);
+ return this.apRendererService.addContext(person);
}
@bindThis
@@ -687,6 +790,13 @@ export class ActivityPubServerService {
reply.header('Access-Control-Allow-Methods', 'GET, OPTIONS');
reply.header('Access-Control-Allow-Origin', '*');
reply.header('Access-Control-Expose-Headers', 'Vary');
+
+ /* tell any caching proxy that they should not cache these
+ responses: we wouldn't want the proxy to return a 403 to
+ someone presenting a valid signature, or return a cached
+ response body to someone we've blocked!
+ */
+ reply.header('Cache-Control', 'private, max-age=0, must-revalidate');
done();
});
@@ -697,16 +807,22 @@ export class ActivityPubServerService {
// note
fastify.get<{ Params: { note: string; } }>('/notes/:note', { constraints: { apOrHtml: 'ap' } }, async (request, reply) => {
- if (await this.shouldRefuseGetRequest(request, reply)) return;
-
vary(reply.raw, 'Accept');
+ if (this.meta.federation === 'none') {
+ reply.code(403);
+ return;
+ }
+
const note = await this.notesRepository.findOneBy({
id: request.params.note,
visibility: In(['public', 'home']),
localOnly: false,
});
+ const { reject } = await this.checkAuthorizedFetch(request, reply, note?.userId);
+ if (reject) return;
+
if (note == null) {
reply.code(404);
return;
@@ -722,7 +838,6 @@ export class ActivityPubServerService {
return;
}
- if (!this.config.checkActivityPubGetSignature) reply.header('Cache-Control', 'public, max-age=180');
this.setResponseType(request, reply);
const author = await this.usersRepository.findOneByOrFail({ id: note.userId });
@@ -731,10 +846,13 @@ export class ActivityPubServerService {
// note activity
fastify.get<{ Params: { note: string; } }>('/notes/:note/activity', async (request, reply) => {
- if (await this.shouldRefuseGetRequest(request, reply)) return;
-
vary(reply.raw, 'Accept');
+ if (this.meta.federation === 'none') {
+ reply.code(403);
+ return;
+ }
+
const note = await this.notesRepository.findOneBy({
id: request.params.note,
userHost: IsNull(),
@@ -742,18 +860,66 @@ export class ActivityPubServerService {
localOnly: false,
});
+ const { reject } = await this.checkAuthorizedFetch(request, reply, note?.userId);
+ if (reject) return;
+
if (note == null) {
reply.code(404);
return;
}
- if (!this.config.checkActivityPubGetSignature) reply.header('Cache-Control', 'public, max-age=180');
this.setResponseType(request, reply);
const author = await this.usersRepository.findOneByOrFail({ id: note.userId });
return (this.apRendererService.addContext(await this.packActivity(note, author)));
});
+ // replies
+ fastify.get<{
+ Params: { note: string; };
+ Querystring: { page?: unknown; until_id?: unknown; };
+ }>('/notes/:note/replies', async (request, reply) => {
+ vary(reply.raw, 'Accept');
+ this.setResponseType(request, reply);
+
+ // Raw query to avoid fetching the while entity just to check access and get the user ID
+ const note = await this.notesRepository
+ .createQueryBuilder('note')
+ .andWhere({
+ id: request.params.note,
+ userHost: IsNull(),
+ visibility: In(['public', 'home']),
+ localOnly: false,
+ })
+ .select(['note.id', 'note.userId'])
+ .getRawOne<{ note_id: string, note_userId: string }>();
+
+ const { reject } = await this.checkAuthorizedFetch(request, reply, note?.note_userId);
+ if (reject) return;
+
+ if (note == null) {
+ reply.code(404);
+ return;
+ }
+
+ const untilId = request.query.until_id;
+ if (untilId != null && typeof(untilId) !== 'string') {
+ reply.code(400);
+ return;
+ }
+
+ // If page is unset, then we just provide the outer wrapper.
+ // This is because the spec doesn't allow the wrapper to contain both elements *and* pages.
+ // We could technically do it anyway, but that may break other instances.
+ if (request.query.page !== 'true') {
+ const collection = await this.apRendererService.renderRepliesCollection(note.note_id);
+ return this.apRendererService.addContext(collection);
+ }
+
+ const page = await this.apRendererService.renderRepliesCollectionPage(note.note_id, untilId ?? undefined);
+ return this.apRendererService.addContext(page);
+ });
+
// outbox
fastify.get<{
Params: { user: string; };
@@ -777,7 +943,13 @@ export class ActivityPubServerService {
// publickey
fastify.get<{ Params: { user: string; } }>('/users/:user/publickey', async (request, reply) => {
- if (await this.shouldRefuseGetRequest(request, reply, request.params.user)) return;
+ if (this.meta.federation === 'none') {
+ reply.code(403);
+ return;
+ }
+
+ const { reject } = await this.checkAuthorizedFetch(request, reply, request.params.user, true);
+ if (reject) return;
const userId = request.params.user;
@@ -794,7 +966,6 @@ export class ActivityPubServerService {
const keypair = await this.userKeypairService.getUserKeypair(user.id);
if (this.userEntityService.isLocalUser(user)) {
- if (!this.config.checkActivityPubGetSignature) reply.header('Cache-Control', 'public, max-age=180');
this.setResponseType(request, reply);
return (this.apRendererService.addContext(this.apRendererService.renderKey(user, keypair)));
} else {
@@ -804,10 +975,16 @@ export class ActivityPubServerService {
});
fastify.get<{ Params: { user: string; } }>('/users/:user', { constraints: { apOrHtml: 'ap' } }, async (request, reply) => {
- if (await this.shouldRefuseGetRequest(request, reply, request.params.user)) return;
+ const { reject, redact } = await this.checkAuthorizedFetch(request, reply, request.params.user, true);
+ if (reject) return;
vary(reply.raw, 'Accept');
+ if (this.meta.federation === 'none') {
+ reply.code(403);
+ return;
+ }
+
const userId = request.params.user;
const user = await this.usersRepository.findOneBy({
@@ -815,29 +992,41 @@ export class ActivityPubServerService {
isSuspended: false,
});
- return await this.userInfo(request, reply, user);
+ return await this.userInfo(request, reply, user, redact);
});
fastify.get<{ Params: { acct: string; } }>('/@:acct', { constraints: { apOrHtml: 'ap' } }, async (request, reply) => {
- if (await this.shouldRefuseGetRequest(request, reply, request.params.acct)) return;
-
vary(reply.raw, 'Accept');
+ if (this.meta.federation === 'none') {
+ reply.code(403);
+ return;
+ }
+
const acct = Acct.parse(request.params.acct);
const user = await this.usersRepository.findOneBy({
- usernameLower: acct.username,
+ usernameLower: acct.username.toLowerCase(),
host: acct.host ?? IsNull(),
isSuspended: false,
});
- return await this.userInfo(request, reply, user);
+ const { reject, redact } = await this.checkAuthorizedFetch(request, reply, user?.id, true);
+ if (reject) return;
+
+ return await this.userInfo(request, reply, user, redact);
});
//#endregion
// emoji
fastify.get<{ Params: { emoji: string; } }>('/emojis/:emoji', async (request, reply) => {
- if (await this.shouldRefuseGetRequest(request, reply)) return;
+ if (this.meta.federation === 'none') {
+ reply.code(403);
+ return;
+ }
+
+ const { reject } = await this.checkAuthorizedFetch(request, reply);
+ if (reject) return;
const emoji = await this.emojisRepository.findOneBy({
host: IsNull(),
@@ -849,17 +1038,22 @@ export class ActivityPubServerService {
return;
}
- if (!this.config.checkActivityPubGetSignature) reply.header('Cache-Control', 'public, max-age=180');
this.setResponseType(request, reply);
return (this.apRendererService.addContext(await this.apRendererService.renderEmoji(emoji)));
});
// like
fastify.get<{ Params: { like: string; } }>('/likes/:like', async (request, reply) => {
- if (await this.shouldRefuseGetRequest(request, reply)) return;
+ if (this.meta.federation === 'none') {
+ reply.code(403);
+ return;
+ }
const reaction = await this.noteReactionsRepository.findOneBy({ id: request.params.like });
+ const { reject } = await this.checkAuthorizedFetch(request, reply, reaction?.userId);
+ if (reject) return;
+
if (reaction == null) {
reply.code(404);
return;
@@ -872,14 +1066,19 @@ export class ActivityPubServerService {
return;
}
- if (!this.config.checkActivityPubGetSignature) reply.header('Cache-Control', 'public, max-age=180');
this.setResponseType(request, reply);
return (this.apRendererService.addContext(await this.apRendererService.renderLike(reaction, note)));
});
// follow
fastify.get<{ Params: { follower: string; followee: string; } }>('/follows/:follower/:followee', async (request, reply) => {
- if (await this.shouldRefuseGetRequest(request, reply)) return;
+ if (this.meta.federation === 'none') {
+ reply.code(403);
+ return;
+ }
+
+ const { reject } = await this.checkAuthorizedFetch(request, reply, request.params.follower);
+ if (reject) return;
// This may be used before the follow is completed, so we do not
// check if the following exists.
@@ -900,14 +1099,16 @@ export class ActivityPubServerService {
return;
}
- if (!this.config.checkActivityPubGetSignature) reply.header('Cache-Control', 'public, max-age=180');
this.setResponseType(request, reply);
return (this.apRendererService.addContext(this.apRendererService.renderFollow(follower, followee)));
});
// follow
- fastify.get<{ Params: { followRequestId: string ; } }>('/follows/:followRequestId', async (request, reply) => {
- if (await this.shouldRefuseGetRequest(request, reply)) return;
+ fastify.get<{ Params: { followRequestId: string; } }>('/follows/:followRequestId', async (request, reply) => {
+ if (this.meta.federation === 'none') {
+ reply.code(403);
+ return;
+ }
// This may be used before the follow is completed, so we do not
// check if the following exists and only check if the follow request exists.
@@ -916,6 +1117,9 @@ export class ActivityPubServerService {
id: request.params.followRequestId,
});
+ const { reject } = await this.checkAuthorizedFetch(request, reply, followRequest?.followerId);
+ if (reject) return;
+
if (followRequest == null) {
reply.code(404);
return;
@@ -937,11 +1141,21 @@ export class ActivityPubServerService {
return;
}
- if (!this.config.checkActivityPubGetSignature) reply.header('Cache-Control', 'public, max-age=180');
this.setResponseType(request, reply);
return (this.apRendererService.addContext(this.apRendererService.renderFollow(follower, followee)));
});
done();
}
+
+ private async getUnsignedFetchAllowance(userId: string | undefined) {
+ const user = userId ? await this.cacheService.findLocalUserById(userId) : null;
+
+ // User system value if there is no user, or if user has deferred the choice.
+ if (!user?.allowUnsignedFetch || user.allowUnsignedFetch === 'staff') {
+ return this.meta.allowUnsignedFetch;
+ }
+
+ return user.allowUnsignedFetch;
+ }
}