Merge branch 'develop' into feature/2024.10

This commit is contained in:
dakkar 2024-12-05 09:17:55 +00:00
commit 74b9351572
21 changed files with 190 additions and 51 deletions

View file

@ -117,6 +117,7 @@ type Source = {
};
pidFile: string;
filePermissionBits?: string;
};
export type Config = {
@ -215,6 +216,7 @@ export type Config = {
} | undefined;
pidFile: string;
filePermissionBits?: string;
};
const _filename = fileURLToPath(import.meta.url);
@ -351,6 +353,7 @@ export function loadConfig(): Config {
deactivateAntennaThreshold: config.deactivateAntennaThreshold ?? (1000 * 60 * 60 * 24 * 7),
import: config.import,
pidFile: config.pidFile,
filePermissionBits: config.filePermissionBits,
};
}
@ -456,7 +459,10 @@ function applyEnvOverrides(config: Source) {
}
}
const alwaysStrings = { 'chmodSocket': true } as { [key: string]: boolean };
const alwaysStrings: { [key in string]?: boolean } = {
'chmodSocket': true,
'filePermissionBits': true,
};
function _assign(path: (string | number)[], lastStep: string | number, value: string) {
let thisConfig = config as any;
@ -494,7 +500,7 @@ function applyEnvOverrides(config: Source) {
_apply_top(['sentryForBackend', 'enableNodeProfiling']);
_apply_top([['clusterLimit', 'deliverJobConcurrency', 'inboxJobConcurrency', 'relashionshipJobConcurrency', 'deliverJobPerSec', 'inboxJobPerSec', 'relashionshipJobPerSec', 'deliverJobMaxAttempts', 'inboxJobMaxAttempts']]);
_apply_top([['outgoingAddress', 'outgoingAddressFamily', 'proxy', 'proxySmtp', 'mediaProxy', 'proxyRemoteFiles', 'videoThumbnailGenerator']]);
_apply_top([['maxFileSize', 'maxNoteLength', 'maxRemoteNoteLength', 'maxAltTextLength', 'maxRemoteAltTextLength', 'pidFile']]);
_apply_top([['maxFileSize', 'maxNoteLength', 'maxRemoteNoteLength', 'maxAltTextLength', 'maxRemoteAltTextLength', 'pidFile', 'filePermissionBits']]);
_apply_top(['import', ['downloadTimeout', 'maxFileSize']]);
_apply_top([['signToActivityPubGet', 'checkActivityPubGetSignature', 'setupPassword']]);
}

View file

@ -4,7 +4,7 @@
*/
import * as fs from 'node:fs';
import { copyFile, mkdir, unlink, writeFile } from 'node:fs/promises';
import { copyFile, unlink, writeFile, chmod } from 'node:fs/promises';
import * as Path from 'node:path';
import { fileURLToPath } from 'node:url';
import { dirname } from 'node:path';
@ -41,12 +41,20 @@ export class InternalStorageService {
@bindThis
public async saveFromPath(key: string, srcPath: string): Promise<string> {
await copyFile(srcPath, this.resolvePath(key));
return `${this.config.url}/files/${key}`;
return await this.finalizeSavedFile(key);
}
@bindThis
public async saveFromBuffer(key: string, data: Buffer): Promise<string> {
await writeFile(this.resolvePath(key), data);
return await this.finalizeSavedFile(key);
}
private async finalizeSavedFile(key: string): Promise<string> {
if (this.config.filePermissionBits) {
const path = this.resolvePath(key);
await chmod(path, this.config.filePermissionBits);
}
return `${this.config.url}/files/${key}`;
}

View file

@ -4,9 +4,10 @@
*/
import { URL } from 'node:url';
import { toASCII } from 'punycode';
import punycode from 'punycode/punycode.js';
import { Inject, Injectable } from '@nestjs/common';
import RE2 from 're2';
import psl from 'psl';
import { DI } from '@/di-symbols.js';
import type { Config } from '@/config.js';
import { bindThis } from '@/decorators.js';
@ -106,13 +107,13 @@ export class UtilityService {
@bindThis
public toPuny(host: string): string {
return toASCII(host.toLowerCase());
return punycode.toASCII(host.toLowerCase());
}
@bindThis
public toPunyNullable(host: string | null | undefined): string | null {
if (host == null) return null;
return toASCII(host.toLowerCase());
return punycode.toASCII(host.toLowerCase());
}
@bindThis
@ -122,6 +123,27 @@ export class UtilityService {
return host;
}
@bindThis
private specialSuffix(hostname: string): string | null {
// masto.host provides domain names for its clients, we have to
// treat it as if it were a public suffix
const mastoHost = hostname.match(/\.?([a-zA-Z0-9-]+\.masto\.host)$/i);
if (mastoHost) {
return mastoHost[1];
}
return null;
}
@bindThis
public punyHostPSLDomain(url: string): string {
const urlObj = new URL(url);
const hostname = urlObj.hostname;
const domain = this.specialSuffix(hostname) ?? psl.get(hostname) ?? hostname;
const host = `${this.toPuny(domain)}${urlObj.port.length > 0 ? ':' + urlObj.port : ''}`;
return host;
}
@bindThis
public isFederationAllowedHost(host: string): boolean {
if (this.meta.federation === 'none') return false;

View file

@ -32,7 +32,7 @@ import { AbuseReportService } from '@/core/AbuseReportService.js';
import { FederatedInstanceService } from '@/core/FederatedInstanceService.js';
import { fromTuple } from '@/misc/from-tuple.js';
import { IdentifiableError } from '@/misc/identifiable-error.js';
import { getApHrefNullable, getApId, getApIds, getApType, getNullableApId, isAccept, isActor, isAdd, isAnnounce, isApObject, isBlock, isCollection, isCollectionOrOrderedCollection, isCreate, isDelete, isFlag, isFollow, isLike, isMove, isPost, isReject, isRemove, isTombstone, isUndo, isUpdate, validActor, validPost } from './type.js';
import { getApHrefNullable, getApId, getApIds, getApType, getNullableApId, isAccept, isActor, isAdd, isAnnounce, isApObject, isBlock, isCollection, isCollectionOrOrderedCollection, isCreate, isDelete, isFlag, isFollow, isLike, isDislike, isMove, isPost, isReject, isRemove, isTombstone, isUndo, isUpdate, validActor, validPost } from './type.js';
import { ApNoteService } from './models/ApNoteService.js';
import { ApLoggerService } from './ApLoggerService.js';
import { ApDbResolverService } from './ApDbResolverService.js';
@ -41,7 +41,7 @@ import { ApAudienceService } from './ApAudienceService.js';
import { ApPersonService } from './models/ApPersonService.js';
import { ApQuestionService } from './models/ApQuestionService.js';
import type { Resolver } from './ApResolverService.js';
import type { IAccept, IAdd, IAnnounce, IBlock, ICreate, IDelete, IFlag, IFollow, ILike, IObject, IReject, IRemove, IUndo, IUpdate, IMove, IPost } from './type.js';
import type { IAccept, IAdd, IAnnounce, IBlock, ICreate, IDelete, IFlag, IFollow, ILike, IDislike, IObject, IReject, IRemove, IUndo, IUpdate, IMove, IPost } from './type.js';
@Injectable()
export class ApInboxService {
@ -167,6 +167,8 @@ export class ApInboxService {
return await this.announce(actor, activity, resolver);
} else if (isLike(activity)) {
return await this.like(actor, activity, resolver);
} else if (isDislike(activity)) {
return await this.dislike(actor, activity);
} else if (isUndo(activity)) {
return await this.undo(actor, activity, resolver);
} else if (isBlock(activity)) {
@ -221,6 +223,11 @@ export class ApInboxService {
}
}
@bindThis
private async dislike(actor: MiRemoteUser, dislike: IDislike): Promise<string> {
return await this.undoLike(actor, dislike);
}
@bindThis
private async accept(actor: MiRemoteUser, activity: IAccept, resolver?: Resolver): Promise<string> {
const uri = activity.id ?? activity;
@ -783,7 +790,7 @@ export class ApInboxService {
}
@bindThis
private async undoLike(actor: MiRemoteUser, activity: ILike): Promise<string> {
private async undoLike(actor: MiRemoteUser, activity: ILike | IDislike): Promise<string> {
const targetUri = getApId(activity.object);
const note = await this.apNoteService.fetchNote(targetUri);

View file

@ -242,8 +242,10 @@ export class ApRequestService {
const alternate = document.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)) {
return await this.signedGet(href, user, false);
if (href) {
if (this.utilityService.punyHostPSLDomain(url) === this.utilityService.punyHostPSLDomain(href)) {
return await this.signedGet(href, user, false);
}
}
}
} catch (e) {

View file

@ -131,7 +131,7 @@ export class Resolver {
throw new UnrecoverableError(`invalid AP object ${value}: missing id`);
}
if (this.utilityService.punyHost(object.id) !== this.utilityService.punyHost(value)) {
if (this.utilityService.punyHostPSLDomain(object.id) !== this.utilityService.punyHostPSLDomain(value)) {
throw new UnrecoverableError(`invalid AP object ${value}: id ${object.id} has different host`);
}

View file

@ -192,8 +192,8 @@ export class ApNoteService {
throw new UnrecoverableError(`unexpected schema of note.url ${url} in ${entryUri}`);
}
if (this.utilityService.punyHost(url) !== this.utilityService.punyHost(note.id)) {
throw new Error(`note url <> uri host mismatch: ${url} <> ${note.id} in ${entryUri}`);
if (this.utilityService.punyHostPSLDomain(url) !== this.utilityService.punyHostPSLDomain(note.id)) {
throw new UnrecoverableError(`note url <> uri host mismatch: ${url} <> ${note.id} in ${entryUri}`);
}
}
@ -444,7 +444,7 @@ export class ApNoteService {
throw new UnrecoverableError(`unexpected schema of note.url ${url} in ${noteUri}`);
}
if (this.utilityService.punyHost(url) !== this.utilityService.punyHost(note.id)) {
if (this.utilityService.punyHostPSLDomain(url) !== this.utilityService.punyHostPSLDomain(note.id)) {
throw new UnrecoverableError(`note url <> id host mismatch: ${url} <> ${note.id} in ${noteUri}`);
}
}

View file

@ -138,7 +138,7 @@ export class ApPersonService implements OnModuleInit {
*/
@bindThis
private validateActor(x: IObject, uri: string): IActor {
const expectHost = this.utilityService.punyHost(uri);
const expectHost = this.utilityService.punyHostPSLDomain(uri);
if (!isActor(x)) {
throw new UnrecoverableError(`invalid Actor type '${x.type}' in ${uri}`);
@ -152,7 +152,7 @@ export class ApPersonService implements OnModuleInit {
throw new UnrecoverableError(`invalid Actor ${uri} - wrong inbox type`);
}
const inboxHost = this.utilityService.punyHost(x.inbox);
const inboxHost = this.utilityService.punyHostPSLDomain(x.inbox);
if (inboxHost !== expectHost) {
throw new UnrecoverableError(`invalid Actor ${uri} - wrong inbox ${inboxHost}`);
}
@ -160,7 +160,7 @@ export class ApPersonService implements OnModuleInit {
const sharedInboxObject = x.sharedInbox ?? (x.endpoints ? x.endpoints.sharedInbox : undefined);
if (sharedInboxObject != null) {
const sharedInbox = getApId(sharedInboxObject);
if (!(typeof sharedInbox === 'string' && sharedInbox.length > 0 && this.utilityService.punyHost(sharedInbox) === expectHost)) {
if (!(typeof sharedInbox === 'string' && sharedInbox.length > 0 && this.utilityService.punyHostPSLDomain(sharedInbox) === expectHost)) {
throw new UnrecoverableError(`invalid Actor ${uri} - wrong shared inbox ${sharedInbox}`);
}
}
@ -170,7 +170,7 @@ export class ApPersonService implements OnModuleInit {
if (xCollection != null) {
const collectionUri = getApId(xCollection);
if (typeof collectionUri === 'string' && collectionUri.length > 0) {
if (this.utilityService.punyHost(collectionUri) !== expectHost) {
if (this.utilityService.punyHostPSLDomain(collectionUri) !== expectHost) {
throw new UnrecoverableError(`invalid Actor ${uri} - wrong ${collection} ${collectionUri}`);
}
} else if (collectionUri != null) {
@ -202,7 +202,7 @@ export class ApPersonService implements OnModuleInit {
x.summary = truncate(x.summary, summaryLength);
}
const idHost = this.utilityService.punyHost(x.id);
const idHost = this.utilityService.punyHostPSLDomain(x.id);
if (idHost !== expectHost) {
throw new UnrecoverableError(`invalid Actor ${uri} - wrong id ${x.id}`);
}
@ -212,7 +212,7 @@ export class ApPersonService implements OnModuleInit {
throw new UnrecoverableError(`invalid Actor ${uri} - wrong publicKey.id type`);
}
const publicKeyIdHost = this.utilityService.punyHost(x.publicKey.id);
const publicKeyIdHost = this.utilityService.punyHostPSLDomain(x.publicKey.id);
if (publicKeyIdHost !== expectHost) {
throw new UnrecoverableError(`invalid Actor ${uri} - wrong publicKey.id ${x.publicKey.id}`);
}
@ -357,7 +357,7 @@ export class ApPersonService implements OnModuleInit {
throw new UnrecoverableError(`unexpected schema of person url ${url} in ${uri}`);
}
if (this.utilityService.punyHost(url) !== this.utilityService.punyHost(person.id)) {
if (this.utilityService.punyHostPSLDomain(url) !== this.utilityService.punyHostPSLDomain(person.id)) {
throw new UnrecoverableError(`person url <> uri host mismatch: ${url} <> ${person.id} in ${uri}`);
}
}
@ -574,7 +574,7 @@ export class ApPersonService implements OnModuleInit {
throw new UnrecoverableError(`unexpected schema of person url ${url} in ${uri}`);
}
if (this.utilityService.punyHost(url) !== this.utilityService.punyHost(person.id)) {
if (this.utilityService.punyHostPSLDomain(url) !== this.utilityService.punyHostPSLDomain(person.id)) {
throw new UnrecoverableError(`person url <> uri host mismatch: ${url} <> ${person.id} in ${uri}`);
}
}

View file

@ -339,6 +339,10 @@ export interface ILike extends IActivity {
_misskey_reaction?: string;
}
export interface IDislike extends IActivity {
type: 'Dislike';
}
export interface IAnnounce extends IActivity {
type: 'Announce';
}
@ -371,6 +375,7 @@ export const isLike = (object: IObject): object is ILike => {
const type = getApType(object);
return type != null && ['Like', 'EmojiReaction', 'EmojiReact'].includes(type);
};
export const isDislike = (object: IObject): object is IDislike => getApType(object) === 'Dislike';
export const isAnnounce = (object: IObject): object is IAnnounce => getApType(object) === 'Announce';
export const isBlock = (object: IObject): object is IBlock => getApType(object) === 'Block';
export const isFlag = (object: IObject): object is IFlag => getApType(object) === 'Flag';