From 795b8366b50324a92c39816fb4a3189177175d75 Mon Sep 17 00:00:00 2001 From: anatawa12 Date: Thu, 1 May 2025 17:58:34 +0900 Subject: [PATCH] Block deliver by software (#15727) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(backend): suspend instance by software * feat(frontend): suspend instance by software * docs(chaangelog): 連合先のソフトウェア及びバージョン名により配信停止を行えるようになりました * chore: 例で使うバージョン名を変える * fix: broken lockfile * fix: broken lock file * fix broken lock file * update changelog * fix dependencies * Update CHANGELOG.md --------- Co-authored-by: syuilo <4439005+syuilo@users.noreply.github.com> --- CHANGELOG.md | 1 + locales/index.d.ts | 16 ++++ locales/ja-JP.yml | 4 + .../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 ++++ .../frontend/src/pages/admin/settings.vue | 76 +++++++++++++++++++ packages/frontend/src/pages/instance-info.vue | 4 +- packages/frontend/src/use/use-form.ts | 3 +- packages/frontend/tsconfig.json | 1 + packages/misskey-js/src/autogen/types.ts | 10 ++- pnpm-lock.yaml | 12 +-- 18 files changed, 208 insertions(+), 20 deletions(-) create mode 100644 packages/backend/migration/1743403874305-DeliverSuspendedSoftware.js diff --git a/CHANGELOG.md b/CHANGELOG.md index bc3a322d2b..aba43b9e3a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ ### Server - Enhance: 凍結されたユーザのノートが各種タイムラインで表示されないように `#15775` +- Enhance: 連合先のソフトウェア及びバージョン名により配信停止を行えるように `#15727` ## 2025.4.1 diff --git a/locales/index.d.ts b/locales/index.d.ts index 1ee01d52f4..08b1624e25 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -898,6 +898,10 @@ export interface Locale extends ILocale { * ソフトウェア */ "software": string; + /** + * ソフトウェア名 + */ + "softwareName": string; /** * バージョン */ @@ -5871,6 +5875,10 @@ export interface Locale extends ILocale { * サーバー応答なしのため停止中 */ "autoSuspendedForNotResponding": string; + /** + * 配信停止中のソフトウェアであるため停止中 + */ + "softwareSuspended": string; }; }; "_bubbleGame": { @@ -6356,6 +6364,14 @@ export interface Locale extends ILocale { * 一定期間モデレーターのアクティビティが検出されなかった場合、スパム防止のためこの設定は自動でオフになります。 */ "thisSettingWillAutomaticallyOffWhenModeratorsInactive": string; + /** + * 配信停止中のソフトウェア + */ + "deliverSuspendedSoftware": string; + /** + * 脆弱性などの理由で、サーバーのソフトウェアの名前及びバージョンの範囲を指定して配信を停止できます。このバージョン情報はサーバーが提供したものであり、信頼性は保証されません。バージョン指定には semver の範囲指定が使用できますが、>= 2024.3.1 と指定すると 2024.3.1-custom.0 のようなカスタムバージョンが含まれないため、>= 2024.3.1-0 のように prerelease の指定を行うことを推奨します。 + */ + "deliverSuspendedSoftwareDescription": string; }; "_accountMigration": { /** diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index edb7914110..d962369799 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -220,6 +220,7 @@ silenceThisInstance: "サーバーをサイレンス" mediaSilenceThisInstance: "サーバーをメディアサイレンス" operations: "操作" software: "ソフトウェア" +softwareName: "ソフトウェア名" version: "バージョン" metadata: "メタデータ" withNFiles: "{n}つのファイル" @@ -1477,6 +1478,7 @@ _delivery: manuallySuspended: "手動停止中" goneSuspended: "サーバー削除のため停止中" autoSuspendedForNotResponding: "サーバー応答なしのため停止中" + softwareSuspended: "配信停止中のソフトウェアであるため停止中" _bubbleGame: howToPlay: "遊び方" @@ -1615,6 +1617,8 @@ _serverSettings: openRegistration: "アカウントの作成をオープンにする" openRegistrationWarning: "登録を開放することはリスクが伴います。サーバーを常に監視し、トラブルが発生した際にすぐに対応できる体制がある場合のみオンにすることを推奨します。" thisSettingWillAutomaticallyOffWhenModeratorsInactive: "一定期間モデレーターのアクティビティが検出されなかった場合、スパム防止のためこの設定は自動でオフになります。" + deliverSuspendedSoftware: "配信停止中のソフトウェア" + deliverSuspendedSoftwareDescription: "脆弱性などの理由で、サーバーのソフトウェアの名前及びバージョンの範囲を指定して配信を停止できます。このバージョン情報はサーバーが提供したものであり、信頼性は保証されません。バージョン指定には semver の範囲指定が使用できますが、>= 2024.3.1 と指定すると 2024.3.1-custom.0 のようなカスタムバージョンが含まれないため、>= 2024.3.1-0 のように prerelease の指定を行うことを推奨します。" _accountMigration: moveFrom: "別のアカウントからこのアカウントに移行" 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 c4de44df18..3c6dcc6523 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -169,6 +169,7 @@ "sanitize-html": "2.16.0", "secure-json-parse": "3.0.2", "sharp": "0.34.1", + "semver": "7.7.1", "slacc": "0.0.10", "strict-event-emitter-types": "2.0.0", "stringz": "2.1.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/frontend/src/pages/admin/settings.vue b/packages/frontend/src/pages/admin/settings.vue index d7454882b2..5914e243c6 100644 --- a/packages/frontend/src/pages/admin/settings.vue +++ b/packages/frontend/src/pages/admin/settings.vue @@ -230,6 +230,31 @@ SPDX-License-Identifier: AGPL-3.0-only + + + + + + +
+ {{ i18n.ts._serverSettings.deliverSuspendedSoftwareDescription }} +
+ +
+ + + + + + +
+
+
+
@@ -368,10 +393,12 @@ const urlPreviewForm = useForm({ const federationForm = useForm({ federation: meta.federation, federationHosts: meta.federationHosts.join('\n'), + deliverSuspendedSoftware: meta.deliverSuspendedSoftware, }, async (state) => { await os.apiWithDialog('admin/update-meta', { federation: state.federation, federationHosts: state.federationHosts.split('\n'), + deliverSuspendedSoftware: state.deliverSuspendedSoftware, }); fetchInstance(true); }); @@ -398,4 +425,53 @@ definePage(() => ({ font-size: 0.85em; color: color(from var(--MI_THEME-fg) srgb r g b / 0.75); } + +.metadataRoot { + container-type: inline-size; +} + +.fieldDragItem { + display: flex; + padding: 10px; + align-items: flex-end; + border-radius: 6px; + + /* (drag button) 32px + (drag button margin) 8px + (input width) 200px * 2 + (input gap) 12px = 452px */ + @container (max-width: 452px) { + align-items: center; + } +} + +.dragItemHandle { + cursor: grab; + width: 32px; + height: 32px; + margin: 0 8px 0 0; + opacity: 0.5; + flex-shrink: 0; + + &:active { + cursor: grabbing; + } +} + +.dragItemRemove { + @extend .dragItemHandle; + + color: #ff2a2a; + opacity: 1; + cursor: pointer; + + &:hover, &:focus { + opacity: .7; + } + + &:active { + cursor: pointer; + } +} + +.dragItemForm { + flex-grow: 1; +} diff --git a/packages/frontend/src/pages/instance-info.vue b/packages/frontend/src/pages/instance-info.vue index 28ce02b87c..96a43f67e8 100644 --- a/packages/frontend/src/pages/instance-info.vue +++ b/packages/frontend/src/pages/instance-info.vue @@ -42,7 +42,7 @@ SPDX-License-Identifier: AGPL-3.0-only {{ i18n.ts._delivery.stop }} - {{ i18n.ts._delivery.resume }} + {{ i18n.ts._delivery.resume }} {{ i18n.ts.blockThisInstance }} {{ i18n.ts.silenceThisInstance }} {{ i18n.ts.mediaSilenceThisInstance }} @@ -164,7 +164,7 @@ const tab = ref('overview'); const chartSrc = ref('instance-requests'); const meta = ref(null); const instance = ref(null); -const suspensionState = ref<'none' | 'manuallySuspended' | 'goneSuspended' | 'autoSuspendedForNotResponding'>('none'); +const suspensionState = ref<'none' | 'manuallySuspended' | 'goneSuspended' | 'autoSuspendedForNotResponding' | 'softwareSuspended'>('none'); const isBlocked = ref(false); const isSilenced = ref(false); const isMediaSilenced = ref(false); diff --git a/packages/frontend/src/use/use-form.ts b/packages/frontend/src/use/use-form.ts index 26cca839c3..1c93557413 100644 --- a/packages/frontend/src/use/use-form.ts +++ b/packages/frontend/src/use/use-form.ts @@ -5,6 +5,7 @@ import { computed, reactive, watch } from 'vue'; import type { Reactive } from 'vue'; +import { deepEqual } from '@/utility/deep-equal'; function copy(v: T): T { return JSON.parse(JSON.stringify(v)); @@ -27,7 +28,7 @@ export function useForm>(initialState: T, save: (n watch([currentState, previousState], () => { for (const key in modifiedStates) { - modifiedStates[key] = currentState[key] !== previousState[key]; + modifiedStates[key] = !deepEqual(currentState[key], previousState[key]); } }, { deep: true }); diff --git a/packages/frontend/tsconfig.json b/packages/frontend/tsconfig.json index ee200d6890..9c7c611e5b 100644 --- a/packages/frontend/tsconfig.json +++ b/packages/frontend/tsconfig.json @@ -46,6 +46,7 @@ }, "compileOnSave": false, "include": [ + "./lib/**/*.ts", "./src/**/*.ts", "./src/**/*.vue", "./test/**/*.ts", diff --git a/packages/misskey-js/src/autogen/types.ts b/packages/misskey-js/src/autogen/types.ts index b9d48f02ae..c83e1f1fbe 100644 --- a/packages/misskey-js/src/autogen/types.ts +++ b/packages/misskey-js/src/autogen/types.ts @@ -4989,7 +4989,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; @@ -8765,6 +8765,10 @@ export type operations = { /** @enum {string} */ federation: 'all' | 'specified' | 'none'; federationHosts: string[]; + deliverSuspendedSoftware: { + software: string; + versionRange: string; + }[]; }; }; }; @@ -11431,6 +11435,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 5b3c95d703..0b21d85a28 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -390,6 +390,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.34.1 version: 0.34.1 @@ -9564,11 +9567,6 @@ packages: engines: {node: '>=10'} hasBin: true - semver@7.6.3: - resolution: {integrity: sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==} - engines: {node: '>=10'} - hasBin: true - semver@7.7.1: resolution: {integrity: sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==} engines: {node: '>=10'} @@ -17051,7 +17049,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.3(eslint@9.25.1) xml-name-validator: 4.0.0 @@ -20987,8 +20985,6 @@ snapshots: dependencies: lru-cache: 6.0.0 - semver@7.6.3: {} - semver@7.7.1: {} send@0.19.0: