Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(federation): InboxにきたCreate, AnnounceのobjectがBearcaps urlだった際はスキップするように #13610

Merged
merged 12 commits into from
May 28, 2024
117 changes: 68 additions & 49 deletions packages/backend/src/core/activitypub/ApInboxService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import type { UsersRepository, NotesRepository, FollowingsRepository, AbuseUserR
import { bindThis } from '@/decorators.js';
import type { MiRemoteUser } from '@/models/User.js';
import { isNotNull } from '@/misc/is-not-null.js';
import { GlobalEventService } from '@/core/GlobalEventService.js';
import { getApHrefNullable, getApId, getApIds, getApType, isAccept, isActor, isAdd, isAnnounce, isBlock, isCollection, isCollectionOrOrderedCollection, isCreate, isDelete, isFlag, isFollow, isLike, isMove, isPost, isReject, isRemove, isTombstone, isUndo, isUpdate, validActor, validPost } from './type.js';
import { ApNoteService } from './models/ApNoteService.js';
import { ApLoggerService } from './ApLoggerService.js';
Expand All @@ -36,9 +37,8 @@ import { ApResolverService } from './ApResolverService.js';
import { ApAudienceService } from './ApAudienceService.js';
import { ApPersonService } from './models/ApPersonService.js';
import { ApQuestionService } from './models/ApQuestionService.js';
import { GlobalEventService } from '@/core/GlobalEventService.js';
import type { Resolver } from './ApResolverService.js';
import type { IAccept, IAdd, IAnnounce, IBlock, ICreate, IDelete, IFlag, IFollow, ILike, IObject, IReject, IRemove, IUndo, IUpdate, IMove } from './type.js';
import type { IAccept, IAdd, IAnnounce, IBlock, ICreate, IDelete, IFlag, IFollow, ILike, IObject, IReject, IRemove, IUndo, IUpdate, IMove, IPost } from './type.js';

@Injectable()
export class ApInboxService {
Expand Down Expand Up @@ -90,13 +90,15 @@ export class ApInboxService {
}

@bindThis
public async performActivity(actor: MiRemoteUser, activity: IObject): Promise<void> {
public async performActivity(actor: MiRemoteUser, activity: IObject): Promise<string | void> {
let result = undefined as string | void;
if (isCollectionOrOrderedCollection(activity)) {
const results = [] as [string, string | void][];
const resolver = this.apResolverService.createResolver();
for (const item of toArray(isCollection(activity) ? activity.items : activity.orderedItems)) {
const act = await resolver.resolve(item);
try {
await this.performOneActivity(actor, act);
results.push([getApId(item), await this.performOneActivity(actor, act)]);
} catch (err) {
if (err instanceof Error || typeof err === 'string') {
this.logger.error(err);
Expand All @@ -105,8 +107,13 @@ export class ApInboxService {
}
}
}

const hasReason = results.some(([, reason]) => (reason != null && !reason.startsWith('ok')));
if (hasReason) {
result = results.map(([id, reason]) => `${id}: ${reason}`).join('\n');
}
} else {
await this.performOneActivity(actor, activity);
result = await this.performOneActivity(actor, activity);
}

// ついでにリモートユーザーの情報が古かったら更新しておく
Expand All @@ -117,42 +124,43 @@ export class ApInboxService {
});
}
}
return result;
}

@bindThis
public async performOneActivity(actor: MiRemoteUser, activity: IObject): Promise<void> {
public async performOneActivity(actor: MiRemoteUser, activity: IObject): Promise<string | void> {
if (actor.isSuspended) return;

if (isCreate(activity)) {
await this.create(actor, activity);
return await this.create(actor, activity);
syuilo marked this conversation as resolved.
Show resolved Hide resolved
} else if (isDelete(activity)) {
await this.delete(actor, activity);
return await this.delete(actor, activity);
} else if (isUpdate(activity)) {
await this.update(actor, activity);
return await this.update(actor, activity);
} else if (isFollow(activity)) {
await this.follow(actor, activity);
return await this.follow(actor, activity);
} else if (isAccept(activity)) {
await this.accept(actor, activity);
return await this.accept(actor, activity);
} else if (isReject(activity)) {
await this.reject(actor, activity);
return await this.reject(actor, activity);
} else if (isAdd(activity)) {
await this.add(actor, activity).catch(err => this.logger.error(err));
return await this.add(actor, activity);
} else if (isRemove(activity)) {
await this.remove(actor, activity).catch(err => this.logger.error(err));
return await this.remove(actor, activity);
} else if (isAnnounce(activity)) {
await this.announce(actor, activity);
return await this.announce(actor, activity);
} else if (isLike(activity)) {
await this.like(actor, activity);
return await this.like(actor, activity);
} else if (isUndo(activity)) {
await this.undo(actor, activity);
return await this.undo(actor, activity);
} else if (isBlock(activity)) {
await this.block(actor, activity);
return await this.block(actor, activity);
} else if (isFlag(activity)) {
await this.flag(actor, activity);
return await this.flag(actor, activity);
} else if (isMove(activity)) {
await this.move(actor, activity);
return await this.move(actor, activity);
} else {
this.logger.warn(`unrecognized activity type: ${activity.type}`);
return `unrecognized activity type: ${activity.type}`;
}
}

Expand Down Expand Up @@ -234,38 +242,49 @@ export class ApInboxService {
}

@bindThis
private async add(actor: MiRemoteUser, activity: IAdd): Promise<void> {
private async add(actor: MiRemoteUser, activity: IAdd): Promise<string | void> {
if (actor.uri !== activity.actor) {
throw new Error('invalid actor');
return 'invalid actor';
}

if (activity.target == null) {
throw new Error('target is null');
return 'target is null';
}

if (activity.target === actor.featured) {
const note = await this.apNoteService.resolveNote(activity.object);
if (note == null) throw new Error('note not found');
if (note == null) return 'note not found';
await this.notePiningService.addPinned(actor, note.id);
return;
}

throw new Error(`unknown target: ${activity.target}`);
return `unknown target: ${activity.target}`;
}

@bindThis
private async announce(actor: MiRemoteUser, activity: IAnnounce): Promise<void> {
private async announce(actor: MiRemoteUser, activity: IAnnounce): Promise<string | void> {
const uri = getApId(activity);

this.logger.info(`Announce: ${uri}`);

const resolver = this.apResolverService.createResolver();

if (!activity.object) return 'skip: activity has no object property';
const targetUri = getApId(activity.object);
if (targetUri.startsWith('bear:')) return 'skip: bearcaps url not supported.';

const target = await resolver.resolve(activity.object).catch(e => {
this.logger.error(`Resolution failed: ${e}`);
return e;
});

if (isPost(target)) return await this.announceNote(actor, activity, target);

await this.announceNote(actor, activity, targetUri);
return `skip: unknown object type ${getApType(target)}`;
}

@bindThis
private async announceNote(actor: MiRemoteUser, activity: IAnnounce, targetUri: string): Promise<void> {
private async announceNote(actor: MiRemoteUser, activity: IAnnounce, target: IPost): Promise<string | void> {
const uri = getApId(activity);

if (actor.isSuspended) {
Expand All @@ -288,24 +307,21 @@ export class ApInboxService {
// Announce対象をresolve
let renote;
try {
renote = await this.apNoteService.resolveNote(targetUri);
if (renote == null) throw new Error('announce target is null');
renote = await this.apNoteService.resolveNote(target);
if (renote == null) return 'announce target is null';
} catch (err) {
// 対象が4xxならスキップ
if (err instanceof StatusError) {
if (!err.isRetryable) {
this.logger.warn(`Ignored announce target ${targetUri} - ${err.statusCode}`);
return;
return `Ignored announce target ${target.id} - ${err.statusCode}`;
}

this.logger.warn(`Error in announce target ${targetUri} - ${err.statusCode}`);
return `Error in announce target ${target.id} - ${err.statusCode}`;
}
throw err;
}

if (!await this.noteEntityService.isVisibleForMe(renote, actor.id)) {
this.logger.warn('skip: invalid actor for this activity');
return;
return 'skip: invalid actor for this activity';
}

this.logger.info(`Creating the (Re)Note: ${uri}`);
Expand All @@ -314,8 +330,7 @@ export class ApInboxService {
const createdAt = activity.published ? new Date(activity.published) : null;

if (createdAt && createdAt < this.idService.parse(renote.id).date) {
this.logger.warn('skip: malformed createdAt');
return;
return 'skip: malformed createdAt';
}

await this.noteCreateService.create(actor, {
Expand Down Expand Up @@ -349,11 +364,15 @@ export class ApInboxService {
}

@bindThis
private async create(actor: MiRemoteUser, activity: ICreate): Promise<void> {
private async create(actor: MiRemoteUser, activity: ICreate): Promise<string | void> {
const uri = getApId(activity);

this.logger.info(`Create: ${uri}`);

if (!activity.object) return 'skip: activity has no object property';
const targetUri = getApId(activity.object);
if (targetUri.startsWith('bear:')) return 'skip: bearcaps url not supported.';

// copy audiences between activity <=> object.
if (typeof activity.object === 'object') {
const to = unique(concat([toArray(activity.to), toArray(activity.object.to)]));
Expand All @@ -380,7 +399,7 @@ export class ApInboxService {
if (isPost(object)) {
await this.createNote(resolver, actor, object, false, activity);
} else {
this.logger.warn(`Unknown type: ${getApType(object)}`);
return `Unknown type: ${getApType(object)}`;
}
}

Expand Down Expand Up @@ -422,7 +441,7 @@ export class ApInboxService {
@bindThis
private async delete(actor: MiRemoteUser, activity: IDelete): Promise<string> {
if (actor.uri !== activity.actor) {
throw new Error('invalid actor');
return 'invalid actor';
}

// 削除対象objectのtype
Expand Down Expand Up @@ -581,29 +600,29 @@ export class ApInboxService {
}

@bindThis
private async remove(actor: MiRemoteUser, activity: IRemove): Promise<void> {
private async remove(actor: MiRemoteUser, activity: IRemove): Promise<string | void> {
if (actor.uri !== activity.actor) {
throw new Error('invalid actor');
return 'invalid actor';
}

if (activity.target == null) {
throw new Error('target is null');
return 'target is null';
}

if (activity.target === actor.featured) {
const note = await this.apNoteService.resolveNote(activity.object);
if (note == null) throw new Error('note not found');
if (note == null) return 'note not found';
await this.notePiningService.removePinned(actor, note.id);
return;
}

throw new Error(`unknown target: ${activity.target}`);
return `unknown target: ${activity.target}`;
}

@bindThis
private async undo(actor: MiRemoteUser, activity: IUndo): Promise<string> {
if (actor.uri !== activity.actor) {
throw new Error('invalid actor');
return 'invalid actor';
}

const uri = activity.id ?? activity;
Expand All @@ -614,7 +633,7 @@ export class ApInboxService {

const object = await resolver.resolve(activity.object).catch(e => {
this.logger.error(`Resolution failed: ${e}`);
throw e;
return e;
});

// don't queue because the sender may attempt again when timeout
Expand Down
8 changes: 4 additions & 4 deletions packages/backend/src/core/activitypub/models/ApNoteService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,20 +81,20 @@ export class ApNoteService {
const expectHost = this.utilityService.extractDbHost(uri);

if (!validPost.includes(getApType(object))) {
return new Error(`invalid Note: invalid object type ${getApType(object)}`);
return new IdentifiableError('d450b8a9-48e4-4dab-ae36-f4db763fda7c', `invalid Note: invalid object type ${getApType(object)}`);
}

if (object.id && this.utilityService.extractDbHost(object.id) !== expectHost) {
return new Error(`invalid Note: id has different host. expected: ${expectHost}, actual: ${this.utilityService.extractDbHost(object.id)}`);
return new IdentifiableError('d450b8a9-48e4-4dab-ae36-f4db763fda7c', `invalid Note: id has different host. expected: ${expectHost}, actual: ${this.utilityService.extractDbHost(object.id)}`);
}

const actualHost = object.attributedTo && this.utilityService.extractDbHost(getOneApId(object.attributedTo));
if (object.attributedTo && actualHost !== expectHost) {
return new Error(`invalid Note: attributedTo has different host. expected: ${expectHost}, actual: ${actualHost}`);
return new IdentifiableError('d450b8a9-48e4-4dab-ae36-f4db763fda7c', `invalid Note: attributedTo has different host. expected: ${expectHost}, actual: ${actualHost}`);
}

if (object.published && !this.idService.isSafeT(new Date(object.published).valueOf())) {
return new Error('invalid Note: published timestamp is malformed');
return new IdentifiableError('d450b8a9-48e4-4dab-ae36-f4db763fda7c', 'invalid Note: published timestamp is malformed');
}

return null;
Expand Down
1 change: 1 addition & 0 deletions packages/backend/src/core/activitypub/type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -328,3 +328,4 @@ export const isAnnounce = (object: IObject): object is IAnnounce => getApType(ob
export const isBlock = (object: IObject): object is IBlock => getApType(object) === 'Block';
export const isFlag = (object: IObject): object is IFlag => getApType(object) === 'Flag';
export const isMove = (object: IObject): object is IMove => getApType(object) === 'Move';
export const isNote = (object: IObject): object is IPost => getApType(object) === 'Note';
13 changes: 11 additions & 2 deletions packages/backend/src/queue/processors/InboxProcessorService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -204,13 +204,22 @@ export class InboxProcessorService {

// アクティビティを処理
try {
await this.apInboxService.performActivity(authUser.user, activity);
const result = await this.apInboxService.performActivity(authUser.user, activity);
if (result && !result.startsWith('ok')) {
this.logger.warn(`inbox activity ignored (maybe): id=${activity.id} reason=${result}`);
return result;
}
} catch (e) {
if (e instanceof IdentifiableError) {
if (e.id === '689ee33f-f97c-479a-ac49-1b9f8140af99') {
return 'blocked notes with prohibited words';
}
if (e.id === '85ab9bd7-3a41-4530-959d-f07073900109') return 'actor has been suspended';
if (e.id === '85ab9bd7-3a41-4530-959d-f07073900109') {
return 'actor has been suspended';
}
if (e.id === 'd450b8a9-48e4-4dab-ae36-f4db763fda7c') { // invalid Note
return e.message;
}
}
throw e;
}
Expand Down
Loading