merge: more robust "delivery suspend software" (!1204)

View MR for information: https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/1204

Approved-by: Hazelnoot <acomputerdog@gmail.com>
Approved-by: Marie <github@yuugi.dev>
This commit is contained in:
Marie 2025-08-08 08:25:20 +00:00
commit 0f6413d4b9
4 changed files with 140 additions and 13 deletions

2
locales/index.d.ts vendored
View file

@ -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;
/**

View file

@ -222,18 +222,37 @@ export class UtilityService {
@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 }));
// a missing name or version is treated as the empty string
const softwareName = software.softwareName ?? '';
const softwareVersion = 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 === softwareName ||
maybeRegexpMatch(x.software, softwareName)
) && (
x.versionRange.trim() === '*' ||
semver.satisfies(softwareVersion, x.versionRange, { includePrerelease: true }) ||
maybeRegexpMatch(x.versionRange, softwareVersion)
)
)
);
}
/**

View file

@ -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<MiMeta>;
beforeAll(async () => {
const app = await Test.createTestingModule({
imports: [GlobalModule, CoreModule],
}).compile();
providers: [MetaService],
})
.overrideProvider(MetaService).useValue({ fetch: jest.fn() })
.compile();
utilityService = app.get<UtilityService>(UtilityService);
meta = app.get<MiMeta>(DI.meta) as jest.Mocked<MiMeta>;
});
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<MiInstance, 'softwareName' | 'softwareVersion'>, 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',
);
});
});
});

View file

@ -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: