summaryrefslogtreecommitdiff
path: root/packages/backend/src/core
diff options
context:
space:
mode:
authordakkar <dakkar@thenautilus.net>2025-03-02 18:36:04 +0000
committerdakkar <dakkar@thenautilus.net>2025-03-02 18:36:04 +0000
commit504e90c190bcf6adc71a47d9ca643ff088e649bf (patch)
treeba3fa1cac7e7d09b622764b6dc895b0b7e489731 /packages/backend/src/core
parentmerge: handle scheduled notes when deleting and migrating accounts - fixes #9... (diff)
parentfilter `url` properties by `mediaType` (diff)
downloadsharkey-504e90c190bcf6adc71a47d9ca643ff088e649bf.tar.gz
sharkey-504e90c190bcf6adc71a47d9ca643ff088e649bf.tar.bz2
sharkey-504e90c190bcf6adc71a47d9ca643ff088e649bf.zip
merge: Remove assertActivityMatchesUrls in favor of three-way same-authority checks (resolves #956 and #914) (!914)
View MR for information: https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/914 Closes #956 and #914 Approved-by: dakkar <dakkar@thenautilus.net> Approved-by: Marie <github@yuugi.dev>
Diffstat (limited to 'packages/backend/src/core')
-rw-r--r--packages/backend/src/core/CoreModule.ts8
-rw-r--r--packages/backend/src/core/HttpRequestService.ts11
-rw-r--r--packages/backend/src/core/activitypub/ApRequestService.ts16
-rw-r--r--packages/backend/src/core/activitypub/ApResolverService.ts20
-rw-r--r--packages/backend/src/core/activitypub/ApUtilityService.ts108
-rw-r--r--packages/backend/src/core/activitypub/misc/check-against-url.ts31
-rw-r--r--packages/backend/src/core/activitypub/models/ApNoteService.ts38
-rw-r--r--packages/backend/src/core/activitypub/models/ApPersonService.ts29
-rw-r--r--packages/backend/src/core/activitypub/type.ts15
9 files changed, 156 insertions, 120 deletions
diff --git a/packages/backend/src/core/CoreModule.ts b/packages/backend/src/core/CoreModule.ts
index 47be6967d7..3c35dfc4ff 100644
--- a/packages/backend/src/core/CoreModule.ts
+++ b/packages/backend/src/core/CoreModule.ts
@@ -17,6 +17,8 @@ import { WebhookTestService } from '@/core/WebhookTestService.js';
import { FlashService } from '@/core/FlashService.js';
import { TimeService } from '@/core/TimeService.js';
import { EnvService } from '@/core/EnvService.js';
+import { ApUtilityService } from '@/core/activitypub/ApUtilityService.js';
+import { ApLogService } from '@/core/ApLogService.js';
import { AccountMoveService } from './AccountMoveService.js';
import { AccountUpdateService } from './AccountUpdateService.js';
import { AnnouncementService } from './AnnouncementService.js';
@@ -157,7 +159,6 @@ import { QueueService } from './QueueService.js';
import { LoggerService } from './LoggerService.js';
import { SponsorsService } from './SponsorsService.js';
import type { Provider } from '@nestjs/common';
-import { ApLogService } from '@/core/ApLogService.js';
//#region 文字列ベースでのinjection用(循環参照対応のため)
const $LoggerService: Provider = { provide: 'LoggerService', useExisting: LoggerService };
@@ -308,6 +309,7 @@ const $ApMentionService: Provider = { provide: 'ApMentionService', useExisting:
const $ApNoteService: Provider = { provide: 'ApNoteService', useExisting: ApNoteService };
const $ApPersonService: Provider = { provide: 'ApPersonService', useExisting: ApPersonService };
const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting: ApQuestionService };
+const $ApUtilityService: Provider = { provide: 'ApUtilityService', useExisting: ApUtilityService };
//#endregion
const $SponsorsService: Provider = { provide: 'SponsorsService', useExisting: SponsorsService };
@@ -465,6 +467,7 @@ const $SponsorsService: Provider = { provide: 'SponsorsService', useExisting: Sp
ApNoteService,
ApPersonService,
ApQuestionService,
+ ApUtilityService,
QueueService,
SponsorsService,
@@ -618,6 +621,7 @@ const $SponsorsService: Provider = { provide: 'SponsorsService', useExisting: Sp
$ApNoteService,
$ApPersonService,
$ApQuestionService,
+ $ApUtilityService,
//#endregion
$SponsorsService,
@@ -771,6 +775,7 @@ const $SponsorsService: Provider = { provide: 'SponsorsService', useExisting: Sp
ApNoteService,
ApPersonService,
ApQuestionService,
+ ApUtilityService,
QueueService,
SponsorsService,
@@ -922,6 +927,7 @@ const $SponsorsService: Provider = { provide: 'SponsorsService', useExisting: Sp
$ApNoteService,
$ApPersonService,
$ApQuestionService,
+ $ApUtilityService,
//#endregion
$SponsorsService,
diff --git a/packages/backend/src/core/HttpRequestService.ts b/packages/backend/src/core/HttpRequestService.ts
index 083153940a..19992a7597 100644
--- a/packages/backend/src/core/HttpRequestService.ts
+++ b/packages/backend/src/core/HttpRequestService.ts
@@ -16,8 +16,8 @@ import type { Config } from '@/config.js';
import { StatusError } from '@/misc/status-error.js';
import { bindThis } from '@/decorators.js';
import { validateContentTypeSetAsActivityPub } from '@/core/activitypub/misc/validator.js';
-import { assertActivityMatchesUrls } from '@/core/activitypub/misc/check-against-url.js';
-import type { IObject } from '@/core/activitypub/type.js';
+import { IObject } from '@/core/activitypub/type.js';
+import { ApUtilityService } from './activitypub/ApUtilityService.js';
import type { Response } from 'node-fetch';
import type { URL } from 'node:url';
@@ -145,6 +145,7 @@ export class HttpRequestService {
constructor(
@Inject(DI.config)
private config: Config,
+ private readonly apUtilityService: ApUtilityService,
) {
const cache = new CacheableLookup({
maxTtl: 3600, // 1hours
@@ -198,6 +199,7 @@ export class HttpRequestService {
* Get agent by URL
* @param url URL
* @param bypassProxy Allways bypass proxy
+ * @param isLocalAddressAllowed
*/
@bindThis
public getAgentByUrl(url: URL, bypassProxy = false, isLocalAddressAllowed = false): http.Agent | https.Agent {
@@ -229,10 +231,11 @@ export class HttpRequestService {
validators: [validateContentTypeSetAsActivityPub],
});
- const finalUrl = res.url; // redirects may have been involved
const activity = await res.json() as IObject;
- assertActivityMatchesUrls(activity, [finalUrl]);
+ // Make sure the object ID matches the final URL (which is where it actually exists).
+ // The caller (ApResolverService) will verify the ID against the original / entry URL, which ensures that all three match.
+ this.apUtilityService.assertIdMatchesUrlAuthority(activity, res.url);
return activity;
}
diff --git a/packages/backend/src/core/activitypub/ApRequestService.ts b/packages/backend/src/core/activitypub/ApRequestService.ts
index 6291768e8c..b63d4eb2ab 100644
--- a/packages/backend/src/core/activitypub/ApRequestService.ts
+++ b/packages/backend/src/core/activitypub/ApRequestService.ts
@@ -11,13 +11,12 @@ import { DI } from '@/di-symbols.js';
import type { Config } from '@/config.js';
import type { MiUser } from '@/models/User.js';
import { UserKeypairService } from '@/core/UserKeypairService.js';
-import { UtilityService } from '@/core/UtilityService.js';
+import { ApUtilityService } from '@/core/activitypub/ApUtilityService.js';
import { HttpRequestService } from '@/core/HttpRequestService.js';
import { LoggerService } from '@/core/LoggerService.js';
import { bindThis } from '@/decorators.js';
import type Logger from '@/logger.js';
import { validateContentTypeSetAsActivityPub } from '@/core/activitypub/misc/validator.js';
-import { assertActivityMatchesUrls } from '@/core/activitypub/misc/check-against-url.js';
import type { IObject } from './type.js';
type Request = {
@@ -148,7 +147,7 @@ export class ApRequestService {
private userKeypairService: UserKeypairService,
private httpRequestService: HttpRequestService,
private loggerService: LoggerService,
- private utilityService: UtilityService,
+ private readonly apUtilityService: ApUtilityService,
) {
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
this.logger = this.loggerService?.getLogger('ap-request'); // なぜか TypeError: Cannot read properties of undefined (reading 'getLogger') と言われる
@@ -183,9 +182,10 @@ export class ApRequestService {
* Get AP object with http-signature
* @param user http-signature user
* @param url URL to fetch
+ * @param followAlternate
*/
@bindThis
- public async signedGet(url: string, user: { id: MiUser['id'] }, followAlternate?: boolean): Promise<object> {
+ public async signedGet(url: string, user: { id: MiUser['id'] }, followAlternate?: boolean): Promise<IObject> {
const _followAlternate = followAlternate ?? true;
const keypair = await this.userKeypairService.getUserKeypair(user.id);
@@ -253,7 +253,7 @@ export class ApRequestService {
if (alternate) {
const href = alternate.getAttribute('href');
- if (href && this.utilityService.punyHostPSLDomain(url) === this.utilityService.punyHostPSLDomain(href)) {
+ if (href && this.apUtilityService.haveSameAuthority(url, href)) {
return await this.signedGet(href, user, false);
}
}
@@ -266,10 +266,12 @@ export class ApRequestService {
//#endregion
validateContentTypeSetAsActivityPub(res);
- const finalUrl = res.url; // redirects may have been involved
+
const activity = await res.json() as IObject;
- assertActivityMatchesUrls(activity, [finalUrl]);
+ // Make sure the object ID matches the final URL (which is where it actually exists).
+ // The caller (ApResolverService) will verify the ID against the original / entry URL, which ensures that all three match.
+ this.apUtilityService.assertIdMatchesUrlAuthority(activity, res.url);
return activity;
}
diff --git a/packages/backend/src/core/activitypub/ApResolverService.ts b/packages/backend/src/core/activitypub/ApResolverService.ts
index f5b63a2827..f9ccf10fa7 100644
--- a/packages/backend/src/core/activitypub/ApResolverService.ts
+++ b/packages/backend/src/core/activitypub/ApResolverService.ts
@@ -18,7 +18,8 @@ import type Logger from '@/logger.js';
import { fromTuple } from '@/misc/from-tuple.js';
import { IdentifiableError } from '@/misc/identifiable-error.js';
import { ApLogService, calculateDurationSince, extractObjectContext } from '@/core/ApLogService.js';
-import { getNullableApId, isCollectionOrOrderedCollection } from './type.js';
+import { ApUtilityService } from '@/core/activitypub/ApUtilityService.js';
+import { getApId, getNullableApId, isCollectionOrOrderedCollection } from './type.js';
import { ApDbResolverService } from './ApDbResolverService.js';
import { ApRendererService } from './ApRendererService.js';
import { ApRequestService } from './ApRequestService.js';
@@ -45,6 +46,7 @@ export class Resolver {
private apDbResolverService: ApDbResolverService,
private loggerService: LoggerService,
private readonly apLogService: ApLogService,
+ private readonly apUtilityService: ApUtilityService,
private recursionLimit = 256,
) {
this.history = new Set();
@@ -176,20 +178,16 @@ export class Resolver {
throw new IdentifiableError('72180409-793c-4973-868e-5a118eb5519b', `invalid AP object ${value}: does not have ActivityStreams context`);
}
- // Since redirects are allowed, we cannot safely validate an anonymous object.
- // Reject any responses without an ID, as all other checks depend on that value.
- if (object.id == null) {
- throw new IdentifiableError('ad2dc287-75c1-44c4-839d-3d2e64576675', `invalid AP object ${value}: missing id`);
- }
+ // The object ID is already validated to match the final URL's authority by signedGet / getActivityJson.
+ // We only need to validate that it also matches the original URL's authority, in case of redirects.
+ const objectId = getApId(object);
// We allow some limited cross-domain redirects, which means the host may have changed during fetch.
// Additional checks are needed to validate the scope of cross-domain redirects.
- const finalHost = this.utilityService.extractDbHost(object.id);
+ const finalHost = this.utilityService.extractDbHost(objectId);
if (finalHost !== host) {
// Make sure the redirect stayed within the same authority.
- if (this.utilityService.punyHostPSLDomain(object.id) !== this.utilityService.punyHostPSLDomain(value)) {
- throw new IdentifiableError('fd93c2fa-69a8-440f-880b-bf178e0ec877', `invalid AP object ${value}: id ${object.id} has different host`);
- }
+ this.apUtilityService.assertIdMatchesUrlAuthority(object, value);
// Check if the redirect bounce from [allowed domain] to [blocked domain].
if (!this.utilityService.isFederationAllowedHost(finalHost)) {
@@ -287,6 +285,7 @@ export class ApResolverService {
private apDbResolverService: ApDbResolverService,
private loggerService: LoggerService,
private readonly apLogService: ApLogService,
+ private readonly apUtilityService: ApUtilityService,
) {
}
@@ -308,6 +307,7 @@ export class ApResolverService {
this.apDbResolverService,
this.loggerService,
this.apLogService,
+ this.apUtilityService,
);
}
}
diff --git a/packages/backend/src/core/activitypub/ApUtilityService.ts b/packages/backend/src/core/activitypub/ApUtilityService.ts
new file mode 100644
index 0000000000..ae6e4997e4
--- /dev/null
+++ b/packages/backend/src/core/activitypub/ApUtilityService.ts
@@ -0,0 +1,108 @@
+/*
+ * SPDX-FileCopyrightText: hazelnoot and other Sharkey contributors
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { Injectable } from '@nestjs/common';
+import { UtilityService } from '@/core/UtilityService.js';
+import { IdentifiableError } from '@/misc/identifiable-error.js';
+import { toArray } from '@/misc/prelude/array.js';
+import { EnvService } from '@/core/EnvService.js';
+import { getApId, getOneApHrefNullable, IObject } from './type.js';
+
+@Injectable()
+export class ApUtilityService {
+ constructor(
+ private readonly utilityService: UtilityService,
+ private readonly envService: EnvService,
+ ) {}
+
+ /**
+ * Verifies that the object's ID has the same authority as the provided URL.
+ * Returns on success, throws on any validation error.
+ */
+ public assertIdMatchesUrlAuthority(object: IObject, url: string): void {
+ // 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.
+ const id = getApId(object);
+
+ // Make sure the object ID matches the final URL (which is where it actually exists).
+ // The caller (ApResolverService) will verify the ID against the original / entry URL, which ensures that all three match.
+ if (!this.haveSameAuthority(url, id)) {
+ throw new IdentifiableError('fd93c2fa-69a8-440f-880b-bf178e0ec877', `invalid AP object ${url}: id ${id} has different host authority`);
+ }
+ }
+
+ /**
+ * Checks if two URLs have the same host authority
+ */
+ public haveSameAuthority(url1: string, url2: string): boolean {
+ if (url1 === url2) return true;
+
+ const authority1 = this.utilityService.punyHostPSLDomain(url1);
+ const authority2 = this.utilityService.punyHostPSLDomain(url2);
+ return authority1 === authority2;
+ }
+
+ /**
+ * Finds the "best" URL for a given AP object.
+ * The list of URLs is first filtered via findSameAuthorityUrl, then further filtered based on mediaType, and finally sorted to select the best one.
+ * @throws {IdentifiableError} if object does not have an ID
+ * @returns the best URL, or null if none were found
+ */
+ public findBestObjectUrl(object: IObject): string | null {
+ const targetUrl = getApId(object);
+ const targetAuthority = this.utilityService.punyHostPSLDomain(targetUrl);
+
+ const rawUrls = toArray(object.url);
+ const acceptableUrls = rawUrls
+ .map(raw => ({
+ url: getOneApHrefNullable(raw),
+ type: typeof(raw) === 'object'
+ ? raw.mediaType?.toLowerCase()
+ : undefined,
+ }))
+ .filter(({ url, type }) => {
+ if (!url) return false;
+ if (!this.checkHttps(url)) return false;
+ if (!isAcceptableUrlType(type)) return false;
+
+ const urlAuthority = this.utilityService.punyHostPSLDomain(url);
+ return urlAuthority === targetAuthority;
+ })
+ .sort((a, b) => {
+ return rankUrlType(a.type) - rankUrlType(b.type);
+ });
+
+ return acceptableUrls[0]?.url ?? null;
+ }
+
+ /**
+ * Checks if the URL contains HTTPS.
+ * Additionally, allows HTTP in non-production environments.
+ * Based on check-https.ts.
+ */
+ private checkHttps(url: string): boolean {
+ const isNonProd = this.envService.env.NODE_ENV !== 'production';
+
+ // noinspection HttpUrlsUsage
+ return url.startsWith('https://') || (url.startsWith('http://') && isNonProd);
+ }
+}
+
+function isAcceptableUrlType(type: string | undefined): boolean {
+ if (!type) return true;
+ if (type.startsWith('text/')) return true;
+ if (type.startsWith('application/ld+json')) return true;
+ if (type.startsWith('application/activity+json')) return true;
+ return false;
+}
+
+function rankUrlType(type: string | undefined): number {
+ if (!type) return 2;
+ if (type === 'text/html') return 0;
+ if (type.startsWith('text/')) return 1;
+ if (type.startsWith('application/ld+json')) return 3;
+ if (type.startsWith('application/activity+json')) return 4;
+ return 5;
+}
diff --git a/packages/backend/src/core/activitypub/misc/check-against-url.ts b/packages/backend/src/core/activitypub/misc/check-against-url.ts
deleted file mode 100644
index 0c676c6a29..0000000000
--- a/packages/backend/src/core/activitypub/misc/check-against-url.ts
+++ /dev/null
@@ -1,31 +0,0 @@
-/*
- * SPDX-FileCopyrightText: dakkar and sharkey-project
- * SPDX-License-Identifier: AGPL-3.0-only
- */
-
-import { UnrecoverableError } from 'bullmq';
-import type { IObject } from '../type.js';
-
-function getHrefsFrom(one: IObject | string | undefined | (IObject | string | undefined)[]): (string | undefined)[] {
- if (Array.isArray(one)) {
- return one.flatMap(h => getHrefsFrom(h));
- }
- return [
- typeof(one) === 'object' ? one.href : one,
- ];
-}
-
-export function assertActivityMatchesUrls(activity: IObject, urls: string[]) {
- const expectedUrls = new Set(urls
- .filter(u => URL.canParse(u))
- .map(u => new URL(u).href),
- );
-
- const actualUrls = [activity.id, ...getHrefsFrom(activity.url)]
- .filter(u => u && URL.canParse(u))
- .map(u => new URL(u as string).href);
-
- if (!actualUrls.some(u => expectedUrls.has(u))) {
- throw new UnrecoverableError(`bad Activity: neither id nor url (${actualUrls.join(', ')}) match location (${urls.join(', ')})`);
- }
-}
diff --git a/packages/backend/src/core/activitypub/models/ApNoteService.ts b/packages/backend/src/core/activitypub/models/ApNoteService.ts
index 606ab4c26e..63f9887a8d 100644
--- a/packages/backend/src/core/activitypub/models/ApNoteService.ts
+++ b/packages/backend/src/core/activitypub/models/ApNoteService.ts
@@ -26,12 +26,13 @@ import { bindThis } from '@/decorators.js';
import { checkHttps } from '@/misc/check-https.js';
import { IdentifiableError } from '@/misc/identifiable-error.js';
import { isRetryableError } from '@/misc/is-retryable-error.js';
-import { getOneApId, getApId, getOneApHrefNullable, validPost, isEmoji, getApType, isApObject, isDocument, IApDocument } from '../type.js';
+import { getOneApId, getApId, validPost, isEmoji, getApType, isApObject, isDocument, IApDocument } from '../type.js';
import { ApLoggerService } from '../ApLoggerService.js';
import { ApMfmService } from '../ApMfmService.js';
import { ApDbResolverService } from '../ApDbResolverService.js';
import { ApResolverService } from '../ApResolverService.js';
import { ApAudienceService } from '../ApAudienceService.js';
+import { ApUtilityService } from '../ApUtilityService.js';
import { ApPersonService } from './ApPersonService.js';
import { extractApHashtags } from './tag.js';
import { ApMentionService } from './ApMentionService.js';
@@ -82,6 +83,7 @@ export class ApNoteService {
private noteEditService: NoteEditService,
private apDbResolverService: ApDbResolverService,
private apLoggerService: ApLoggerService,
+ private readonly apUtilityService: ApUtilityService,
) {
this.logger = this.apLoggerService.logger;
}
@@ -92,7 +94,6 @@ export class ApNoteService {
uri: string,
actor?: MiRemoteUser,
user?: MiRemoteUser,
- note?: MiNote,
): Error | null {
const expectHost = this.utilityService.extractDbHost(uri);
const apType = getApType(object);
@@ -124,13 +125,6 @@ export class ApNoteService {
}
}
- if (note) {
- const url = (object.url) ? getOneApId(object.url) : note.url;
- if (url && url !== note.url) {
- return new IdentifiableError('d450b8a9-48e4-4dab-ae36-f4db763fda7c', `invalid Note: updated url does not match original url. updated url: ${url}, original url: ${note.url}`);
- }
- }
-
return null;
}
@@ -186,17 +180,7 @@ export class ApNoteService {
throw new UnrecoverableError(`unexpected schema of note.id ${note.id} in ${entryUri}`);
}
- const url = getOneApHrefNullable(note.url);
-
- if (url != null) {
- if (!checkHttps(url)) {
- throw new UnrecoverableError(`unexpected schema of note.url ${url} 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}`);
- }
- }
+ const url = this.apUtilityService.findBestObjectUrl(note);
this.logger.info(`Creating the Note: ${note.id}`);
@@ -385,7 +369,7 @@ export class ApNoteService {
const object = await resolver.resolve(value);
const entryUri = getApId(value);
- const err = this.validateNote(object, entryUri, actor, user, updatedNote);
+ const err = this.validateNote(object, entryUri, actor, user);
if (err) {
this.logger.error(err.message, {
resolver: { history: resolver.getHistory() },
@@ -411,17 +395,7 @@ export class ApNoteService {
throw new UnrecoverableError(`unexpected schema of note.id ${note.id} in ${noteUri}`);
}
- const url = getOneApHrefNullable(note.url);
-
- if (url != null) {
- if (!checkHttps(url)) {
- throw new UnrecoverableError(`unexpected schema of note.url ${url} in ${noteUri}`);
- }
-
- if (this.utilityService.punyHostPSLDomain(url) !== this.utilityService.punyHostPSLDomain(note.id)) {
- throw new UnrecoverableError(`note url <> id host mismatch: ${url} <> ${note.id} in ${noteUri}`);
- }
- }
+ const url = this.apUtilityService.findBestObjectUrl(note);
this.logger.info(`Creating the Note: ${note.id}`);
diff --git a/packages/backend/src/core/activitypub/models/ApPersonService.ts b/packages/backend/src/core/activitypub/models/ApPersonService.ts
index b4b909d849..da29a3c527 100644
--- a/packages/backend/src/core/activitypub/models/ApPersonService.ts
+++ b/packages/backend/src/core/activitypub/models/ApPersonService.ts
@@ -39,8 +39,8 @@ import { bindThis } from '@/decorators.js';
import { RoleService } from '@/core/RoleService.js';
import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js';
import type { AccountMoveService } from '@/core/AccountMoveService.js';
-import { checkHttps } from '@/misc/check-https.js';
-import { getApId, getApType, getOneApHrefNullable, isActor, isCollection, isCollectionOrOrderedCollection, isPropertyValue } from '../type.js';
+import { ApUtilityService } from '@/core/activitypub/ApUtilityService.js';
+import { getApId, getApType, isActor, isCollection, isCollectionOrOrderedCollection, isPropertyValue } from '../type.js';
import { extractApHashtags } from './tag.js';
import type { OnModuleInit } from '@nestjs/common';
import type { ApNoteService } from './ApNoteService.js';
@@ -106,6 +106,7 @@ export class ApPersonService implements OnModuleInit {
private followingsRepository: FollowingsRepository,
private roleService: RoleService,
+ private readonly apUtilityService: ApUtilityService,
) {
}
@@ -346,21 +347,11 @@ export class ApPersonService implements OnModuleInit {
const bday = person['vcard:bday']?.match(/^\d{4}-\d{2}-\d{2}/);
- const url = getOneApHrefNullable(person.url);
-
if (person.id == null) {
throw new UnrecoverableError(`Refusing to create person without id: ${uri}`);
}
- if (url != null) {
- if (!checkHttps(url)) {
- throw new UnrecoverableError(`unexpected schema of person url ${url} in ${uri}`);
- }
-
- if (this.utilityService.punyHostPSLDomain(url) !== this.utilityService.punyHostPSLDomain(person.id)) {
- throw new UnrecoverableError(`person url <> uri host mismatch: ${url} <> ${person.id} in ${uri}`);
- }
- }
+ const url = this.apUtilityService.findBestObjectUrl(person);
// Create user
let user: MiRemoteUser | null = null;
@@ -566,21 +557,11 @@ export class ApPersonService implements OnModuleInit {
const bday = person['vcard:bday']?.match(/^\d{4}-\d{2}-\d{2}/);
- const url = getOneApHrefNullable(person.url);
-
if (person.id == null) {
throw new UnrecoverableError(`Refusing to update person without id: ${uri}`);
}
- if (url != null) {
- if (!checkHttps(url)) {
- throw new UnrecoverableError(`unexpected schema of person url ${url} in ${uri}`);
- }
-
- if (this.utilityService.punyHostPSLDomain(url) !== this.utilityService.punyHostPSLDomain(person.id)) {
- throw new UnrecoverableError(`person url <> uri host mismatch: ${url} <> ${person.id} in ${uri}`);
- }
- }
+ const url = this.apUtilityService.findBestObjectUrl(person);
const updates = {
lastFetchedAt: new Date(),
diff --git a/packages/backend/src/core/activitypub/type.ts b/packages/backend/src/core/activitypub/type.ts
index 731a46c0d4..d8e7b3c9c3 100644
--- a/packages/backend/src/core/activitypub/type.ts
+++ b/packages/backend/src/core/activitypub/type.ts
@@ -3,7 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
-import { UnrecoverableError } from 'bullmq';
+import { IdentifiableError } from '@/misc/identifiable-error.js';
import { fromTuple } from '@/misc/from-tuple.js';
export type Obj = { [x: string]: any };
@@ -57,28 +57,21 @@ export function getOneApId(value: ApObject): string {
}
/**
- * Minimal AP payload - just an object with optional ID.
- */
-export interface ObjectWithId {
- id?: string;
-}
-
-/**
* Get ActivityStreams Object id
*/
-export function getApId(value: string | ObjectWithId | [string | ObjectWithId]): string {
+export function getApId(value: string | IObject | [string | IObject]): string {
// eslint-disable-next-line no-param-reassign
value = fromTuple(value);
if (typeof value === 'string') return value;
if (typeof value.id === 'string') return value.id;
- throw new UnrecoverableError('cannot determine id');
+ throw new IdentifiableError('ad2dc287-75c1-44c4-839d-3d2e64576675', `invalid AP object ${value}: missing id`);
}
/**
* Get ActivityStreams Object id, or null if not present
*/
-export function getNullableApId(value: string | ObjectWithId | [string | ObjectWithId]): string | null {
+export function getNullableApId(value: string | IObject | [string | IObject]): string | null {
// eslint-disable-next-line no-param-reassign
value = fromTuple(value);