feat(backend): suspend instance by software

This commit is contained in:
anatawa12 2025-03-31 13:10:05 +09:00
parent 49f1f7194d
commit fa731ac717
No known key found for this signature in database
GPG Key ID: 9CA909848B8E4EA6
11 changed files with 137 additions and 41 deletions

View File

@ -0,0 +1,16 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
export class DeliverSuspendedSoftware1743403874305 {
name = 'DeliverSuspendedSoftware1743403874305'
async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "meta" ADD "deliverSuspendedSoftware" jsonb NOT NULL DEFAULT '[]'`);
}
async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "deliverSuspendedSoftware"`);
}
}

View File

@ -166,6 +166,7 @@
"rxjs": "7.8.2", "rxjs": "7.8.2",
"sanitize-html": "2.15.0", "sanitize-html": "2.15.0",
"secure-json-parse": "3.0.2", "secure-json-parse": "3.0.2",
"semver": "7.7.1",
"sharp": "0.33.5", "sharp": "0.33.5",
"slacc": "0.0.10", "slacc": "0.0.10",
"strict-event-emitter-types": "2.0.0", "strict-event-emitter-types": "2.0.0",

View File

@ -6,10 +6,12 @@
import { URL, domainToASCII } from 'node:url'; import { URL, domainToASCII } from 'node:url';
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import RE2 from 're2'; import RE2 from 're2';
import semver from 'semver';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import type { Config } from '@/config.js'; import type { Config } from '@/config.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { MiMeta } from '@/models/Meta.js'; import { MiMeta, SoftwareSuspension } from '@/models/Meta.js';
import { MiInstance } from '@/models/Instance.js';
@Injectable() @Injectable()
export class UtilityService { export class UtilityService {
@ -143,4 +145,20 @@ export class UtilityService {
const host = this.extractDbHost(uri); const host = this.extractDbHost(uri);
return this.isFederationAllowedHost(host); return this.isFederationAllowedHost(host);
} }
@bindThis
public isDeliverSuspendedSoftware(software: Pick<MiInstance, 'softwareName' | 'softwareVersion'>): SoftwareSuspension | undefined {
if (software.softwareName == null) return undefined;
if (software.softwareVersion == null) {
// software version is null; suspend iff versionRange is *
return this.meta.deliverSuspendedSoftware.find(x =>
x.software === software.softwareName
&& x.versionRange.trim() === '*');
} else {
const softwareVersion = software.softwareVersion;
return this.meta.deliverSuspendedSoftware.find(x =>
x.software === software.softwareName
&& semver.satisfies(softwareVersion, x.versionRange, { includePrerelease: true }));
}
}
} }

View File

@ -31,6 +31,7 @@ export class InstanceEntityService {
me?: { id: MiUser['id']; } | null | undefined, me?: { id: MiUser['id']; } | null | undefined,
): Promise<Packed<'FederationInstance'>> { ): Promise<Packed<'FederationInstance'>> {
const iAmModerator = me ? await this.roleService.isModerator(me as MiUser) : false; const iAmModerator = me ? await this.roleService.isModerator(me as MiUser) : false;
const softwareSuspended = this.utilityService.isDeliverSuspendedSoftware(instance);
return { return {
id: instance.id, id: instance.id,
@ -41,8 +42,8 @@ export class InstanceEntityService {
followingCount: instance.followingCount, followingCount: instance.followingCount,
followersCount: instance.followersCount, followersCount: instance.followersCount,
isNotResponding: instance.isNotResponding, isNotResponding: instance.isNotResponding,
isSuspended: instance.suspensionState !== 'none', isSuspended: instance.suspensionState !== 'none' || Boolean(softwareSuspended),
suspensionState: instance.suspensionState, suspensionState: instance.suspensionState === 'none' && softwareSuspended ? 'softwareSuspended' : instance.suspensionState,
isBlocked: this.utilityService.isBlockedHost(this.meta.blockedHosts, instance.host), isBlocked: this.utilityService.isBlockedHost(this.meta.blockedHosts, instance.host),
softwareName: instance.softwareName, softwareName: instance.softwareName,
softwareVersion: instance.softwareVersion, softwareVersion: instance.softwareVersion,

View File

@ -664,4 +664,14 @@ export class MiMeta {
nullable: true, nullable: true,
}) })
public googleAnalyticsMeasurementId: string | null; public googleAnalyticsMeasurementId: string | null;
@Column('jsonb', {
default: [],
})
public deliverSuspendedSoftware: SoftwareSuspension[];
} }
export type SoftwareSuspension = {
software: string,
versionRange: string,
};

View File

@ -48,7 +48,7 @@ export const packedFederationInstanceSchema = {
suspensionState: { suspensionState: {
type: 'string', type: 'string',
nullable: false, optional: false, nullable: false, optional: false,
enum: ['none', 'manuallySuspended', 'goneSuspended', 'autoSuspendedForNotResponding'], enum: ['none', 'manuallySuspended', 'goneSuspended', 'autoSuspendedForNotResponding', 'softwareSuspended'],
}, },
isBlocked: { isBlocked: {
type: 'boolean', type: 'boolean',

View File

@ -71,6 +71,15 @@ export class DeliverProcessorService {
return 'skip (suspended)'; return 'skip (suspended)';
} }
const i = await (this.meta.enableStatsForFederatedInstances
? this.federatedInstanceService.fetchOrRegister(host)
: this.federatedInstanceService.fetch(host));
// suspend server by software
if (i != null && this.utilityService.isDeliverSuspendedSoftware(i)) {
return 'skip (software suspended)';
}
try { try {
await this.apRequestService.signedPost(job.data.user, job.data.to, job.data.content, job.data.digest); await this.apRequestService.signedPost(job.data.user, job.data.to, job.data.content, job.data.digest);
@ -79,10 +88,6 @@ export class DeliverProcessorService {
// Update instance stats // Update instance stats
process.nextTick(async () => { process.nextTick(async () => {
const i = await (this.meta.enableStatsForFederatedInstances
? this.federatedInstanceService.fetchOrRegister(host)
: this.federatedInstanceService.fetch(host));
if (i == null) return; if (i == null) return;
if (i.isNotResponding) { if (i.isNotResponding) {

View File

@ -528,6 +528,24 @@ export const meta = {
optional: false, nullable: false, optional: false, nullable: false,
}, },
}, },
deliverSuspendedSoftware: {
type: 'array',
optional: false, nullable: false,
items: {
type: 'object',
optional: false, nullable: false,
properties: {
software: {
type: 'string',
optional: false, nullable: false,
},
versionRange: {
type: 'string',
optional: false, nullable: false,
},
},
},
},
}, },
}, },
} as const; } as const;
@ -672,6 +690,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
urlPreviewSummaryProxyUrl: instance.urlPreviewSummaryProxyUrl, urlPreviewSummaryProxyUrl: instance.urlPreviewSummaryProxyUrl,
federation: instance.federation, federation: instance.federation,
federationHosts: instance.federationHosts, federationHosts: instance.federationHosts,
deliverSuspendedSoftware: instance.deliverSuspendedSoftware,
}; };
}); });
} }

View File

@ -185,6 +185,17 @@ export const paramDef = {
type: 'string', type: 'string',
}, },
}, },
deliverSuspendedSoftware: {
type: 'array',
items: {
type: 'object',
properties: {
software: { type: 'string' },
versionRange: { type: 'string' },
},
required: ['software', 'versionRange'],
},
},
}, },
required: [], required: [],
} as const; } as const;
@ -671,6 +682,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
set.federation = ps.federation; set.federation = ps.federation;
} }
if (ps.deliverSuspendedSoftware !== undefined) {
set.deliverSuspendedSoftware = ps.deliverSuspendedSoftware;
}
if (Array.isArray(ps.federationHosts)) { if (Array.isArray(ps.federationHosts)) {
set.federationHosts = ps.federationHosts.filter(Boolean).map(x => x.toLowerCase()); set.federationHosts = ps.federationHosts.filter(Boolean).map(x => x.toLowerCase());
} }

View File

@ -4933,7 +4933,7 @@ export type components = {
isNotResponding: boolean; isNotResponding: boolean;
isSuspended: boolean; isSuspended: boolean;
/** @enum {string} */ /** @enum {string} */
suspensionState: 'none' | 'manuallySuspended' | 'goneSuspended' | 'autoSuspendedForNotResponding'; suspensionState: 'none' | 'manuallySuspended' | 'goneSuspended' | 'autoSuspendedForNotResponding' | 'softwareSuspended';
isBlocked: boolean; isBlocked: boolean;
/** @example misskey */ /** @example misskey */
softwareName: string | null; softwareName: string | null;
@ -8662,6 +8662,10 @@ export type operations = {
/** @enum {string} */ /** @enum {string} */
federation: 'all' | 'specified' | 'none'; federation: 'all' | 'specified' | 'none';
federationHosts: string[]; federationHosts: string[];
deliverSuspendedSoftware: {
software: string;
versionRange: string;
}[];
}; };
}; };
}; };
@ -11007,6 +11011,10 @@ export type operations = {
/** @enum {string} */ /** @enum {string} */
federation?: 'all' | 'none' | 'specified'; federation?: 'all' | 'none' | 'specified';
federationHosts?: string[]; federationHosts?: string[];
deliverSuspendedSoftware?: {
software: string;
versionRange: string;
}[];
}; };
}; };
}; };

View File

@ -389,6 +389,9 @@ importers:
secure-json-parse: secure-json-parse:
specifier: 3.0.2 specifier: 3.0.2
version: 3.0.2 version: 3.0.2
semver:
specifier: 7.7.1
version: 7.7.1
sharp: sharp:
specifier: 0.33.5 specifier: 0.33.5
version: 0.33.5 version: 0.33.5
@ -9491,8 +9494,8 @@ packages:
engines: {node: '>=10'} engines: {node: '>=10'}
hasBin: true hasBin: true
semver@7.6.3: semver@7.7.1:
resolution: {integrity: sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==} resolution: {integrity: sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==}
engines: {node: '>=10'} engines: {node: '>=10'}
hasBin: true hasBin: true
@ -12570,7 +12573,7 @@ snapshots:
nopt: 5.0.0 nopt: 5.0.0
npmlog: 5.0.1 npmlog: 5.0.1
rimraf: 3.0.2 rimraf: 3.0.2
semver: 7.6.3 semver: 7.7.1
tar: 6.2.1 tar: 6.2.1
transitivePeerDependencies: transitivePeerDependencies:
- encoding - encoding
@ -12790,7 +12793,7 @@ snapshots:
'@npmcli/fs@3.1.0': '@npmcli/fs@3.1.0':
dependencies: dependencies:
semver: 7.6.3 semver: 7.7.1
'@nuxt/opencollective@0.4.1': '@nuxt/opencollective@0.4.1':
dependencies: dependencies:
@ -12912,7 +12915,7 @@ snapshots:
'@opentelemetry/instrumentation': 0.57.1(@opentelemetry/api@1.9.0) '@opentelemetry/instrumentation': 0.57.1(@opentelemetry/api@1.9.0)
'@opentelemetry/semantic-conventions': 1.28.0 '@opentelemetry/semantic-conventions': 1.28.0
forwarded-parse: 2.1.2 forwarded-parse: 2.1.2
semver: 7.6.3 semver: 7.7.1
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
@ -13045,7 +13048,7 @@ snapshots:
'@types/shimmer': 1.2.0 '@types/shimmer': 1.2.0
import-in-the-middle: 1.11.2 import-in-the-middle: 1.11.2
require-in-the-middle: 7.3.0 require-in-the-middle: 7.3.0
semver: 7.6.3 semver: 7.7.1
shimmer: 1.2.1 shimmer: 1.2.1
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
@ -13057,7 +13060,7 @@ snapshots:
'@types/shimmer': 1.2.0 '@types/shimmer': 1.2.0
import-in-the-middle: 1.11.2 import-in-the-middle: 1.11.2
require-in-the-middle: 7.3.0 require-in-the-middle: 7.3.0
semver: 7.6.3 semver: 7.7.1
shimmer: 1.2.1 shimmer: 1.2.1
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
@ -13069,7 +13072,7 @@ snapshots:
'@types/shimmer': 1.2.0 '@types/shimmer': 1.2.0
import-in-the-middle: 1.11.2 import-in-the-middle: 1.11.2
require-in-the-middle: 7.3.0 require-in-the-middle: 7.3.0
semver: 7.6.3 semver: 7.7.1
shimmer: 1.2.1 shimmer: 1.2.1
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
@ -14033,7 +14036,7 @@ snapshots:
jsdoc-type-pratt-parser: 4.1.0 jsdoc-type-pratt-parser: 4.1.0
process: 0.11.10 process: 0.11.10
recast: 0.23.6 recast: 0.23.6
semver: 7.6.3 semver: 7.7.1
util: 0.12.5 util: 0.12.5
ws: 8.18.1(bufferutil@4.0.9)(utf-8-validate@6.0.5) ws: 8.18.1(bufferutil@4.0.9)(utf-8-validate@6.0.5)
optionalDependencies: optionalDependencies:
@ -14188,7 +14191,7 @@ snapshots:
fast-glob: 3.3.3 fast-glob: 3.3.3
minimatch: 9.0.4 minimatch: 9.0.4
piscina: 4.4.0 piscina: 4.4.0
semver: 7.6.3 semver: 7.7.1
slash: 3.0.0 slash: 3.0.0
source-map: 0.7.4 source-map: 0.7.4
optionalDependencies: optionalDependencies:
@ -14857,7 +14860,7 @@ snapshots:
fast-glob: 3.3.3 fast-glob: 3.3.3
is-glob: 4.0.3 is-glob: 4.0.3
minimatch: 9.0.4 minimatch: 9.0.4
semver: 7.6.3 semver: 7.7.1
ts-api-utils: 2.0.1(typescript@5.8.2) ts-api-utils: 2.0.1(typescript@5.8.2)
typescript: 5.8.2 typescript: 5.8.2
transitivePeerDependencies: transitivePeerDependencies:
@ -14871,7 +14874,7 @@ snapshots:
fast-glob: 3.3.3 fast-glob: 3.3.3
is-glob: 4.0.3 is-glob: 4.0.3
minimatch: 9.0.4 minimatch: 9.0.4
semver: 7.6.3 semver: 7.7.1
ts-api-utils: 2.0.1(typescript@5.8.2) ts-api-utils: 2.0.1(typescript@5.8.2)
typescript: 5.8.2 typescript: 5.8.2
transitivePeerDependencies: transitivePeerDependencies:
@ -15636,7 +15639,7 @@ snapshots:
bin-version-check@5.1.0: bin-version-check@5.1.0:
dependencies: dependencies:
bin-version: 6.0.0 bin-version: 6.0.0
semver: 7.6.3 semver: 7.7.1
semver-truncate: 3.0.0 semver-truncate: 3.0.0
bin-version@6.0.0: bin-version@6.0.0:
@ -15747,7 +15750,7 @@ snapshots:
ioredis: 5.6.0 ioredis: 5.6.0
msgpackr: 1.11.2 msgpackr: 1.11.2
node-abort-controller: 3.1.1 node-abort-controller: 3.1.1
semver: 7.6.3 semver: 7.7.1
tslib: 2.8.1 tslib: 2.8.1
uuid: 9.0.1 uuid: 9.0.1
transitivePeerDependencies: transitivePeerDependencies:
@ -16343,7 +16346,7 @@ snapshots:
process: 0.11.10 process: 0.11.10
proxy-from-env: 1.0.0 proxy-from-env: 1.0.0
request-progress: 3.0.0 request-progress: 3.0.0
semver: 7.6.3 semver: 7.7.1
supports-color: 8.1.1 supports-color: 8.1.1
tmp: 0.2.3 tmp: 0.2.3
tree-kill: 1.2.2 tree-kill: 1.2.2
@ -16389,7 +16392,7 @@ snapshots:
process: 0.11.10 process: 0.11.10
proxy-from-env: 1.0.0 proxy-from-env: 1.0.0
request-progress: 3.0.0 request-progress: 3.0.0
semver: 7.6.3 semver: 7.7.1
supports-color: 8.1.1 supports-color: 8.1.1
tmp: 0.2.3 tmp: 0.2.3
tree-kill: 1.2.2 tree-kill: 1.2.2
@ -16676,7 +16679,7 @@ snapshots:
'@one-ini/wasm': 0.1.1 '@one-ini/wasm': 0.1.1
commander: 10.0.1 commander: 10.0.1
minimatch: 9.0.1 minimatch: 9.0.1
semver: 7.6.3 semver: 7.7.1
ee-first@1.1.1: {} ee-first@1.1.1: {}
@ -17063,7 +17066,7 @@ snapshots:
natural-compare: 1.4.0 natural-compare: 1.4.0
nth-check: 2.1.1 nth-check: 2.1.1
postcss-selector-parser: 6.1.2 postcss-selector-parser: 6.1.2
semver: 7.6.3 semver: 7.7.1
vue-eslint-parser: 10.1.1(eslint@9.22.0) vue-eslint-parser: 10.1.1(eslint@9.22.0)
xml-name-validator: 4.0.0 xml-name-validator: 4.0.0
@ -17425,7 +17428,7 @@ snapshots:
process-warning: 4.0.0 process-warning: 4.0.0
rfdc: 1.4.1 rfdc: 1.4.1
secure-json-parse: 3.0.2 secure-json-parse: 3.0.2
semver: 7.6.3 semver: 7.7.1
toad-cache: 3.7.0 toad-cache: 3.7.0
fastq@1.17.1: fastq@1.17.1:
@ -18325,7 +18328,7 @@ snapshots:
'@babel/parser': 7.25.6 '@babel/parser': 7.25.6
'@istanbuljs/schema': 0.1.3 '@istanbuljs/schema': 0.1.3
istanbul-lib-coverage: 3.2.2 istanbul-lib-coverage: 3.2.2
semver: 7.6.3 semver: 7.7.1
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
@ -18712,7 +18715,7 @@ snapshots:
jest-util: 29.7.0 jest-util: 29.7.0
natural-compare: 1.4.0 natural-compare: 1.4.0
pretty-format: 29.7.0 pretty-format: 29.7.0
semver: 7.6.3 semver: 7.7.1
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
@ -19094,7 +19097,7 @@ snapshots:
make-dir@4.0.0: make-dir@4.0.0:
dependencies: dependencies:
semver: 7.6.3 semver: 7.7.1
make-fetch-happen@13.0.0: make-fetch-happen@13.0.0:
dependencies: dependencies:
@ -19720,7 +19723,7 @@ snapshots:
node-abi@3.62.0: node-abi@3.62.0:
dependencies: dependencies:
semver: 7.6.3 semver: 7.7.1
node-abort-controller@3.1.1: {} node-abort-controller@3.1.1: {}
@ -19765,7 +19768,7 @@ snapshots:
make-fetch-happen: 13.0.0 make-fetch-happen: 13.0.0
nopt: 7.2.0 nopt: 7.2.0
proc-log: 4.2.0 proc-log: 4.2.0
semver: 7.6.3 semver: 7.7.1
tar: 6.2.1 tar: 6.2.1
which: 4.0.0 which: 4.0.0
transitivePeerDependencies: transitivePeerDependencies:
@ -19784,7 +19787,7 @@ snapshots:
ignore-by-default: 1.0.1 ignore-by-default: 1.0.1
minimatch: 3.1.2 minimatch: 3.1.2
pstree.remy: 1.1.8 pstree.remy: 1.1.8
semver: 7.6.3 semver: 7.7.1
simple-update-notifier: 2.0.0 simple-update-notifier: 2.0.0
supports-color: 5.5.0 supports-color: 5.5.0
touch: 3.1.0 touch: 3.1.0
@ -19820,7 +19823,7 @@ snapshots:
dependencies: dependencies:
hosted-git-info: 4.1.0 hosted-git-info: 4.1.0
is-core-module: 2.15.1 is-core-module: 2.15.1
semver: 7.6.3 semver: 7.7.1
validate-npm-package-license: 3.0.4 validate-npm-package-license: 3.0.4
normalize-path@3.0.0: {} normalize-path@3.0.0: {}
@ -21027,7 +21030,7 @@ snapshots:
semver-truncate@3.0.0: semver-truncate@3.0.0:
dependencies: dependencies:
semver: 7.6.3 semver: 7.7.1
semver@5.7.1: {} semver@5.7.1: {}
@ -21037,7 +21040,7 @@ snapshots:
dependencies: dependencies:
lru-cache: 6.0.0 lru-cache: 6.0.0
semver@7.6.3: {} semver@7.7.1: {}
send@0.19.0: send@0.19.0:
dependencies: dependencies:
@ -21099,7 +21102,7 @@ snapshots:
dependencies: dependencies:
color: 4.2.3 color: 4.2.3
detect-libc: 2.0.3 detect-libc: 2.0.3
semver: 7.6.3 semver: 7.7.1
optionalDependencies: optionalDependencies:
'@img/sharp-darwin-arm64': 0.33.5 '@img/sharp-darwin-arm64': 0.33.5
'@img/sharp-darwin-x64': 0.33.5 '@img/sharp-darwin-x64': 0.33.5
@ -21178,7 +21181,7 @@ snapshots:
simple-update-notifier@2.0.0: simple-update-notifier@2.0.0:
dependencies: dependencies:
semver: 7.6.3 semver: 7.7.1
sinon@18.0.1: sinon@18.0.1:
dependencies: dependencies:
@ -22205,7 +22208,7 @@ snapshots:
vscode-languageclient@9.0.1: vscode-languageclient@9.0.1:
dependencies: dependencies:
minimatch: 5.1.2 minimatch: 5.1.2
semver: 7.6.3 semver: 7.7.1
vscode-languageserver-protocol: 3.17.5 vscode-languageserver-protocol: 3.17.5
vscode-languageserver-protocol@3.17.5: vscode-languageserver-protocol@3.17.5:
@ -22260,7 +22263,7 @@ snapshots:
espree: 10.3.0 espree: 10.3.0
esquery: 1.6.0 esquery: 1.6.0
lodash: 4.17.21 lodash: 4.17.21
semver: 7.6.3 semver: 7.7.1
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color