Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

cleanup(backend): refactor UtilityService #858

Merged
merged 2 commits into from
Dec 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions packages/backend/src/core/CustomEmojiService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -311,7 +311,7 @@ export class CustomEmojiService implements OnApplicationShutdown {
: this.utilityService.isSelfHost(src) ? null // 自ホスト指定
: (src || noteUserHost); // 指定されたホスト || ノートなどの所有者のホスト (こっちがリアクションにマッチすることはない)

host = this.utilityService.toPunyNullable(host);
host = host ? this.utilityService.normalizeHost(host) : null;

return host;
}
Expand All @@ -324,7 +324,7 @@ export class CustomEmojiService implements OnApplicationShutdown {
const name = match[1];

// ホスト正規化
const host = this.utilityService.toPunyNullable(this.normalizeHost(match[2], noteUserHost));
const host = this.normalizeHost(match[2], noteUserHost);

return { name, host };
}
Expand Down
4 changes: 2 additions & 2 deletions packages/backend/src/core/EmailService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -236,7 +236,7 @@ export class EmailService {
}

const emailDomain: string = emailAddress.split('@')[1];
const isBanned = this.utilityService.isBlockedHost(meta.bannedEmailDomains, emailDomain);
const isBanned = this.utilityService.isItemListedIn(emailDomain, meta.bannedEmailDomains);

if (isBanned) {
return {
Expand Down Expand Up @@ -304,7 +304,7 @@ export class EmailService {
reason: 'mx',
};
}
if (json.mx_host?.some(host => this.utilityService.isBlockedHost(meta.bannedEmailDomains, host))) {
if (json.mx_host?.some(host => this.utilityService.isItemListedIn(host, meta.bannedEmailDomains))) {
return {
valid: false,
reason: 'mx',
Expand Down
2 changes: 1 addition & 1 deletion packages/backend/src/core/FederatedInstanceService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ export class FederatedInstanceService implements OnApplicationShutdown {

@bindThis
public async fetch(host: string): Promise<MiInstance> {
host = this.utilityService.toPuny(host);
host = this.utilityService.normalizeHost(host);

const cached = await this.federatedInstanceCache.get(host);
if (cached) return cached;
Expand Down
2 changes: 1 addition & 1 deletion packages/backend/src/core/NoteCreateService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -285,7 +285,7 @@ export class NoteCreateService implements OnApplicationShutdown {
throw new IdentifiableError('689ee33f-f97c-479a-ac49-1b9f8140af99', 'Notes including prohibited words are not allowed.');
}

const inSilencedInstance = this.utilityService.isSilencedHost(meta.silencedHosts, user.host);
const inSilencedInstance = this.utilityService.isItemListedIn(user.host, meta.silencedHosts);

if (data.visibility === 'public' && inSilencedInstance && user.host !== null) {
data.visibility = 'home';
Expand Down
2 changes: 1 addition & 1 deletion packages/backend/src/core/ReactionService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ export class ReactionService {
} else if (_reaction) {
const custom = reaction.match(isCustomEmojiRegexp);
if (custom) {
const reacterHost = this.utilityService.toPunyNullable(user.host);
const reacterHost = user.host ? this.utilityService.normalizeHost(user.host) : null;

const name = custom[1];
const emoji = reacterHost == null
Expand Down
6 changes: 3 additions & 3 deletions packages/backend/src/core/RemoteUserResolveService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,9 +54,7 @@ export class RemoteUserResolveService {
}) as MiLocalUser;
}

host = this.utilityService.toPuny(host);

if (host === this.utilityService.toPuny(this.config.host)) {
if (this.utilityService.isSelfHost(host)) {
this.logger.info(`return local user: ${usernameLower}`);
return await this.usersRepository.findOneBy({ usernameLower, host: IsNull() }).then(u => {
if (u == null) {
Expand All @@ -67,6 +65,8 @@ export class RemoteUserResolveService {
}) as MiLocalUser;
}

host = this.utilityService.normalizeHost(host);

const user = await this.usersRepository.findOneBy({ usernameLower, host }) as MiRemoteUser | null;

const acctLower = `${usernameLower}@${host}`;
Expand Down
2 changes: 1 addition & 1 deletion packages/backend/src/core/SignupService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ export class SignupService {
id: this.idService.gen(),
username: username,
usernameLower: username.toLowerCase(),
host: this.utilityService.toPunyNullable(host),
host: host ? this.utilityService.normalizeHost(host) : null,
token: secret,
isRoot: isTheFirstUser,
}));
Expand Down
2 changes: 1 addition & 1 deletion packages/backend/src/core/UserFollowingService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,7 @@ export class UserFollowingService implements OnModuleInit {
followee.isLocked ||
(followeeProfile.carefulBot && follower.isBot) ||
(this.userEntityService.isLocalUser(follower) && this.userEntityService.isRemoteUser(followee) && process.env.FORCE_FOLLOW_REMOTE_USER_FOR_TESTING !== 'true') ||
(this.userEntityService.isLocalUser(followee) && this.userEntityService.isRemoteUser(follower) && this.utilityService.isSilencedHost((await this.metaService.fetch()).silencedHosts, follower.host))
(this.userEntityService.isLocalUser(followee) && this.userEntityService.isRemoteUser(follower) && this.utilityService.isItemListedIn(follower.host, (await this.metaService.fetch()).silencedHosts))
) {
let autoAccept = false;

Expand Down
50 changes: 14 additions & 36 deletions packages/backend/src/core/UtilityService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@

import { URL } from 'node:url';
import punycode from 'punycode.js';
import { Inject, Injectable } from '@nestjs/common';
import RE2 from 're2';
import { Inject, Injectable } from '@nestjs/common';
import { DI } from '@/di-symbols.js';
import type { Config } from '@/config.js';
import { bindThis } from '@/decorators.js';
Expand All @@ -21,36 +21,26 @@ export class UtilityService {

@bindThis
public getFullApAccount(username: string, host: string | null): string {
return host ? `${username}@${this.toPuny(host)}` : `${username}@${this.toPuny(this.config.host)}`;
return host ? `${username}@${this.normalizeHost(host)}` : `${username}@${this.normalizeHost(this.config.host)}`;
}

@bindThis
public isSelfHost(host: string | null): boolean {
if (host == null) return true;
return this.toPuny(this.config.host) === this.toPuny(host);
return this.normalizeHost(this.config.host) === this.normalizeHost(host);
}

@bindThis
public isUriLocal(uri: string): boolean {
return this.punyHost(uri) === this.toPuny(this.config.host);
}

@bindThis
public isBlockedHost(blockedHosts: string[], host: string | null): boolean {
if (host == null) return false;
return blockedHosts.some(x => `.${host.toLowerCase()}`.endsWith(`.${x}`));
}

@bindThis
public isSilencedHost(silencedHosts: string[] | undefined, host: string | null): boolean {
if (!silencedHosts || host == null) return false;
return silencedHosts.some(x => `.${host.toLowerCase()}`.endsWith(`.${x}`));
return this.normalizeHost(this.config.hostname) === this.extractHost(uri);
}

@bindThis
public isSensitiveMediaHost(sensitiveMediaHosts: string[] | undefined, host: string | null): boolean {
if (!sensitiveMediaHosts || host == null) return false;
return sensitiveMediaHosts.some(x => `.${host.toLowerCase()}`.endsWith(`.${x}`));
public isItemListedIn(item: string | null, list: string[] | undefined): boolean {
if (!list || !item) return false;
list = list.map(x => '.' + this.normalizeHost(x).split(':')[0]);
item = '.' + this.normalizeHost(item).split(':')[0];
return list.some(x => item.endsWith(x));
}

@bindThis
Expand Down Expand Up @@ -93,26 +83,14 @@ export class UtilityService {
}

@bindThis
public extractDbHost(uri: string): string {
const url = new URL(uri);
return this.toPuny(url.host);
}

@bindThis
public toPuny(host: string): string {
return punycode.toASCII(host.toLowerCase());
}

@bindThis
public toPunyNullable(host: string | null | undefined): string | null {
if (host == null) return null;
public normalizeHost(host: string): string {
return punycode.toASCII(host.toLowerCase());
}

@bindThis
public punyHost(url: string): string {
const urlObj = new URL(url);
const host = `${this.toPuny(urlObj.hostname)}${urlObj.port.length > 0 ? ':' + urlObj.port : ''}`;
return host;
public extractHost(uri: string): string {
// ASCII String で返されるので punycode 化はいらない
// ref: https://url.spec.whatwg.org/#host-serializing
return new URL(uri).host;
}
}
5 changes: 3 additions & 2 deletions packages/backend/src/core/activitypub/ApDbResolverService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,9 @@ export class ApDbResolverService implements OnApplicationShutdown {
public parseUri(value: string | IObject): UriParseResult {
const separator = '/';

const uri = new URL(getApId(value));
if (this.utilityService.toPuny(uri.host) !== this.utilityService.toPuny(this.config.host)) {
const apId = getApId(value);
const uri = new URL(apId);
if (!this.utilityService.isUriLocal(apId)) {
return { local: false, uri: uri.href };
}

Expand Down
8 changes: 4 additions & 4 deletions packages/backend/src/core/activitypub/ApInboxService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,12 +100,12 @@ export class ApInboxService {

const items = toArray(isCollection(activity) ? activity.items : activity.orderedItems);
if (items.length >= resolver.getRecursionLimit()) {
throw new Error(`skipping activity: collection would surpass recursion limit: ${this.utilityService.extractDbHost(actor.uri)}`);
throw new Error(`skipping activity: collection would surpass recursion limit: ${this.utilityService.extractHost(actor.uri)}`);
}

for (const item of items) {
const act = await resolver.resolve(item);
if (act.id == null || this.utilityService.extractDbHost(act.id) !== this.utilityService.extractDbHost(actor.uri)) {
if (act.id == null || this.utilityService.extractHost(act.id) !== this.utilityService.extractHost(actor.uri)) {
this.logger.warn('skipping activity: activity id is null or mismatching');
continue;
}
Expand Down Expand Up @@ -310,7 +310,7 @@ export class ApInboxService {

// アナウンス先をブロックしてたら中断
const meta = await this.metaService.fetch();
if (this.utilityService.isBlockedHost(meta.blockedHosts, this.utilityService.extractDbHost(uri))) return 'skip: blocked host';
if (this.utilityService.isItemListedIn(this.utilityService.extractHost(uri), meta.blockedHosts)) return 'skip: blocked host';

const unlock = await this.appLockService.getApLock(uri);

Expand Down Expand Up @@ -432,7 +432,7 @@ export class ApInboxService {
}

if (typeof note.id === 'string') {
if (this.utilityService.extractDbHost(actor.uri) !== this.utilityService.extractDbHost(note.id)) {
if (this.utilityService.extractHost(actor.uri) !== this.utilityService.extractHost(note.id)) {
return 'skip: host in actor.uri !== note.id';
}
} else {
Expand Down
2 changes: 1 addition & 1 deletion packages/backend/src/core/activitypub/ApRequestService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -220,7 +220,7 @@ export class ApRequestService {
const alternate = fragment.querySelector('head > link[rel="alternate"][type="application/activity+json"]');
if (alternate) {
const href = alternate.getAttribute('href');
if (href && this.utilityService.punyHost(url) === this.utilityService.punyHost(href)) {
if (href && this.utilityService.extractHost(url) === this.utilityService.extractHost(href)) {
return await this.signedGet(href, user, false);
}
}
Expand Down
8 changes: 4 additions & 4 deletions packages/backend/src/core/activitypub/ApResolverService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,18 +89,18 @@ export class Resolver {
}

if (this.history.size > this.recursionLimit) {
throw new Error(`hit recursion limit: ${this.utilityService.extractDbHost(value)}`);
throw new Error(`hit recursion limit: ${this.utilityService.extractHost(value)}`);
}

this.history.add(value);

const host = this.utilityService.extractDbHost(value);
const host = this.utilityService.extractHost(value);
if (this.utilityService.isSelfHost(host)) {
return await this.resolveLocal(value);
}

const meta = await this.metaService.fetch();
if (this.utilityService.isBlockedHost(meta.blockedHosts, host)) {
if (this.utilityService.isItemListedIn(host, meta.blockedHosts)) {
throw new Error('Instance is blocked');
}

Expand Down Expand Up @@ -128,7 +128,7 @@ export class Resolver {
throw new Error('invalid AP object: missing id');
}

if (this.utilityService.punyHost(object.id) !== this.utilityService.punyHost(value)) {
if (this.utilityService.extractHost(object.id) !== this.utilityService.extractHost(value)) {
throw new Error(`invalid AP object ${value}: id ${object.id} has different host`);
}

Expand Down
25 changes: 13 additions & 12 deletions packages/backend/src/core/activitypub/models/ApNoteService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,20 +81,21 @@ export class ApNoteService {

@bindThis
public validateNote(object: IObject, uri: string, actor?: MiRemoteUser): Error | null {
const expectHost = this.utilityService.extractDbHost(uri);
const expectedHost = this.utilityService.extractHost(uri);
const apType = getApType(object);

if (apType == null || !validPost.includes(apType)) {
return new IdentifiableError('d450b8a9-48e4-4dab-ae36-f4db763fda7c', `invalid Note: invalid object type ${apType ?? 'undefined'}`);
}

if (object.id && this.utilityService.extractDbHost(object.id) !== expectHost) {
return new IdentifiableError('d450b8a9-48e4-4dab-ae36-f4db763fda7c', `invalid Note: id has different host. expected: ${expectHost}, actual: ${this.utilityService.extractDbHost(object.id)}`);
let actualHost = object.id && this.utilityService.extractHost(object.id);
if (actualHost && expectedHost !== actualHost) {
return new IdentifiableError('d450b8a9-48e4-4dab-ae36-f4db763fda7c', `invalid Note: id has different host. expected: ${expectedHost}, actual: ${actualHost}`);
}

const actualHost = object.attributedTo && this.utilityService.extractDbHost(getOneApId(object.attributedTo));
if (object.attributedTo && actualHost !== expectHost) {
return new IdentifiableError('d450b8a9-48e4-4dab-ae36-f4db763fda7c', `invalid Note: attributedTo has different host. expected: ${expectHost}, actual: ${actualHost}`);
actualHost = object.attributedTo && this.utilityService.extractHost(getOneApId(object.attributedTo));
if (actualHost && expectedHost !== actualHost) {
return new IdentifiableError('d450b8a9-48e4-4dab-ae36-f4db763fda7c', `invalid Note: attributedTo has different host. expected: ${expectedHost}, actual: ${actualHost}`);
}

if (object.published && !this.idService.isSafeT(new Date(object.published).valueOf())) {
Expand Down Expand Up @@ -165,8 +166,8 @@ export class ApNoteService {
throw new Error('unexpected schema of note url: ' + url);
}

if (this.utilityService.punyHost(url) !== this.utilityService.punyHost(note.id)) {
throw new Error(`note url & uri host mismatch: note url: ${url}, note uri: ${note.id}`);
if (this.utilityService.extractHost(note.id) !== this.utilityService.extractHost(url)) {
throw new Error(`note id and url have different host: ${note.id} - ${url}`);
}
}

Expand Down Expand Up @@ -234,7 +235,7 @@ export class ApNoteService {
}
}

const isSensitiveMediaHost = this.utilityService.isSensitiveMediaHost(meta.sensitiveMediaHosts, this.utilityService.extractDbHost(note.id ?? entryUri));
const isSensitiveMediaHost = this.utilityService.isItemListedIn(this.utilityService.extractHost(note.id ?? entryUri), meta.sensitiveMediaHosts);

// 添付ファイル
const files: MiDriveFile[] = [];
Expand Down Expand Up @@ -349,7 +350,7 @@ export class ApNoteService {
this.logger.info('The note is already inserted while creating itself, reading again');
const duplicate = await this.fetchNote(value);
if (!duplicate) {
throw new Error('The note creation failed with duplication error even when there is no duplication');
throw new Error(`The note creation failed with duplication error even when there is no duplication: ${entryUri}`);
}
return duplicate;
}
Expand All @@ -367,7 +368,7 @@ export class ApNoteService {

// ブロックしていたら中断
const meta = await this.metaService.fetch();
if (this.utilityService.isBlockedHost(meta.blockedHosts, this.utilityService.extractDbHost(uri))) {
if (this.utilityService.isItemListedIn(this.utilityService.extractHost(uri), meta.blockedHosts)) {
throw new StatusError('blocked host', 451);
}

Expand Down Expand Up @@ -396,7 +397,7 @@ export class ApNoteService {
@bindThis
public async extractEmojis(tags: IObject | IObject[], host: string): Promise<MiEmoji[]> {
// eslint-disable-next-line no-param-reassign
host = this.utilityService.toPuny(host);
host = this.utilityService.normalizeHost(host);

const eomjiTags = toArray(tags).filter(isEmoji);

Expand Down
Loading
Loading