From fa731ac717eb4a9cec8939c787eb32f3aa1c22cd Mon Sep 17 00:00:00 2001 From: anatawa12 Date: Mon, 31 Mar 2025 13:10:05 +0900 Subject: [PATCH] feat(backend): suspend instance by software --- .../1743403874305-DeliverSuspendedSoftware.js | 16 +++++ packages/backend/package.json | 1 + packages/backend/src/core/UtilityService.ts | 20 +++++- .../core/entities/InstanceEntityService.ts | 5 +- packages/backend/src/models/Meta.ts | 10 +++ .../models/json-schema/federation-instance.ts | 2 +- .../processors/DeliverProcessorService.ts | 13 ++-- .../src/server/api/endpoints/admin/meta.ts | 19 ++++++ .../server/api/endpoints/admin/update-meta.ts | 15 +++++ packages/misskey-js/src/autogen/types.ts | 10 ++- pnpm-lock.yaml | 67 ++++++++++--------- 11 files changed, 137 insertions(+), 41 deletions(-) create mode 100644 packages/backend/migration/1743403874305-DeliverSuspendedSoftware.js diff --git a/packages/backend/migration/1743403874305-DeliverSuspendedSoftware.js b/packages/backend/migration/1743403874305-DeliverSuspendedSoftware.js new file mode 100644 index 0000000000..19983a72bd --- /dev/null +++ b/packages/backend/migration/1743403874305-DeliverSuspendedSoftware.js @@ -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"`); + } +} diff --git a/packages/backend/package.json b/packages/backend/package.json index bcaa6357ce..4f81addf81 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -166,6 +166,7 @@ "rxjs": "7.8.2", "sanitize-html": "2.15.0", "secure-json-parse": "3.0.2", + "semver": "7.7.1", "sharp": "0.33.5", "slacc": "0.0.10", "strict-event-emitter-types": "2.0.0", diff --git a/packages/backend/src/core/UtilityService.ts b/packages/backend/src/core/UtilityService.ts index 23fb928ac9..67ec6cc7b0 100644 --- a/packages/backend/src/core/UtilityService.ts +++ b/packages/backend/src/core/UtilityService.ts @@ -6,10 +6,12 @@ import { URL, domainToASCII } from 'node:url'; import { Inject, Injectable } from '@nestjs/common'; import RE2 from 're2'; +import semver from 'semver'; import { DI } from '@/di-symbols.js'; import type { Config } from '@/config.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() export class UtilityService { @@ -143,4 +145,20 @@ export class UtilityService { const host = this.extractDbHost(uri); return this.isFederationAllowedHost(host); } + + @bindThis + public isDeliverSuspendedSoftware(software: Pick): 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 })); + } + } } diff --git a/packages/backend/src/core/entities/InstanceEntityService.ts b/packages/backend/src/core/entities/InstanceEntityService.ts index 284537b986..3688cfb363 100644 --- a/packages/backend/src/core/entities/InstanceEntityService.ts +++ b/packages/backend/src/core/entities/InstanceEntityService.ts @@ -31,6 +31,7 @@ export class InstanceEntityService { me?: { id: MiUser['id']; } | null | undefined, ): Promise> { const iAmModerator = me ? await this.roleService.isModerator(me as MiUser) : false; + const softwareSuspended = this.utilityService.isDeliverSuspendedSoftware(instance); return { id: instance.id, @@ -41,8 +42,8 @@ export class InstanceEntityService { followingCount: instance.followingCount, followersCount: instance.followersCount, isNotResponding: instance.isNotResponding, - isSuspended: instance.suspensionState !== 'none', - suspensionState: instance.suspensionState, + isSuspended: instance.suspensionState !== 'none' || Boolean(softwareSuspended), + suspensionState: instance.suspensionState === 'none' && softwareSuspended ? 'softwareSuspended' : instance.suspensionState, isBlocked: this.utilityService.isBlockedHost(this.meta.blockedHosts, instance.host), softwareName: instance.softwareName, softwareVersion: instance.softwareVersion, diff --git a/packages/backend/src/models/Meta.ts b/packages/backend/src/models/Meta.ts index 1fbf5371bc..46f3b2e3c0 100644 --- a/packages/backend/src/models/Meta.ts +++ b/packages/backend/src/models/Meta.ts @@ -664,4 +664,14 @@ export class MiMeta { nullable: true, }) public googleAnalyticsMeasurementId: string | null; + + @Column('jsonb', { + default: [], + }) + public deliverSuspendedSoftware: SoftwareSuspension[]; } + +export type SoftwareSuspension = { + software: string, + versionRange: string, +}; diff --git a/packages/backend/src/models/json-schema/federation-instance.ts b/packages/backend/src/models/json-schema/federation-instance.ts index 912a0399d8..85f84952f1 100644 --- a/packages/backend/src/models/json-schema/federation-instance.ts +++ b/packages/backend/src/models/json-schema/federation-instance.ts @@ -48,7 +48,7 @@ export const packedFederationInstanceSchema = { suspensionState: { type: 'string', nullable: false, optional: false, - enum: ['none', 'manuallySuspended', 'goneSuspended', 'autoSuspendedForNotResponding'], + enum: ['none', 'manuallySuspended', 'goneSuspended', 'autoSuspendedForNotResponding', 'softwareSuspended'], }, isBlocked: { type: 'boolean', diff --git a/packages/backend/src/queue/processors/DeliverProcessorService.ts b/packages/backend/src/queue/processors/DeliverProcessorService.ts index 5a16496011..391ccdac05 100644 --- a/packages/backend/src/queue/processors/DeliverProcessorService.ts +++ b/packages/backend/src/queue/processors/DeliverProcessorService.ts @@ -71,6 +71,15 @@ export class DeliverProcessorService { 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 { 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 process.nextTick(async () => { - const i = await (this.meta.enableStatsForFederatedInstances - ? this.federatedInstanceService.fetchOrRegister(host) - : this.federatedInstanceService.fetch(host)); - if (i == null) return; if (i.isNotResponding) { diff --git a/packages/backend/src/server/api/endpoints/admin/meta.ts b/packages/backend/src/server/api/endpoints/admin/meta.ts index 53e2b2b237..4a106e7175 100644 --- a/packages/backend/src/server/api/endpoints/admin/meta.ts +++ b/packages/backend/src/server/api/endpoints/admin/meta.ts @@ -528,6 +528,24 @@ export const meta = { 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; @@ -672,6 +690,7 @@ export default class extends Endpoint { // eslint- urlPreviewSummaryProxyUrl: instance.urlPreviewSummaryProxyUrl, federation: instance.federation, federationHosts: instance.federationHosts, + deliverSuspendedSoftware: instance.deliverSuspendedSoftware, }; }); } diff --git a/packages/backend/src/server/api/endpoints/admin/update-meta.ts b/packages/backend/src/server/api/endpoints/admin/update-meta.ts index bc05587668..31eeaa5e38 100644 --- a/packages/backend/src/server/api/endpoints/admin/update-meta.ts +++ b/packages/backend/src/server/api/endpoints/admin/update-meta.ts @@ -185,6 +185,17 @@ export const paramDef = { type: 'string', }, }, + deliverSuspendedSoftware: { + type: 'array', + items: { + type: 'object', + properties: { + software: { type: 'string' }, + versionRange: { type: 'string' }, + }, + required: ['software', 'versionRange'], + }, + }, }, required: [], } as const; @@ -671,6 +682,10 @@ export default class extends Endpoint { // eslint- set.federation = ps.federation; } + if (ps.deliverSuspendedSoftware !== undefined) { + set.deliverSuspendedSoftware = ps.deliverSuspendedSoftware; + } + if (Array.isArray(ps.federationHosts)) { set.federationHosts = ps.federationHosts.filter(Boolean).map(x => x.toLowerCase()); } diff --git a/packages/misskey-js/src/autogen/types.ts b/packages/misskey-js/src/autogen/types.ts index 57c4824424..bd6ccb31ee 100644 --- a/packages/misskey-js/src/autogen/types.ts +++ b/packages/misskey-js/src/autogen/types.ts @@ -4933,7 +4933,7 @@ export type components = { isNotResponding: boolean; isSuspended: boolean; /** @enum {string} */ - suspensionState: 'none' | 'manuallySuspended' | 'goneSuspended' | 'autoSuspendedForNotResponding'; + suspensionState: 'none' | 'manuallySuspended' | 'goneSuspended' | 'autoSuspendedForNotResponding' | 'softwareSuspended'; isBlocked: boolean; /** @example misskey */ softwareName: string | null; @@ -8662,6 +8662,10 @@ export type operations = { /** @enum {string} */ federation: 'all' | 'specified' | 'none'; federationHosts: string[]; + deliverSuspendedSoftware: { + software: string; + versionRange: string; + }[]; }; }; }; @@ -11007,6 +11011,10 @@ export type operations = { /** @enum {string} */ federation?: 'all' | 'none' | 'specified'; federationHosts?: string[]; + deliverSuspendedSoftware?: { + software: string; + versionRange: string; + }[]; }; }; }; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3034b9bb84..3a97601987 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -389,6 +389,9 @@ importers: secure-json-parse: specifier: 3.0.2 version: 3.0.2 + semver: + specifier: 7.7.1 + version: 7.7.1 sharp: specifier: 0.33.5 version: 0.33.5 @@ -9491,8 +9494,8 @@ packages: engines: {node: '>=10'} hasBin: true - semver@7.6.3: - resolution: {integrity: sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==} + semver@7.7.1: + resolution: {integrity: sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==} engines: {node: '>=10'} hasBin: true @@ -12570,7 +12573,7 @@ snapshots: nopt: 5.0.0 npmlog: 5.0.1 rimraf: 3.0.2 - semver: 7.6.3 + semver: 7.7.1 tar: 6.2.1 transitivePeerDependencies: - encoding @@ -12790,7 +12793,7 @@ snapshots: '@npmcli/fs@3.1.0': dependencies: - semver: 7.6.3 + semver: 7.7.1 '@nuxt/opencollective@0.4.1': dependencies: @@ -12912,7 +12915,7 @@ snapshots: '@opentelemetry/instrumentation': 0.57.1(@opentelemetry/api@1.9.0) '@opentelemetry/semantic-conventions': 1.28.0 forwarded-parse: 2.1.2 - semver: 7.6.3 + semver: 7.7.1 transitivePeerDependencies: - supports-color @@ -13045,7 +13048,7 @@ snapshots: '@types/shimmer': 1.2.0 import-in-the-middle: 1.11.2 require-in-the-middle: 7.3.0 - semver: 7.6.3 + semver: 7.7.1 shimmer: 1.2.1 transitivePeerDependencies: - supports-color @@ -13057,7 +13060,7 @@ snapshots: '@types/shimmer': 1.2.0 import-in-the-middle: 1.11.2 require-in-the-middle: 7.3.0 - semver: 7.6.3 + semver: 7.7.1 shimmer: 1.2.1 transitivePeerDependencies: - supports-color @@ -13069,7 +13072,7 @@ snapshots: '@types/shimmer': 1.2.0 import-in-the-middle: 1.11.2 require-in-the-middle: 7.3.0 - semver: 7.6.3 + semver: 7.7.1 shimmer: 1.2.1 transitivePeerDependencies: - supports-color @@ -14033,7 +14036,7 @@ snapshots: jsdoc-type-pratt-parser: 4.1.0 process: 0.11.10 recast: 0.23.6 - semver: 7.6.3 + semver: 7.7.1 util: 0.12.5 ws: 8.18.1(bufferutil@4.0.9)(utf-8-validate@6.0.5) optionalDependencies: @@ -14188,7 +14191,7 @@ snapshots: fast-glob: 3.3.3 minimatch: 9.0.4 piscina: 4.4.0 - semver: 7.6.3 + semver: 7.7.1 slash: 3.0.0 source-map: 0.7.4 optionalDependencies: @@ -14857,7 +14860,7 @@ snapshots: fast-glob: 3.3.3 is-glob: 4.0.3 minimatch: 9.0.4 - semver: 7.6.3 + semver: 7.7.1 ts-api-utils: 2.0.1(typescript@5.8.2) typescript: 5.8.2 transitivePeerDependencies: @@ -14871,7 +14874,7 @@ snapshots: fast-glob: 3.3.3 is-glob: 4.0.3 minimatch: 9.0.4 - semver: 7.6.3 + semver: 7.7.1 ts-api-utils: 2.0.1(typescript@5.8.2) typescript: 5.8.2 transitivePeerDependencies: @@ -15636,7 +15639,7 @@ snapshots: bin-version-check@5.1.0: dependencies: bin-version: 6.0.0 - semver: 7.6.3 + semver: 7.7.1 semver-truncate: 3.0.0 bin-version@6.0.0: @@ -15747,7 +15750,7 @@ snapshots: ioredis: 5.6.0 msgpackr: 1.11.2 node-abort-controller: 3.1.1 - semver: 7.6.3 + semver: 7.7.1 tslib: 2.8.1 uuid: 9.0.1 transitivePeerDependencies: @@ -16343,7 +16346,7 @@ snapshots: process: 0.11.10 proxy-from-env: 1.0.0 request-progress: 3.0.0 - semver: 7.6.3 + semver: 7.7.1 supports-color: 8.1.1 tmp: 0.2.3 tree-kill: 1.2.2 @@ -16389,7 +16392,7 @@ snapshots: process: 0.11.10 proxy-from-env: 1.0.0 request-progress: 3.0.0 - semver: 7.6.3 + semver: 7.7.1 supports-color: 8.1.1 tmp: 0.2.3 tree-kill: 1.2.2 @@ -16676,7 +16679,7 @@ snapshots: '@one-ini/wasm': 0.1.1 commander: 10.0.1 minimatch: 9.0.1 - semver: 7.6.3 + semver: 7.7.1 ee-first@1.1.1: {} @@ -17063,7 +17066,7 @@ snapshots: natural-compare: 1.4.0 nth-check: 2.1.1 postcss-selector-parser: 6.1.2 - semver: 7.6.3 + semver: 7.7.1 vue-eslint-parser: 10.1.1(eslint@9.22.0) xml-name-validator: 4.0.0 @@ -17425,7 +17428,7 @@ snapshots: process-warning: 4.0.0 rfdc: 1.4.1 secure-json-parse: 3.0.2 - semver: 7.6.3 + semver: 7.7.1 toad-cache: 3.7.0 fastq@1.17.1: @@ -18325,7 +18328,7 @@ snapshots: '@babel/parser': 7.25.6 '@istanbuljs/schema': 0.1.3 istanbul-lib-coverage: 3.2.2 - semver: 7.6.3 + semver: 7.7.1 transitivePeerDependencies: - supports-color @@ -18712,7 +18715,7 @@ snapshots: jest-util: 29.7.0 natural-compare: 1.4.0 pretty-format: 29.7.0 - semver: 7.6.3 + semver: 7.7.1 transitivePeerDependencies: - supports-color @@ -19094,7 +19097,7 @@ snapshots: make-dir@4.0.0: dependencies: - semver: 7.6.3 + semver: 7.7.1 make-fetch-happen@13.0.0: dependencies: @@ -19720,7 +19723,7 @@ snapshots: node-abi@3.62.0: dependencies: - semver: 7.6.3 + semver: 7.7.1 node-abort-controller@3.1.1: {} @@ -19765,7 +19768,7 @@ snapshots: make-fetch-happen: 13.0.0 nopt: 7.2.0 proc-log: 4.2.0 - semver: 7.6.3 + semver: 7.7.1 tar: 6.2.1 which: 4.0.0 transitivePeerDependencies: @@ -19784,7 +19787,7 @@ snapshots: ignore-by-default: 1.0.1 minimatch: 3.1.2 pstree.remy: 1.1.8 - semver: 7.6.3 + semver: 7.7.1 simple-update-notifier: 2.0.0 supports-color: 5.5.0 touch: 3.1.0 @@ -19820,7 +19823,7 @@ snapshots: dependencies: hosted-git-info: 4.1.0 is-core-module: 2.15.1 - semver: 7.6.3 + semver: 7.7.1 validate-npm-package-license: 3.0.4 normalize-path@3.0.0: {} @@ -21027,7 +21030,7 @@ snapshots: semver-truncate@3.0.0: dependencies: - semver: 7.6.3 + semver: 7.7.1 semver@5.7.1: {} @@ -21037,7 +21040,7 @@ snapshots: dependencies: lru-cache: 6.0.0 - semver@7.6.3: {} + semver@7.7.1: {} send@0.19.0: dependencies: @@ -21099,7 +21102,7 @@ snapshots: dependencies: color: 4.2.3 detect-libc: 2.0.3 - semver: 7.6.3 + semver: 7.7.1 optionalDependencies: '@img/sharp-darwin-arm64': 0.33.5 '@img/sharp-darwin-x64': 0.33.5 @@ -21178,7 +21181,7 @@ snapshots: simple-update-notifier@2.0.0: dependencies: - semver: 7.6.3 + semver: 7.7.1 sinon@18.0.1: dependencies: @@ -22205,7 +22208,7 @@ snapshots: vscode-languageclient@9.0.1: dependencies: minimatch: 5.1.2 - semver: 7.6.3 + semver: 7.7.1 vscode-languageserver-protocol: 3.17.5 vscode-languageserver-protocol@3.17.5: @@ -22260,7 +22263,7 @@ snapshots: espree: 10.3.0 esquery: 1.6.0 lodash: 4.17.21 - semver: 7.6.3 + semver: 7.7.1 transitivePeerDependencies: - supports-color