fetchPersonWithRenewal

This commit is contained in:
tamaina 2024-03-01 06:49:38 +00:00
parent a5cccf3799
commit 54fe8ca600
6 changed files with 64 additions and 26 deletions

View File

@ -40,6 +40,7 @@ type NodeInfo = {
@Injectable() @Injectable()
export class FetchInstanceMetadataService { export class FetchInstanceMetadataService {
private logger: Logger; private logger: Logger;
private httpColon = 'https://';
constructor( constructor(
private httpRequestService: HttpRequestService, private httpRequestService: HttpRequestService,
@ -49,6 +50,7 @@ export class FetchInstanceMetadataService {
private redisClient: Redis.Redis, private redisClient: Redis.Redis,
) { ) {
this.logger = this.loggerService.getLogger('metadata', 'cyan'); this.logger = this.loggerService.getLogger('metadata', 'cyan');
this.httpColon = 'http://';
} }
@bindThis @bindThis
@ -72,8 +74,7 @@ export class FetchInstanceMetadataService {
const _instance = await this.federatedInstanceService.fetch(host); const _instance = await this.federatedInstanceService.fetch(host);
const now = Date.now(); const now = Date.now();
if (_instance && _instance.infoUpdatedAt && (now - _instance.infoUpdatedAt.getTime() < 1000 * 60 * 60 * 3)) { if (_instance && _instance.infoUpdatedAt && (now - _instance.infoUpdatedAt.getTime() < 1000 * 60 * 60 * 3)) {
// unlock at the finally caluse throw new Error('Skip because updated recently');
return;
} }
} }
@ -119,6 +120,12 @@ export class FetchInstanceMetadataService {
await this.federatedInstanceService.update(instance.id, updates); await this.federatedInstanceService.update(instance.id, updates);
this.logger.succ(`Successfuly updated metadata of ${instance.host}`); this.logger.succ(`Successfuly updated metadata of ${instance.host}`);
this.logger.debug('Updated metadata:', {
info: !!info,
dom: !!dom,
manifest: !!manifest,
updates,
});
} catch (e) { } catch (e) {
this.logger.error(`Failed to update metadata of ${instance.host}: ${e}`); this.logger.error(`Failed to update metadata of ${instance.host}: ${e}`);
} finally { } finally {
@ -131,7 +138,7 @@ export class FetchInstanceMetadataService {
this.logger.info(`Fetching nodeinfo of ${instance.host} ...`); this.logger.info(`Fetching nodeinfo of ${instance.host} ...`);
try { try {
const wellknown = await this.httpRequestService.getJson('https://' + instance.host + '/.well-known/nodeinfo') const wellknown = await this.httpRequestService.getJson(this.httpColon + instance.host + '/.well-known/nodeinfo')
.catch(err => { .catch(err => {
if (err.statusCode === 404) { if (err.statusCode === 404) {
throw new Error('No nodeinfo provided'); throw new Error('No nodeinfo provided');
@ -174,7 +181,7 @@ export class FetchInstanceMetadataService {
private async fetchDom(instance: MiInstance): Promise<DOMWindow['document']> { private async fetchDom(instance: MiInstance): Promise<DOMWindow['document']> {
this.logger.info(`Fetching HTML of ${instance.host} ...`); this.logger.info(`Fetching HTML of ${instance.host} ...`);
const url = 'https://' + instance.host; const url = this.httpColon + instance.host;
const html = await this.httpRequestService.getHtml(url); const html = await this.httpRequestService.getHtml(url);
@ -186,7 +193,7 @@ export class FetchInstanceMetadataService {
@bindThis @bindThis
private async fetchManifest(instance: MiInstance): Promise<Record<string, unknown> | null> { private async fetchManifest(instance: MiInstance): Promise<Record<string, unknown> | null> {
const url = 'https://' + instance.host; const url = this.httpColon + instance.host;
const manifestUrl = url + '/manifest.json'; const manifestUrl = url + '/manifest.json';
@ -197,7 +204,7 @@ export class FetchInstanceMetadataService {
@bindThis @bindThis
private async fetchFaviconUrl(instance: MiInstance, doc: DOMWindow['document'] | null): Promise<string | null> { private async fetchFaviconUrl(instance: MiInstance, doc: DOMWindow['document'] | null): Promise<string | null> {
const url = 'https://' + instance.host; const url = this.httpColon + instance.host;
if (doc) { if (doc) {
// https://github.com/misskey-dev/misskey/pull/8220#issuecomment-1025104043 // https://github.com/misskey-dev/misskey/pull/8220#issuecomment-1025104043
@ -224,12 +231,12 @@ export class FetchInstanceMetadataService {
@bindThis @bindThis
private async fetchIconUrl(instance: MiInstance, doc: DOMWindow['document'] | null, manifest: Record<string, any> | null): Promise<string | null> { private async fetchIconUrl(instance: MiInstance, doc: DOMWindow['document'] | null, manifest: Record<string, any> | null): Promise<string | null> {
if (manifest && manifest.icons && manifest.icons.length > 0 && manifest.icons[0].src) { if (manifest && manifest.icons && manifest.icons.length > 0 && manifest.icons[0].src) {
const url = 'https://' + instance.host; const url = this.httpColon + instance.host;
return (new URL(manifest.icons[0].src, url)).href; return (new URL(manifest.icons[0].src, url)).href;
} }
if (doc) { if (doc) {
const url = 'https://' + instance.host; const url = this.httpColon + instance.host;
// https://github.com/misskey-dev/misskey/pull/8220#issuecomment-1025104043 // https://github.com/misskey-dev/misskey/pull/8220#issuecomment-1025104043
const links = Array.from(doc.getElementsByTagName('link')).reverse(); const links = Array.from(doc.getElementsByTagName('link')).reverse();

View File

@ -122,7 +122,7 @@ export class ApDbResolverService implements OnApplicationShutdown {
user: MiRemoteUser; user: MiRemoteUser;
key: MiUserPublickey | null; key: MiUserPublickey | null;
} | null> { } | null> {
const user = await this.apPersonService.resolvePerson(uri) as MiRemoteUser; const user = await this.apPersonService.resolvePerson(uri, undefined, true) as MiRemoteUser;
if (user.isDeleted) return null; if (user.isDeleted) return null;
const keys = await this.publicKeyByUserIdCache.fetch( const keys = await this.publicKeyByUserIdCache.fetch(

View File

@ -36,7 +36,6 @@ import { ApResolverService } from './ApResolverService.js';
import { ApAudienceService } from './ApAudienceService.js'; import { ApAudienceService } from './ApAudienceService.js';
import { ApPersonService } from './models/ApPersonService.js'; import { ApPersonService } from './models/ApPersonService.js';
import { ApQuestionService } from './models/ApQuestionService.js'; import { ApQuestionService } from './models/ApQuestionService.js';
import { CacheService } from '@/core/CacheService.js';
import { GlobalEventService } from '@/core/GlobalEventService.js'; import { GlobalEventService } from '@/core/GlobalEventService.js';
import type { Resolver } from './ApResolverService.js'; import type { Resolver } from './ApResolverService.js';
import type { IAccept, IAdd, IAnnounce, IBlock, ICreate, IDelete, IFlag, IFollow, ILike, IObject, IReject, IRemove, IUndo, IUpdate, IMove } from './type.js'; import type { IAccept, IAdd, IAnnounce, IBlock, ICreate, IDelete, IFlag, IFollow, ILike, IObject, IReject, IRemove, IUndo, IUpdate, IMove } from './type.js';
@ -109,15 +108,6 @@ export class ApInboxService {
} else { } else {
await this.performOneActivity(actor, activity); await this.performOneActivity(actor, activity);
} }
// ついでにリモートユーザーの情報が古かったら更新しておく
if (actor.uri) {
if (actor.lastFetchedAt == null || Date.now() - actor.lastFetchedAt.getTime() > 1000 * 60 * 60 * 24) {
setImmediate(() => {
this.apPersonService.updatePerson(actor.uri);
});
}
}
} }
@bindThis @bindThis

View File

@ -107,16 +107,24 @@ export class ApRequestService {
@bindThis @bindThis
public async signedPost(user: { id: MiUser['id'] }, url: string, object: unknown, level: string): Promise<void> { public async signedPost(user: { id: MiUser['id'] }, url: string, object: unknown, level: string): Promise<void> {
const body = typeof object === 'string' ? object : JSON.stringify(object); const body = typeof object === 'string' ? object : JSON.stringify(object);
const key = await this.getPrivateKey(user.id, level);
const req = createSignedPost({ const req = createSignedPost({
level, level,
key: await this.getPrivateKey(user.id, level), key,
url, url,
body, body,
additionalHeaders: { additionalHeaders: {
'User-Agent': this.config.userAgent,
}, },
}); });
this.logger.debug('create signed post', {
version: 'draft',
level,
url,
keyId: key.keyId,
});
await this.httpRequestService.send(url, { await this.httpRequestService.send(url, {
method: req.request.method, method: req.request.method,
headers: req.request.headers, headers: req.request.headers,
@ -131,14 +139,23 @@ export class ApRequestService {
*/ */
@bindThis @bindThis
public async signedGet(url: string, user: { id: MiUser['id'] }, level: string): Promise<unknown> { public async signedGet(url: string, user: { id: MiUser['id'] }, level: string): Promise<unknown> {
const key = await this.getPrivateKey(user.id, level);
const req = createSignedGet({ const req = createSignedGet({
level, level,
key: await this.getPrivateKey(user.id, level), key,
url, url,
additionalHeaders: { additionalHeaders: {
'User-Agent': this.config.userAgent,
}, },
}); });
this.logger.debug('create signed get', {
version: 'draft',
level,
url,
keyId: key.keyId,
});
const res = await this.httpRequestService.send(url, { const res = await this.httpRequestService.send(url, {
method: req.request.method, method: req.request.method,
headers: req.request.headers, headers: req.request.headers,

View File

@ -248,6 +248,22 @@ export class ApPersonService implements OnModuleInit {
return null; return null;
} }
@bindThis
async fetchPersonWithRenewal(uri: string): Promise<MiLocalUser | MiRemoteUser | null> {
const exist = await this.fetchPerson(uri);
if (exist == null) return null;
// ついでにリモートユーザーの情報が古かったら更新しておく
if (this.userEntityService.isRemoteUser(exist)) {
if (exist.lastFetchedAt == null || Date.now() - exist.lastFetchedAt.getTime() > 1000 * 60 * 60 * 3) {
await this.updatePerson(exist.uri);
return await this.fetchPerson(uri);
}
}
return exist;
}
private async resolveAvatarAndBanner(user: MiRemoteUser, icon: any, image: any): Promise<Partial<Pick<MiRemoteUser, 'avatarId' | 'bannerId' | 'avatarUrl' | 'bannerUrl' | 'avatarBlurhash' | 'bannerBlurhash'>>> { private async resolveAvatarAndBanner(user: MiRemoteUser, icon: any, image: any): Promise<Partial<Pick<MiRemoteUser, 'avatarId' | 'bannerId' | 'avatarUrl' | 'bannerUrl' | 'avatarBlurhash' | 'bannerBlurhash'>>> {
if (user == null) throw new Error('failed to create user: user is null'); if (user == null) throw new Error('failed to create user: user is null');
@ -624,9 +640,9 @@ export class ApPersonService implements OnModuleInit {
* Misskeyに登録しそれを返します * Misskeyに登録しそれを返します
*/ */
@bindThis @bindThis
public async resolvePerson(uri: string, resolver?: Resolver): Promise<MiLocalUser | MiRemoteUser> { public async resolvePerson(uri: string, resolver?: Resolver, withRenewal = false): Promise<MiLocalUser | MiRemoteUser> {
//#region このサーバーに既に登録されていたらそれを返す //#region このサーバーに既に登録されていたらそれを返す
const exist = await this.fetchPerson(uri); const exist = withRenewal ? await this.fetchPersonWithRenewal(uri) : await this.fetchPerson(uri);
if (exist) return exist; if (exist) return exist;
//#endregion //#endregion

View File

@ -19,6 +19,7 @@ import type { MiRemoteUser } from '@/models/User.js';
import type { MiUserPublickey } from '@/models/UserPublickey.js'; import type { MiUserPublickey } from '@/models/UserPublickey.js';
import { ApDbResolverService } from '@/core/activitypub/ApDbResolverService.js'; import { ApDbResolverService } from '@/core/activitypub/ApDbResolverService.js';
import { StatusError } from '@/misc/status-error.js'; import { StatusError } from '@/misc/status-error.js';
import * as Acct from '@/misc/acct.js';
import { UtilityService } from '@/core/UtilityService.js'; import { UtilityService } from '@/core/UtilityService.js';
import { ApPersonService } from '@/core/activitypub/models/ApPersonService.js'; import { ApPersonService } from '@/core/activitypub/models/ApPersonService.js';
import { LdSignatureService } from '@/core/activitypub/LdSignatureService.js'; import { LdSignatureService } from '@/core/activitypub/LdSignatureService.js';
@ -79,7 +80,6 @@ export class InboxProcessorService {
key: MiUserPublickey | null; key: MiUserPublickey | null;
} | null = null; } | null = null;
// keyIdでわからなければ、activity.actorを元にDBから取得 || activity.actorを元にリモートから取得
try { try {
authUser = await this.apDbResolverService.getAuthUserFromApId(getApId(activity.actor), signature.keyId); authUser = await this.apDbResolverService.getAuthUserFromApId(getApId(activity.actor), signature.keyId);
} catch (err) { } catch (err) {
@ -103,7 +103,15 @@ export class InboxProcessorService {
} }
// HTTP-Signatureの検証 // HTTP-Signatureの検証
const httpSignatureValidated = verifyDraftSignature(signature, authUser.key.keyPem); const errorLogger = (ms: any) => this.logger.error(ms);
const httpSignatureValidated = verifyDraftSignature(signature, authUser.key.keyPem, errorLogger);
this.logger.debug('Inbox message validation: ', {
userId: authUser.user.id,
userAcct: Acct.toString(authUser.user),
parsedKeyId: signature.keyId,
foundKeyId: authUser.key.keyId,
httpSignatureValidated,
});
// また、signatureのsignerは、activity.actorと一致する必要がある // また、signatureのsignerは、activity.actorと一致する必要がある
if (!httpSignatureValidated || authUser.user.uri !== activity.actor) { if (!httpSignatureValidated || authUser.user.uri !== activity.actor) {