diff --git a/locales/index.d.ts b/locales/index.d.ts index 9c2769ab82..bd7f29810d 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -6406,7 +6406,7 @@ export interface Locale extends ILocale { */ "deliverSuspendedSoftware": string; /** - * 脆弱性などの理由で、サーバーのソフトウェアの名前及びバージョンの範囲を指定して配信を停止できます。このバージョン情報はサーバーが提供したものであり、信頼性は保証されません。バージョン指定には semver の範囲指定が使用できますが、>= 2024.3.1 と指定すると 2024.3.1-custom.0 のようなカスタムバージョンが含まれないため、>= 2024.3.1-0 のように prerelease の指定を行うことを推奨します。 + * You can specify a range of names and versions of the server's software to stop delivery for vulnerability or other reasons. This version information is provided by the server and is not guaranteed to be reliable. A semver range specification can be used to specify the version, but specifying >= 2024.3.1 will not include custom versions such as 2024.3.1-custom.0, so it is recommended that a prerelease specification be used, such as >= 2024.3.1-0. Specifying * will match any name or version, even when the server doesn't provide one. You can also provide a regular expression like /^sharkey-/i or /^1-/ */ "deliverSuspendedSoftwareDescription": string; /** diff --git a/packages/backend/src/core/UtilityService.ts b/packages/backend/src/core/UtilityService.ts index f4149f8037..a19203ef4c 100644 --- a/packages/backend/src/core/UtilityService.ts +++ b/packages/backend/src/core/UtilityService.ts @@ -222,18 +222,37 @@ export class UtilityService { @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 })); + // a missing name or version is treated as the empty string + const instanceName = software.softwareName ?? ''; + const instanceVersion = software.softwareVersion ?? ''; + + function maybeRegexpMatch(test: string, target: string): boolean { + const regexpStrPair = test.trim().match(/^\/(.+)\/(.*)$/); + if (!regexpStrPair) return false; // not a regexp, can't match + + try { + return new RE2(regexpStrPair[1], regexpStrPair[2]).test(target); + } catch (err) { + return false; // not a well-formed regexp, can't match + } } + + // each element of `meta.deliverSuspendedSoftware` can have a + // normal string, a `*`, or a `/regexp/` for software or + // versionRange + return this.meta.deliverSuspendedSoftware.find( + x => ( + ( + x.software.trim() === '*' || + x.software === instanceName || + maybeRegexpMatch(x.software, instanceName) + ) && ( + x.versionRange.trim() === '*' || + semver.satisfies(instanceVersion, x.versionRange, { includePrerelease: true }) || + maybeRegexpMatch(x.versionRange, instanceVersion) + ) + ) + ); } /** diff --git a/packages/backend/test/unit/UtilityService.ts b/packages/backend/test/unit/UtilityService.ts index cb010ff1f9..f4e92b85a7 100644 --- a/packages/backend/test/unit/UtilityService.ts +++ b/packages/backend/test/unit/UtilityService.ts @@ -1,18 +1,30 @@ import * as assert from 'assert'; import { Test } from '@nestjs/testing'; +import { jest } from '@jest/globals'; import { CoreModule } from '@/core/CoreModule.js'; import { UtilityService } from '@/core/UtilityService.js'; +import { MetaService } from '@/core/MetaService.js'; import { GlobalModule } from '@/GlobalModule.js'; +import { MiMeta } from '@/models/_.js'; +import { DI } from '@/di-symbols.js'; +import type { SoftwareSuspension } from '@/models/Meta.js'; +import type { MiInstance } from '@/models/Instance.js'; describe('UtilityService', () => { let utilityService: UtilityService; + let meta: jest.Mocked; beforeAll(async () => { const app = await Test.createTestingModule({ imports: [GlobalModule, CoreModule], - }).compile(); + providers: [MetaService], + }) + .overrideProvider(MetaService).useValue({ fetch: jest.fn() }) + .compile(); + utilityService = app.get(UtilityService); + meta = app.get(DI.meta) as jest.Mocked; }); describe('punyHost', () => { @@ -61,4 +73,99 @@ describe('UtilityService', () => { assert.equal(utilityService.toPuny('www.foo.com:3000'), 'www.foo.com:3000'); }); }); + + describe('isDeliverSuspendedSoftware', () => { + function checkThis(rules: SoftwareSuspension[], target: Pick, expect: boolean, message: string) { + meta.deliverSuspendedSoftware = rules; + const match = !!utilityService.isDeliverSuspendedSoftware(target); + assert.equal(match, expect, message); + } + + test('equality', () => { + checkThis( + [{ software: 'Test', versionRange: '1.2.3' }], + { softwareName: 'Test', softwareVersion: '1.2.3' }, + true, 'straight match', + ); + }); + + test('normal version', () => { + checkThis( + [{ software: 'Test', versionRange: '1.2.3-pre' }], + { softwareName: 'Test', softwareVersion: '1.2.3-pre+g1234' }, + true, 'straight match', + ); + checkThis( + [{ software: 'Test', versionRange: '1.2.3' }], + { softwareName: 'Test', softwareVersion: '1.2.3-pre+g1234' }, + false, 'pre-release', + ); + checkThis( + [{ software: 'Test', versionRange: '>= 1.0.0 < 2.0.0' }], + { softwareName: 'Test', softwareVersion: '1.2.3-pre+g1234' }, + true, 'range', + ); + checkThis( + [{ software: 'Test', versionRange: '*' }], + { softwareName: 'Test', softwareVersion: '1.2.3-pre+g1234' }, + true, 'asterisk', + ); + checkThis( + [{ software: 'Test', versionRange: '/.*/' }], + { softwareName: 'Test', softwareVersion: '1.2.3-pre+g1234' }, + true, 'regexp matching anything', + ); + checkThis( + [{ software: 'Test', versionRange: '/-pre\\b/' }], + { softwareName: 'Test', softwareVersion: '1.2.3-pre+g1234' }, + true, 'regexp matching the version', + ); + }); + + test('no version', () => { + checkThis( + [{ software: 'Test', versionRange: '1.2.3' }], + { softwareName: 'Test', softwareVersion: null }, + false, 'semver', + ); + checkThis( + [{ software: 'Test', versionRange: '*' }], + { softwareName: 'Test', softwareVersion: null }, + true, 'asterisk', + ); + checkThis( + [{ software: 'Test', versionRange: '/^$/' }], + { softwareName: 'Test', softwareVersion: null }, + true, 'regexp matching empty string', + ); + checkThis( + [{ software: 'Test', versionRange: '/.*/' }], + { softwareName: 'Test', softwareVersion: null }, + true, 'regexp matching anything', + ); + }); + + test('bad version', () => { + checkThis( + [{ software: 'Test', versionRange: '1.2.3' }], + { softwareName: 'Test', softwareVersion: '1-2-3' }, + false, "semver can't parse softwareVersion", + ); + checkThis( + [{ software: 'Test', versionRange: '*' }], + { softwareName: 'Test', softwareVersion: '1-2-3' }, + true, 'asterisk', + ); + checkThis( + [{ software: 'Test', versionRange: '/.*/' }], + { softwareName: 'Test', softwareVersion: '1-2-3' }, + true, 'regexp matching anything', + ); + checkThis( + [{ software: 'Test', versionRange: '/^1-2-/' }], + { softwareName: 'Test', softwareVersion: '1-2-3' }, + true, 'regexp matching the version', + ); + }); + }); }); diff --git a/sharkey-locales/en-US.yml b/sharkey-locales/en-US.yml index 99fe75b26b..d8602a1489 100644 --- a/sharkey-locales/en-US.yml +++ b/sharkey-locales/en-US.yml @@ -220,6 +220,7 @@ _serverSettings: inquiryUrlDescription: "Specify the URL of a web page that contains a contact form or the instance operators' contact information." aboutInstance: "About instance" aboutInstanceDescription: "A longer description that will be displayed in the 'Instance Information' page, going in place of the regular instance description. Supports HTML." + deliverSuspendedSoftwareDescription: "You can specify a range of names and versions of the server's software to stop delivery for vulnerability or other reasons. This version information is provided by the server and is not guaranteed to be reliable. A semver range specification can be used to specify the version, but specifying >= 2024.3.1 will not include custom versions such as 2024.3.1-custom.0, so it is recommended that a prerelease specification be used, such as >= 2024.3.1-0. Specifying * will match any name or version, even when the server doesn't provide one. You can also provide a regular expression like /^sharkey-/i or /^1-/" _accountMigration: moveAccountDescription: "This will migrate your account to a different one.\n ・Followers from this account will automatically be migrated to the new account\n ・This account will unfollow all users it is currently following\n ・You will be unable to create new notes etc. on this account\n\nWhile migration of followers is automatic, you must manually prepare some steps to migrate the list of users you are following. To do so, carry out a follows export that you will later import on the new account in the settings menu. The same procedure applies to your lists as well as your muted and blocked users.\n\n(This explanation applies to Sharkey v13.12.0 and later. Other ActivityPub software, such as Mastodon, might function differently.)" _achievements: