Allowlist implementation

This commit is contained in:
jaina heartles 2024-02-24 14:33:12 -08:00
parent d1e8653449
commit 9dcc7645ea
22 changed files with 158 additions and 5 deletions

View file

@ -0,0 +1,17 @@
export class allowlist1635065559969 {
constructor() {
this.name = 'allowlist1635065559969';
}
async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "meta" ADD "allowedHosts" character varying(256) array NOT NULL DEFAULT '{}'::varchar[]`);
await queryRunner.query(`ALTER TABLE "meta" ADD "allowlistMode" boolean NOT NULL DEFAULT false`);
}
async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "allowedHosts"`);
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "allowlistMode"`);
}
}

View file

@ -42,6 +42,12 @@ export class UtilityService {
return silencedHosts.some(x => `.${host.toLowerCase()}`.endsWith(`.${x}`));
}
@bindThis
public isAllowedHost(allowedHosts: string[], host: string | null): boolean {
if (host == null) return false;
return allowedHosts.some(x => `.${host.toLowerCase()}`.endsWith(`.${x}`));
}
@bindThis
public isSensitiveWordIncluded(text: string, sensitiveWords: string[]): boolean {
if (sensitiveWords.length === 0) return false;

View file

@ -269,7 +269,9 @@ export class ApInboxService {
// アナウンス先をブロックしてたら中断
const meta = await this.metaService.fetch();
if (this.utilityService.isBlockedHost(meta.blockedHosts, this.utilityService.extractDbHost(uri))) return;
const dbHost = this.utilityService.extractDbHost(uri);
if (this.utilityService.isBlockedHost(meta.blockedHosts, dbHost)) return;
if (meta.allowlistMode && !this.utilityService.isAllowedHost(meta.allowedHosts, dbHost)) return;
const unlock = await this.appLockService.getApLock(uri);

View file

@ -99,6 +99,10 @@ export class Resolver {
throw new Error('Instance is blocked');
}
if (meta.allowlistMode && !this.utilityService.isAllowedHost(meta.allowedHosts, host)) {
throw new Error('Instance is blocked');
}
if (this.config.signToActivityPubGet && !this.user) {
this.user = await this.instanceActorService.getInstanceActor();
}

View file

@ -532,7 +532,12 @@ export class ApNoteService {
// ブロックしていたら中断
const meta = await this.metaService.fetch();
if (this.utilityService.isBlockedHost(meta.blockedHosts, this.utilityService.extractDbHost(uri))) {
const dbHost = this.utilityService.extractDbHost(uri);
if (this.utilityService.isBlockedHost(meta.blockedHosts, dbHost)) {
throw new StatusError('blocked host', 451);
}
if (meta.allowlistMode && !this.utilityService.isAllowedHost(meta.allowedHosts, dbHost)) {
throw new StatusError('blocked host', 451);
}

View file

@ -61,11 +61,13 @@ export default class FederationChart extends Chart<typeof schema> { // eslint-di
.select('f.followerHost')
.where('f.followerHost IS NOT NULL');
const skipAllowlistQuery = !meta.allowlistMode || meta.allowedHosts.length === 0;
const [sub, pub, pubsub, subActive, pubActive] = await Promise.all([
this.followingsRepository.createQueryBuilder('following')
.select('COUNT(DISTINCT following.followeeHost)')
.where('following.followeeHost IS NOT NULL')
.andWhere(meta.blockedHosts.length === 0 ? '1=1' : 'following.followeeHost NOT ILIKE ANY(ARRAY[:...blocked])', { blocked: meta.blockedHosts.flatMap(x => [x, `%.${x}`]) })
.andWhere(skipAllowlistQuery ? '1=1' : 'following.followeeHost ILIKE ANY(ARRAY[:...allowed])', { allowed: meta.allowedHosts.flatMap(x => [x, `%.${x}`]) })
.andWhere(`following.followeeHost NOT IN (${ suspendedInstancesQuery.getQuery() })`)
.getRawOne()
.then(x => parseInt(x.count, 10)),
@ -73,6 +75,7 @@ export default class FederationChart extends Chart<typeof schema> { // eslint-di
.select('COUNT(DISTINCT following.followerHost)')
.where('following.followerHost IS NOT NULL')
.andWhere(meta.blockedHosts.length === 0 ? '1=1' : 'following.followerHost NOT ILIKE ANY(ARRAY[:...blocked])', { blocked: meta.blockedHosts.flatMap(x => [x, `%.${x}`]) })
.andWhere(skipAllowlistQuery ? '1=1' : 'following.followerHost ILIKE ANY(ARRAY[:...allowed])', { allowed: meta.allowedHosts.flatMap(x => [x, `%.${x}`]) })
.andWhere(`following.followerHost NOT IN (${ suspendedInstancesQuery.getQuery() })`)
.getRawOne()
.then(x => parseInt(x.count, 10)),
@ -80,6 +83,7 @@ export default class FederationChart extends Chart<typeof schema> { // eslint-di
.select('COUNT(DISTINCT following.followeeHost)')
.where('following.followeeHost IS NOT NULL')
.andWhere(meta.blockedHosts.length === 0 ? '1=1' : 'following.followeeHost NOT ILIKE ANY(ARRAY[:...blocked])', { blocked: meta.blockedHosts.flatMap(x => [x, `%.${x}`]) })
.andWhere(skipAllowlistQuery ? '1=1' : 'following.followeeHost ILIKE ANY(ARRAY[:...allowed])', { allowed: meta.allowedHosts.flatMap(x => [x, `%.${x}`]) })
.andWhere(`following.followeeHost NOT IN (${ suspendedInstancesQuery.getQuery() })`)
.andWhere(`following.followeeHost IN (${ pubsubSubQuery.getQuery() })`)
.setParameters(pubsubSubQuery.getParameters())
@ -89,6 +93,7 @@ export default class FederationChart extends Chart<typeof schema> { // eslint-di
.select('COUNT(instance.id)')
.where(`instance.host IN (${ subInstancesQuery.getQuery() })`)
.andWhere(meta.blockedHosts.length === 0 ? '1=1' : 'instance.host NOT ILIKE ANY(ARRAY[:...blocked])', { blocked: meta.blockedHosts.flatMap(x => [x, `%.${x}`]) })
.andWhere(skipAllowlistQuery ? '1=1' : 'instance.host ILIKE ANY(ARRAY[:...allowed])', { allowed: meta.allowedHosts.flatMap(x => [x, `%.${x}`]) })
.andWhere('instance.isSuspended = false')
.andWhere('instance.isNotResponding = false')
.getRawOne()
@ -97,6 +102,7 @@ export default class FederationChart extends Chart<typeof schema> { // eslint-di
.select('COUNT(instance.id)')
.where(`instance.host IN (${ pubInstancesQuery.getQuery() })`)
.andWhere(meta.blockedHosts.length === 0 ? '1=1' : 'instance.host NOT ILIKE ANY(ARRAY[:...blocked])', { blocked: meta.blockedHosts.flatMap(x => [x, `%.${x}`]) })
.andWhere(skipAllowlistQuery ? '1=1' : 'instance.host ILIKE ANY(ARRAY[:...allowed])', { allowed: meta.allowedHosts.flatMap(x => [x, `%.${x}`]) })
.andWhere('instance.isSuspended = false')
.andWhere('instance.isNotResponding = false')
.getRawOne()

View file

@ -35,6 +35,7 @@ export class InstanceEntityService {
isNotResponding: instance.isNotResponding,
isSuspended: instance.isSuspended,
isBlocked: this.utilityService.isBlockedHost(meta.blockedHosts, instance.host),
isAllowed: meta.allowlistMode ? this.utilityService.isAllowedHost(meta.allowedHosts, instance.host) : null,
softwareName: instance.softwareName,
softwareVersion: instance.softwareVersion,
openRegistrations: instance.openRegistrations,

View file

@ -71,6 +71,16 @@ export class MiMeta {
})
public blockedHosts: string[];
@Column('varchar', {
length: 1024, array: true, default: '{}',
})
public allowedHosts: string[];
@Column('boolean', {
default: false,
})
public allowlistMode: boolean;
@Column('varchar', {
length: 1024, array: true, default: '{}',
})

View file

@ -49,6 +49,10 @@ export const packedFederationInstanceSchema = {
type: 'boolean',
optional: false, nullable: false,
},
isAllowed: {
type: 'boolean',
optional: false, nullable: true,
},
softwareName: {
type: 'string',
optional: false, nullable: true,

View file

@ -53,9 +53,13 @@ export class DeliverProcessorService {
// ブロックしてたら中断
const meta = await this.metaService.fetch();
if (this.utilityService.isBlockedHost(meta.blockedHosts, this.utilityService.toPuny(host))) {
const punyHost = this.utilityService.toPuny(host);
if (this.utilityService.isBlockedHost(meta.blockedHosts, punyHost)) {
return 'skip (blocked)';
}
if (meta.allowlistMode && !this.utilityService.isAllowedHost(meta.allowedHosts, punyHost)) {
return 'skip (not allowed)';
}
// isSuspendedなら中断
let suspendedHosts = this.suspendedHostsCache.get();

View file

@ -67,6 +67,10 @@ export class InboxProcessorService {
return `Blocked request: ${host}`;
}
if (meta.allowlistMode && !this.utilityService.isAllowedHost(meta.allowedHosts, host)) {
return `Blocked request: ${host}`;
}
const keyIdLower = signature.keyId.toLowerCase();
if (keyIdLower.startsWith('acct:')) {
return `Old keyId is no longer supported. ${keyIdLower}`;
@ -159,6 +163,9 @@ export class InboxProcessorService {
if (this.utilityService.isBlockedHost(meta.blockedHosts, ldHost)) {
throw new Bull.UnrecoverableError(`Blocked request: ${ldHost}`);
}
if (meta.allowlistMode && !this.utilityService.isAllowedHost(meta.allowedHosts, ldHost)) {
throw new Bull.UnrecoverableError(`Blocked request: ${ldHost}`);
}
} else {
throw new Bull.UnrecoverableError(`skip: http-signature verification failed and no LD-Signature. keyId=${signature.keyId}`);
}

View file

@ -181,6 +181,11 @@ export class ActivityPubServerService {
this.authlogger.warn(`${request.id} ${request.url} instance ${keyHost} is blocked: refuse`);
reply.code(401);
return true;
} else if (meta.allowlistMode && !this.utilityService.isAllowedHost(meta.allowedHosts, keyHost)) {
/* allowlist mode enabled and instance not on allowlist: refuse */
this.authLogger.warn(`${request.id} ${request.url} instance ${keyHost} is not on allowlist: refuse`);
reply.code(401);
return true;
}
// do we know the signer already?

View file

@ -141,6 +141,17 @@ export const meta = {
type: 'string',
},
},
allowedHosts: {
type: 'array',
optional: false, nullable: false,
items: {
type: 'string',
},
},
allowlistMode: {
type: 'boolean',
optional: false, nullable: false,
},
sensitiveWords: {
type: 'array',
optional: false, nullable: false,
@ -503,6 +514,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
pinnedUsers: instance.pinnedUsers,
hiddenTags: instance.hiddenTags,
blockedHosts: instance.blockedHosts,
allowedHosts: instance.allowedHosts,
allowlistMode: instance.allowlistMode,
silencedHosts: instance.silencedHosts,
sensitiveWords: instance.sensitiveWords,
preservedUsernames: instance.preservedUsernames,

View file

@ -36,6 +36,12 @@ export const paramDef = {
type: 'string',
},
},
allowedHosts: {
type: 'array', nullable: true, items: {
type: 'string',
},
},
allowlistMode: { type: 'boolean', nullable: true },
sensitiveWords: {
type: 'array', nullable: true, items: {
type: 'string',
@ -172,6 +178,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
set.blockedHosts = ps.blockedHosts.filter(Boolean).map(x => x.toLowerCase());
}
if (Array.isArray(ps.allowedHosts)) {
set.allowedHosts = ps.allowedHosts.filter(Boolean).map(x => x.toLowerCase());
}
if (ps.allowlistMode !== undefined) {
set.allowlistMode = ps.allowlistMode;
}
if (Array.isArray(ps.sensitiveWords)) {
set.sensitiveWords = ps.sensitiveWords.filter(Boolean);
}

View file

@ -114,7 +114,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private async fetchAny(uri: string, me: MiLocalUser | null | undefined): Promise<SchemaType<typeof meta['res']> | null> {
// ブロックしてたら中断
const fetchedMeta = await this.metaService.fetch();
if (this.utilityService.isBlockedHost(fetchedMeta.blockedHosts, this.utilityService.extractDbHost(uri))) return null;
const dbHost = this.utilityService.extractDbHost(uri);
if (this.utilityService.isBlockedHost(fetchedMeta.blockedHosts, dbHost)) return null;
if (fetchedMeta.allowlistMode && !this.utilityService.isAllowedHost(fetchedMeta.allowedHosts, dbHost)) return null;
let local = await this.mergePack(me, ...await Promise.all([
this.apDbResolverService.getUserFromApId(uri),

View file

@ -34,6 +34,7 @@ export const paramDef = {
properties: {
host: { type: 'string', nullable: true, description: 'Omit or use `null` to not filter by host.' },
blocked: { type: 'boolean', nullable: true },
allowed: { type: 'boolean', nullable: true },
notResponding: { type: 'boolean', nullable: true },
suspended: { type: 'boolean', nullable: true },
silenced: { type: 'boolean', nullable: true },
@ -107,6 +108,15 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
}
}
if (typeof ps.allowed === 'boolean') {
const meta = await this.metaService.fetch(true);
if (ps.allowed) {
query.andWhere(meta.allowedHosts.length === 0 ? '1=0' : 'instance.host IN (:...allows)', { allows: meta.allowedHosts });
} else {
query.andWhere(meta.allowedHosts.length === 0 ? '1=1' : 'instance.host NOT IN (:...allows)', { allows: meta.allowedHosts });
}
}
if (typeof ps.notResponding === 'boolean') {
if (ps.notResponding) {
query.andWhere('instance.isNotResponding = TRUE');

View file

@ -157,6 +157,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
if (note.user?.isSilenced && me && followings && note.userId !== me.id && !followings[note.userId]) return false;
if (note.user?.isSuspended) return false;
if (this.utilityService.isBlockedHost(meta.blockedHosts, note.userHost)) return false;
if (meta.allowlistMode && !this.utilityService.isAllowedHost(meta.allowedHosts, note.userHost)) return false;
if (this.utilityService.isSilencedHost(meta.silencedHosts, note.userHost)) return false;
return true;
});