mirror of
https://activitypub.software/TransFem-org/Sharkey.git
synced 2025-08-21 05:44:48 +00:00
refactor actor validation to reduce code duplication
This commit is contained in:
parent
3dde7f25a6
commit
af967fe6be
6 changed files with 243 additions and 110 deletions
|
@ -10,8 +10,8 @@ import psl from 'psl';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import type { Config } from '@/config.js';
|
import type { Config } from '@/config.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import { MiMeta, SoftwareSuspension } from '@/models/Meta.js';
|
import type { MiMeta } from '@/models/Meta.js';
|
||||||
import { MiInstance } from '@/models/Instance.js';
|
import type { MiInstance } from '@/models/Instance.js';
|
||||||
import { IdentifiableError } from '@/misc/identifiable-error.js';
|
import { IdentifiableError } from '@/misc/identifiable-error.js';
|
||||||
import { EnvService } from '@/core/EnvService.js';
|
import { EnvService } from '@/core/EnvService.js';
|
||||||
|
|
||||||
|
|
|
@ -7,18 +7,29 @@ import { Injectable } from '@nestjs/common';
|
||||||
import { UtilityService } from '@/core/UtilityService.js';
|
import { UtilityService } from '@/core/UtilityService.js';
|
||||||
import { IdentifiableError } from '@/misc/identifiable-error.js';
|
import { IdentifiableError } from '@/misc/identifiable-error.js';
|
||||||
import { toArray } from '@/misc/prelude/array.js';
|
import { toArray } from '@/misc/prelude/array.js';
|
||||||
import { getApId, getOneApHrefNullable, IObject } from '@/core/activitypub/type.js';
|
import { getApId, getNullableApId, getOneApHrefNullable } from '@/core/activitypub/type.js';
|
||||||
|
import type { IObject, IObjectWithId } from '@/core/activitypub/type.js';
|
||||||
|
import { bindThis } from '@/decorators.js';
|
||||||
|
import { renderInlineError } from '@/misc/render-inline-error.js';
|
||||||
|
import { LoggerService } from '@/core/LoggerService.js';
|
||||||
|
import type Logger from '@/logger.js';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ApUtilityService {
|
export class ApUtilityService {
|
||||||
|
private readonly logger: Logger;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly utilityService: UtilityService,
|
private readonly utilityService: UtilityService,
|
||||||
) {}
|
loggerService: LoggerService,
|
||||||
|
) {
|
||||||
|
this.logger = loggerService.getLogger('ap-utility');
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Verifies that the object's ID has the same authority as the provided URL.
|
* Verifies that the object's ID has the same authority as the provided URL.
|
||||||
* Returns on success, throws on any validation error.
|
* Returns on success, throws on any validation error.
|
||||||
*/
|
*/
|
||||||
|
@bindThis
|
||||||
public assertIdMatchesUrlAuthority(object: IObject, url: string): void {
|
public assertIdMatchesUrlAuthority(object: IObject, url: string): void {
|
||||||
// This throws if the ID is missing or invalid, but that's ok.
|
// This throws if the ID is missing or invalid, but that's ok.
|
||||||
// Anonymous objects are impossible to verify, so we don't allow fetching them.
|
// Anonymous objects are impossible to verify, so we don't allow fetching them.
|
||||||
|
@ -34,6 +45,7 @@ export class ApUtilityService {
|
||||||
/**
|
/**
|
||||||
* Checks if two URLs have the same host authority
|
* Checks if two URLs have the same host authority
|
||||||
*/
|
*/
|
||||||
|
@bindThis
|
||||||
public haveSameAuthority(url1: string, url2: string): boolean {
|
public haveSameAuthority(url1: string, url2: string): boolean {
|
||||||
if (url1 === url2) return true;
|
if (url1 === url2) return true;
|
||||||
|
|
||||||
|
@ -51,6 +63,7 @@ export class ApUtilityService {
|
||||||
* @throws {IdentifiableError} if object does not have an ID
|
* @throws {IdentifiableError} if object does not have an ID
|
||||||
* @returns the best URL, or null if none were found
|
* @returns the best URL, or null if none were found
|
||||||
*/
|
*/
|
||||||
|
@bindThis
|
||||||
public findBestObjectUrl(object: IObject): string | null {
|
public findBestObjectUrl(object: IObject): string | null {
|
||||||
const targetUrl = getApId(object);
|
const targetUrl = getApId(object);
|
||||||
const targetAuthority = this.utilityService.punyHostPSLDomain(targetUrl);
|
const targetAuthority = this.utilityService.punyHostPSLDomain(targetUrl);
|
||||||
|
@ -81,6 +94,75 @@ export class ApUtilityService {
|
||||||
|
|
||||||
return acceptableUrls[0]?.url ?? null;
|
return acceptableUrls[0]?.url ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sanitizes an inline / nested Object property within an AP object.
|
||||||
|
*
|
||||||
|
* Returns true if the property contains a valid string URL, object w/ valid ID, or an array containing one of those.
|
||||||
|
* Returns false and erases the property if it doesn't contain a valid value.
|
||||||
|
*
|
||||||
|
* Arrays are automatically flattened.
|
||||||
|
* Falsy values (including null) are collapsed to undefined.
|
||||||
|
* @param obj Object containing the property to validate
|
||||||
|
* @param key Key of the property in obj
|
||||||
|
* @param parentUri URI of the object that contains this inline object.
|
||||||
|
* @param parentHost PSL host of parentUri
|
||||||
|
* @param keyPath If obj is *itself* a nested object, set this to the property path from root to obj (including the trailing '.'). This does not affect the logic, but improves the clarity of logs.
|
||||||
|
*/
|
||||||
|
@bindThis
|
||||||
|
public sanitizeInlineObject<Key extends string>(obj: Partial<Record<Key, string | { id?: string } | (string | { id?: string })[]>>, key: Key, parentUri: string | URL, parentHost: string, keyPath = ''): obj is Partial<Record<Key, string | { id: string }>> {
|
||||||
|
let value: unknown = obj[key];
|
||||||
|
|
||||||
|
// Unpack arrays
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
value = value[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear the value - we'll add it back once we have a confirmed ID
|
||||||
|
obj[key] = undefined;
|
||||||
|
|
||||||
|
// Collapse falsy values to undefined
|
||||||
|
if (!value) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exclude nested arrays
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
this.logger.warn(`Excluding ${keyPath}${key} from object ${parentUri}: nested arrays are prohibited`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exclude incorrect types
|
||||||
|
if (typeof(value) !== 'string' && typeof(value) !== 'object') {
|
||||||
|
this.logger.warn(`Excluding ${keyPath}${key} from object ${parentUri}: incorrect type ${typeof(value)}`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const valueId = getNullableApId(value);
|
||||||
|
if (!valueId) {
|
||||||
|
// Exclude missing ID
|
||||||
|
this.logger.warn(`Excluding ${keyPath}${key} from object ${parentUri}: missing or invalid ID`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const parsed = this.utilityService.assertUrl(valueId);
|
||||||
|
const parsedHost = this.utilityService.punyHostPSLDomain(parsed);
|
||||||
|
if (parsedHost !== parentHost) {
|
||||||
|
// Exclude wrong host
|
||||||
|
this.logger.warn(`Excluding ${keyPath}${key} from object ${parentUri}: wrong host in ${valueId} (got ${parsedHost}, expected ${parentHost})`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
// Exclude invalid URLs
|
||||||
|
this.logger.warn(`Excluding ${keyPath}${key} from object ${parentUri}: invalid URL ${valueId}: ${renderInlineError(err)}`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Success - store the sanitized value and return
|
||||||
|
obj[key] = value as string | IObjectWithId;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function isAcceptableUrlType(type: string | undefined): boolean {
|
function isAcceptableUrlType(type: string | undefined): boolean {
|
||||||
|
|
|
@ -46,7 +46,7 @@ import { verifyFieldLinks } from '@/misc/verify-field-link.js';
|
||||||
import { isRetryableError } from '@/misc/is-retryable-error.js';
|
import { isRetryableError } from '@/misc/is-retryable-error.js';
|
||||||
import { renderInlineError } from '@/misc/render-inline-error.js';
|
import { renderInlineError } from '@/misc/render-inline-error.js';
|
||||||
import { IdentifiableError } from '@/misc/identifiable-error.js';
|
import { IdentifiableError } from '@/misc/identifiable-error.js';
|
||||||
import { getApId, getApType, getNullableApId, isActor, isCollection, isCollectionOrOrderedCollection, isPropertyValue } from '../type.js';
|
import { getApId, getApType, isActor, isCollection, isCollectionOrOrderedCollection, isPropertyValue } from '../type.js';
|
||||||
import { extractApHashtags } from './tag.js';
|
import { extractApHashtags } from './tag.js';
|
||||||
import type { OnModuleInit } from '@nestjs/common';
|
import type { OnModuleInit } from '@nestjs/common';
|
||||||
import type { ApNoteService } from './ApNoteService.js';
|
import type { ApNoteService } from './ApNoteService.js';
|
||||||
|
@ -159,46 +159,32 @@ export class ApPersonService implements OnModuleInit, OnApplicationShutdown {
|
||||||
const parsedUri = this.utilityService.assertUrl(uri);
|
const parsedUri = this.utilityService.assertUrl(uri);
|
||||||
const expectHost = this.utilityService.punyHostPSLDomain(parsedUri);
|
const expectHost = this.utilityService.punyHostPSLDomain(parsedUri);
|
||||||
|
|
||||||
|
// Validate type
|
||||||
if (!isActor(x)) {
|
if (!isActor(x)) {
|
||||||
throw new UnrecoverableError(`invalid Actor ${uri}: unknown type '${x.type}'`);
|
throw new UnrecoverableError(`invalid Actor ${uri}: unknown type '${x.type}'`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!(typeof x.id === 'string' && x.id.length > 0)) {
|
// Validate id
|
||||||
throw new UnrecoverableError(`invalid Actor ${uri}: wrong id type`);
|
if (!x.id) {
|
||||||
|
throw new UnrecoverableError(`invalid Actor ${uri}: missing id`);
|
||||||
|
}
|
||||||
|
if (typeof(x.id) !== 'string') {
|
||||||
|
throw new UnrecoverableError(`invalid Actor ${uri}: wrong id type ${typeof(x.id)}`);
|
||||||
|
}
|
||||||
|
const parsedId = this.utilityService.assertUrl(x.id);
|
||||||
|
const idHost = this.utilityService.punyHostPSLDomain(parsedId);
|
||||||
|
if (idHost !== expectHost) {
|
||||||
|
throw new UnrecoverableError(`invalid Actor ${uri}: wrong host in id ${x.id} (got ${parsedId}, expected ${expectHost})`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!(typeof x.inbox === 'string' && x.inbox.length > 0)) {
|
// Validate inbox
|
||||||
throw new UnrecoverableError(`invalid Actor ${uri}: wrong inbox type`);
|
this.apUtilityService.sanitizeInlineObject(x, 'inbox', parsedUri, expectHost);
|
||||||
}
|
if (!x.inbox || typeof(x.inbox) !== 'string') {
|
||||||
|
throw new UnrecoverableError(`invalid Actor ${uri}: missing or invalid inbox ${x.inbox}`);
|
||||||
this.utilityService.assertUrl(x.inbox);
|
|
||||||
const inboxHost = this.utilityService.punyHostPSLDomain(x.inbox);
|
|
||||||
if (inboxHost !== expectHost) {
|
|
||||||
throw new UnrecoverableError(`invalid Actor ${uri}: wrong inbox host ${inboxHost}`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sanitize sharedInbox
|
// Sanitize sharedInbox
|
||||||
try {
|
this.apUtilityService.sanitizeInlineObject(x, 'sharedInbox', parsedUri, expectHost);
|
||||||
if (x.sharedInbox) {
|
|
||||||
const sharedInbox = getNullableApId(x.sharedInbox);
|
|
||||||
if (sharedInbox) {
|
|
||||||
const parsed = this.utilityService.assertUrl(sharedInbox);
|
|
||||||
if (this.utilityService.punyHostPSLDomain(parsed) !== expectHost) {
|
|
||||||
this.logger.warn(`Excluding sharedInbox for actor ${uri}: wrong host in ${sharedInbox}`);
|
|
||||||
x.sharedInbox = undefined;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
this.logger.warn(`Excluding sharedInbox for actor ${uri}: missing ID`);
|
|
||||||
x.sharedInbox = undefined;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Collapse all falsy values to undefined
|
|
||||||
x.sharedInbox = undefined;
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// Shared inbox is unparseable - strip out
|
|
||||||
x.sharedInbox = undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sanitize endpoints object
|
// Sanitize endpoints object
|
||||||
if (typeof(x.endpoints) === 'object') {
|
if (typeof(x.endpoints) === 'object') {
|
||||||
|
@ -211,94 +197,47 @@ export class ApPersonService implements OnModuleInit, OnApplicationShutdown {
|
||||||
|
|
||||||
// Sanitize endpoints.sharedInbox
|
// Sanitize endpoints.sharedInbox
|
||||||
if (x.endpoints) {
|
if (x.endpoints) {
|
||||||
try {
|
this.apUtilityService.sanitizeInlineObject(x.endpoints, 'sharedInbox', parsedUri, expectHost, 'endpoints.');
|
||||||
if (x.endpoints.sharedInbox) {
|
|
||||||
const sharedInbox = getNullableApId(x.endpoints.sharedInbox);
|
if (!x.endpoints.sharedInbox) {
|
||||||
if (sharedInbox) {
|
x.endpoints = undefined;
|
||||||
const parsed = this.utilityService.assertUrl(sharedInbox);
|
|
||||||
if (this.utilityService.punyHostPSLDomain(parsed) !== expectHost) {
|
|
||||||
this.logger.warn(`Excluding endpoints.sharedInbox for actor ${uri}: wrong host in ${sharedInbox}`);
|
|
||||||
x.endpoints.sharedInbox = undefined;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
this.logger.warn(`Excluding endpoints.sharedInbox for actor ${uri}: missing ID`);
|
|
||||||
x.endpoints.sharedInbox = undefined;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Collapse all falsy values to undefined
|
|
||||||
x.endpoints.sharedInbox = undefined;
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// Shared inbox is unparseable - strip out
|
|
||||||
x.endpoints.sharedInbox = undefined;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sanitize collections
|
// Sanitize collections
|
||||||
for (const collection of ['outbox', 'followers', 'following', 'featured'] as (keyof IActor)[]) {
|
for (const collection of ['outbox', 'followers', 'following', 'featured'] as const) {
|
||||||
try {
|
this.apUtilityService.sanitizeInlineObject(x, collection, parsedUri, expectHost);
|
||||||
if (x[collection]) {
|
|
||||||
const collectionUri = getNullableApId(x[collection]);
|
|
||||||
if (collectionUri) {
|
|
||||||
const parsed = this.utilityService.assertUrl(collectionUri);
|
|
||||||
if (this.utilityService.punyHostPSLDomain(parsed) !== expectHost) {
|
|
||||||
this.logger.warn(`Excluding ${collection} for actor ${uri}: wrong host in ${collectionUri}`);
|
|
||||||
x[collection] = undefined;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
this.logger.warn(`Excluding ${collection} for actor ${uri}: missing ID`);
|
|
||||||
x[collection] = undefined;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Collapse all falsy values to undefined
|
|
||||||
x[collection] = undefined;
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// Collection is unparseable - strip out
|
|
||||||
x[collection] = undefined;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Validate username
|
||||||
if (!(typeof x.preferredUsername === 'string' && x.preferredUsername.length > 0 && x.preferredUsername.length <= 128 && /^\w([\w-.]*\w)?$/.test(x.preferredUsername))) {
|
if (!(typeof x.preferredUsername === 'string' && x.preferredUsername.length > 0 && x.preferredUsername.length <= 128 && /^\w([\w-.]*\w)?$/.test(x.preferredUsername))) {
|
||||||
throw new UnrecoverableError(`invalid Actor ${uri}: wrong username`);
|
throw new UnrecoverableError(`invalid Actor ${uri}: wrong username`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Sanitize name
|
||||||
// These fields are only informational, and some AP software allows these
|
// These fields are only informational, and some AP software allows these
|
||||||
// fields to be very long. If they are too long, we cut them off. This way
|
// fields to be very long. If they are too long, we cut them off. This way
|
||||||
// we can at least see these users and their activities.
|
// we can at least see these users and their activities.
|
||||||
if (x.name) {
|
if (!x.name) {
|
||||||
if (!(typeof x.name === 'string' && x.name.length > 0)) {
|
|
||||||
throw new UnrecoverableError(`invalid Actor ${uri}: wrong name`);
|
|
||||||
}
|
|
||||||
x.name = truncate(x.name, nameLength);
|
|
||||||
} else if (x.name === '') {
|
|
||||||
// Mastodon emits empty string when the name is not set.
|
|
||||||
x.name = undefined;
|
x.name = undefined;
|
||||||
|
} else if (typeof(x.name) !== 'string') {
|
||||||
|
this.logger.warn(`Excluding name from object ${uri}: incorrect type ${typeof(x)}`);
|
||||||
|
x.name = undefined;
|
||||||
|
} else {
|
||||||
|
x.name = truncate(x.name, nameLength);
|
||||||
}
|
}
|
||||||
if (x.summary) {
|
|
||||||
if (!(typeof x.summary === 'string' && x.summary.length > 0)) {
|
// Sanitize summary
|
||||||
throw new UnrecoverableError(`invalid Actor ${uri}: wrong summary`);
|
if (!x.summary) {
|
||||||
}
|
x.summary = undefined;
|
||||||
|
} else if (typeof(x.summary) !== 'string') {
|
||||||
|
this.logger.warn(`Excluding summary from object ${uri}: incorrect type ${typeof(x)}`);
|
||||||
|
} else {
|
||||||
x.summary = truncate(x.summary, summaryLength);
|
x.summary = truncate(x.summary, summaryLength);
|
||||||
}
|
}
|
||||||
|
|
||||||
const parsedId = this.utilityService.assertUrl(x.id);
|
// Sanitize publicKey
|
||||||
const idHost = this.utilityService.punyHostPSLDomain(parsedId);
|
this.apUtilityService.sanitizeInlineObject(x, 'publicKey', parsedUri, expectHost);
|
||||||
if (idHost !== expectHost) {
|
|
||||||
throw new UnrecoverableError(`invalid Actor ${uri}: wrong id ${x.id}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (x.publicKey) {
|
|
||||||
if (typeof x.publicKey.id !== 'string') {
|
|
||||||
throw new UnrecoverableError(`invalid Actor ${uri}: wrong publicKey.id type`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const parsed = this.utilityService.assertUrl(x.publicKey.id);
|
|
||||||
const publicKeyIdHost = this.utilityService.punyHostPSLDomain(parsed);
|
|
||||||
if (publicKeyIdHost !== expectHost) {
|
|
||||||
throw new UnrecoverableError(`invalid Actor ${uri}: wrong publicKey.id ${x.publicKey.id}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return x;
|
return x;
|
||||||
}
|
}
|
||||||
|
|
|
@ -86,7 +86,7 @@ export function getOneApId(value: ApObject): string {
|
||||||
/**
|
/**
|
||||||
* Get ActivityStreams Object id
|
* Get ActivityStreams Object id
|
||||||
*/
|
*/
|
||||||
export function getApId(value: string | IObject | [string | IObject], sourceForLogs?: string): string {
|
export function getApId(value: unknown | [unknown] | unknown[], sourceForLogs?: string): string {
|
||||||
const id = getNullableApId(value);
|
const id = getNullableApId(value);
|
||||||
|
|
||||||
if (id == null) {
|
if (id == null) {
|
||||||
|
@ -102,7 +102,7 @@ export function getApId(value: string | IObject | [string | IObject], sourceForL
|
||||||
/**
|
/**
|
||||||
* Get ActivityStreams Object id, or null if not present
|
* Get ActivityStreams Object id, or null if not present
|
||||||
*/
|
*/
|
||||||
export function getNullableApId(source: string | IObject | [string | IObject]): string | null {
|
export function getNullableApId(source: unknown | [unknown] | unknown[]): string | null {
|
||||||
const value: unknown = fromTuple(source);
|
const value: unknown = fromTuple(source);
|
||||||
|
|
||||||
if (value != null) {
|
if (value != null) {
|
||||||
|
@ -276,7 +276,7 @@ export interface IActor extends IObject {
|
||||||
followers?: string | ICollection | IOrderedCollection;
|
followers?: string | ICollection | IOrderedCollection;
|
||||||
following?: string | ICollection | IOrderedCollection;
|
following?: string | ICollection | IOrderedCollection;
|
||||||
featured?: string | IOrderedCollection;
|
featured?: string | IOrderedCollection;
|
||||||
outbox: string | IOrderedCollection;
|
outbox?: string | IOrderedCollection;
|
||||||
endpoints?: {
|
endpoints?: {
|
||||||
sharedInbox?: string;
|
sharedInbox?: string;
|
||||||
};
|
};
|
||||||
|
|
|
@ -16,6 +16,7 @@ import { HttpRequestService } from '@/core/HttpRequestService.js';
|
||||||
import { LoggerService } from '@/core/LoggerService.js';
|
import { LoggerService } from '@/core/LoggerService.js';
|
||||||
import { UtilityService } from '@/core/UtilityService.js';
|
import { UtilityService } from '@/core/UtilityService.js';
|
||||||
import { IdService } from '@/core/IdService.js';
|
import { IdService } from '@/core/IdService.js';
|
||||||
|
import { EnvService } from '@/core/EnvService.js';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
|
|
||||||
function mockRedis() {
|
function mockRedis() {
|
||||||
|
@ -46,6 +47,7 @@ describe('FetchInstanceMetadataService', () => {
|
||||||
LoggerService,
|
LoggerService,
|
||||||
UtilityService,
|
UtilityService,
|
||||||
IdService,
|
IdService,
|
||||||
|
EnvService,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
.useMocker((token) => {
|
.useMocker((token) => {
|
||||||
|
|
|
@ -7,6 +7,8 @@ import type { IObject } from '@/core/activitypub/type.js';
|
||||||
import type { EnvService } from '@/core/EnvService.js';
|
import type { EnvService } from '@/core/EnvService.js';
|
||||||
import type { MiMeta } from '@/models/Meta.js';
|
import type { MiMeta } from '@/models/Meta.js';
|
||||||
import type { Config } from '@/config.js';
|
import type { Config } from '@/config.js';
|
||||||
|
import type { LoggerService } from '@/core/LoggerService.js';
|
||||||
|
import Logger from '@/logger.js';
|
||||||
import { ApUtilityService } from '@/core/activitypub/ApUtilityService.js';
|
import { ApUtilityService } from '@/core/activitypub/ApUtilityService.js';
|
||||||
import { UtilityService } from '@/core/UtilityService.js';
|
import { UtilityService } from '@/core/UtilityService.js';
|
||||||
|
|
||||||
|
@ -36,7 +38,17 @@ describe(ApUtilityService, () => {
|
||||||
|
|
||||||
const utilityService = new UtilityService(config, meta, envService);
|
const utilityService = new UtilityService(config, meta, envService);
|
||||||
|
|
||||||
serviceUnderTest = new ApUtilityService(utilityService);
|
const loggerService = {
|
||||||
|
getLogger(domain: string) {
|
||||||
|
const logger = new Logger(domain);
|
||||||
|
Object.defineProperty(logger, 'log', {
|
||||||
|
value: () => {},
|
||||||
|
});
|
||||||
|
return logger;
|
||||||
|
},
|
||||||
|
} as unknown as LoggerService;
|
||||||
|
|
||||||
|
serviceUnderTest = new ApUtilityService(utilityService, loggerService);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('assertIdMatchesUrlAuthority', () => {
|
describe('assertIdMatchesUrlAuthority', () => {
|
||||||
|
@ -361,4 +373,102 @@ describe(ApUtilityService, () => {
|
||||||
expect(result).toBe('http://example.com/1');
|
expect(result).toBe('http://example.com/1');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('sanitizeInlineObject', () => {
|
||||||
|
it('should exclude nested arrays', () => {
|
||||||
|
const input = {
|
||||||
|
test: [[]] as unknown as string[],
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = serviceUnderTest.sanitizeInlineObject(input, 'test', 'https://example.com/actor', 'example.com');
|
||||||
|
|
||||||
|
expect(result).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should exclude incorrect type', () => {
|
||||||
|
const input = {
|
||||||
|
test: 0 as unknown as string,
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = serviceUnderTest.sanitizeInlineObject(input, 'test', 'https://example.com/actor', 'example.com');
|
||||||
|
|
||||||
|
expect(result).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should exclude missing ID', () => {
|
||||||
|
const input = {
|
||||||
|
test: {
|
||||||
|
id: undefined,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = serviceUnderTest.sanitizeInlineObject(input, 'test', 'https://example.com/actor', 'example.com');
|
||||||
|
|
||||||
|
expect(result).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should exclude wrong host', () => {
|
||||||
|
const input = {
|
||||||
|
test: 'https://wrong.com/object',
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = serviceUnderTest.sanitizeInlineObject(input, 'test', 'https://example.com/actor', 'example.com');
|
||||||
|
|
||||||
|
expect(result).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should exclude invalid URLs', () => {
|
||||||
|
const input = {
|
||||||
|
test: 'https://user@example.com/object',
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = serviceUnderTest.sanitizeInlineObject(input, 'test', 'https://example.com/actor', 'example.com');
|
||||||
|
|
||||||
|
expect(result).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should accept string', () => {
|
||||||
|
const input = {
|
||||||
|
test: 'https://example.com/object',
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = serviceUnderTest.sanitizeInlineObject(input, 'test', 'https://example.com/actor', 'example.com');
|
||||||
|
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should accept array of string', () => {
|
||||||
|
const input = {
|
||||||
|
test: ['https://example.com/object'],
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = serviceUnderTest.sanitizeInlineObject(input, 'test', 'https://example.com/actor', 'example.com');
|
||||||
|
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should accept object', () => {
|
||||||
|
const input = {
|
||||||
|
test: {
|
||||||
|
id: 'https://example.com/object',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = serviceUnderTest.sanitizeInlineObject(input, 'test', 'https://example.com/actor', 'example.com');
|
||||||
|
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should accept array of object', () => {
|
||||||
|
const input = {
|
||||||
|
test: [{
|
||||||
|
id: 'https://example.com/object',
|
||||||
|
}],
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = serviceUnderTest.sanitizeInlineObject(input, 'test', 'https://example.com/actor', 'example.com');
|
||||||
|
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
Loading…
Add table
Reference in a new issue