diff --git a/COPYING b/COPYING index 0a2e4cd47..5abc3e989 100644 --- a/COPYING +++ b/COPYING @@ -13,3 +13,7 @@ https://github.com/twitter/twemoji-parser/blob/master/LICENSE.md Emoji keywords for Unicode 11 and below by Mu-An Chiou License: MIT https://github.com/muan/emojilib/blob/master/LICENSE + +RsaSignature2017 implementation by Transmute Industries Inc +License: MIT +https://github.com/transmute-industries/RsaSignature2017/blob/master/LICENSE diff --git a/package.json b/package.json index 29146a8d5..366a98d55 100644 --- a/package.json +++ b/package.json @@ -53,6 +53,7 @@ "@types/cbor": "5.0.0", "@types/dateformat": "3.0.1", "@types/double-ended-queue": "2.1.1", + "@types/escape-regexp": "0.0.0", "@types/glob": "7.1.1", "@types/gulp": "4.0.6", "@types/gulp-rename": "0.0.33", @@ -60,6 +61,7 @@ "@types/is-url": "1.2.28", "@types/js-yaml": "3.12.3", "@types/jsdom": "16.2.1", + "@types/jsonld": "1.5.1", "@types/katex": "0.11.0", "@types/koa": "2.11.3", "@types/koa-bodyparser": "4.3.0", @@ -126,6 +128,7 @@ "dateformat": "3.0.3", "diskusage": "1.1.3", "double-ended-queue": "2.1.0-0", + "escape-regexp": "0.0.1", "eslint": "6.8.0", "eslint-plugin-vue": "6.2.2", "eventemitter3": "4.0.0", @@ -156,6 +159,7 @@ "jsdom": "16.2.2", "json5": "2.1.3", "json5-loader": "4.0.0", + "jsonld": "3.1.0", "jsrsasign": "8.0.15", "katex": "0.11.1", "koa": "2.11.0", diff --git a/src/@types/http-signature.d.ts b/src/@types/http-signature.d.ts index 6366b2add..8d484312d 100644 --- a/src/@types/http-signature.d.ts +++ b/src/@types/http-signature.d.ts @@ -19,10 +19,12 @@ declare module 'http-signature' { clockSkew?: number; } - interface IParsedSignature { + interface IParsedSignature { scheme: string; params: ISignature; signingString: string; + algorithm: string; + keyId: string; } type RequestSignerConstructorOptions = diff --git a/src/queue/index.ts b/src/queue/index.ts index 76e26d8e4..163c57d69 100644 --- a/src/queue/index.ts +++ b/src/queue/index.ts @@ -12,6 +12,7 @@ import procesObjectStorage from './processors/object-storage'; import { queueLogger } from './logger'; import { DriveFile } from '../models/entities/drive-file'; import { getJobInfo } from './get-job-info'; +import { IActivity } from '../remote/activitypub/type'; function initializeQueue(name: string, limitPerSec = -1) { return new Queue(name, { @@ -29,6 +30,12 @@ function initializeQueue(name: string, limitPerSec = -1) { }); } +export type InboxJobData = { + activity: IActivity, + /** HTTP-Signature */ + signature: httpSignature.IParsedSignature +}; + function renderError(e: Error): any { return { stack: e?.stack, diff --git a/src/queue/processors/inbox.ts b/src/queue/processors/inbox.ts index 74108f354..6eea2e564 100644 --- a/src/queue/processors/inbox.ts +++ b/src/queue/processors/inbox.ts @@ -1,95 +1,111 @@ import * as Bull from 'bull'; import * as httpSignature from 'http-signature'; -import { IRemoteUser } from '../../models/entities/user'; import perform from '../../remote/activitypub/perform'; -import { resolvePerson, updatePerson } from '../../remote/activitypub/models/person'; import Logger from '../../services/logger'; import { registerOrFetchInstanceDoc } from '../../services/register-or-fetch-instance-doc'; -import { Instances, Users, UserPublickeys } from '../../models'; +import { Instances } from '../../models'; import { instanceChart } from '../../services/chart'; -import { UserPublickey } from '../../models/entities/user-publickey'; import { fetchMeta } from '../../misc/fetch-meta'; -import { toPuny } from '../../misc/convert-host'; -import { validActor } from '../../remote/activitypub/type'; -import { ensure } from '../../prelude/ensure'; +import { toPuny, extractDbHost } from '../../misc/convert-host'; +import { getApId } from '../../remote/activitypub/type'; import { fetchNodeinfo } from '../../services/fetch-nodeinfo'; +import { InboxJobData } from '..'; +import DbResolver from '../../remote/activitypub/db-resolver'; +import { resolvePerson } from '../../remote/activitypub/models/person'; +import { LdSignature } from '../../remote/activitypub/misc/ld-signature'; const logger = new Logger('inbox'); // ユーザーのinboxにアクティビティが届いた時の処理 -export default async (job: Bull.Job): Promise => { - const signature = job.data.signature; +export default async (job: Bull.Job): Promise => { + const signature = job.data.signature; // HTTP-signature const activity = job.data.activity; //#region Log const info = Object.assign({}, activity); delete info['@context']; - delete info['signature']; logger.debug(JSON.stringify(info, null, 2)); //#endregion - const keyIdLower = signature.keyId.toLowerCase(); - let user: IRemoteUser; - let key: UserPublickey; - - if (keyIdLower.startsWith('acct:')) { - logger.warn(`Old keyId is no longer supported. ${keyIdLower}`); - return; - } - - // アクティビティ内のホストの検証 const host = toPuny(new URL(signature.keyId).hostname); - try { - ValidateActivity(activity, host); - } catch (e) { - logger.warn(e.message); - return; - } // ブロックしてたら中断 const meta = await fetchMeta(); if (meta.blockedHosts.includes(host)) { - logger.info(`Blocked request: ${host}`); - return; + return `Blocked request: ${host}`; } - const _key = await UserPublickeys.findOne({ - keyId: signature.keyId - }); - - if (_key) { - // 登録済みユーザー - user = await Users.findOne(_key.userId) as IRemoteUser; - key = _key; - } else { - // 未登録ユーザーの場合はリモート解決 - user = await resolvePerson(activity.actor) as IRemoteUser; - if (user == null) { - throw new Error('failed to resolve user'); - } - - key = await UserPublickeys.findOne(user.id).then(ensure); + const keyIdLower = signature.keyId.toLowerCase(); + if (keyIdLower.startsWith('acct:')) { + return `Old keyId is no longer supported. ${keyIdLower}`; } - // Update Person activityの場合は、ここで署名検証/更新処理まで実施して終了 - if (activity.type === 'Update') { - if (activity.object && validActor.includes(activity.object.type)) { - if (!httpSignature.verifySignature(signature, key.keyPem)) { - logger.warn('Update activity received, but signature verification failed.'); - } else { - updatePerson(activity.actor, null, activity.object); + const dbResolver = new DbResolver(); + + // HTTP-Signature keyIdを元にDBから取得 + let authUser = await dbResolver.getAuthUserFromKeyId(signature.keyId); + + // keyIdでわからなければ、activity.actorを元にDBから取得 || activity.actorを元にリモートから取得 + if (authUser == null) { + authUser = await dbResolver.getAuthUserFromApId(getApId(activity.actor)); + } + + // それでもわからなければ終了 + if (authUser == null) { + return `skip: failed to resolve user`; + } + + // HTTP-Signatureの検証 + if (!httpSignature.verifySignature(signature, authUser.key.keyPem)) { + return 'signature verification failed'; + } + + // signatureのsignerは、activity.actorと一致する必要がある + if (authUser.user.uri !== activity.actor) { + // 一致しなくても、でもLD-Signatureがありそうならそっちも見る + if (activity.signature) { + if (activity.signature.type !== 'RsaSignature2017') { + return `skip: unsupported LD-signature type ${activity.signature.type}`; + } + + // activity.signature.creator: https://example.oom/users/user#main-key + // みたいになっててUserを引っ張れば公開キーも入ることを期待する + if (activity.signature.creator) { + const candicate = activity.signature.creator.replace(/#.*/, ''); + await resolvePerson(candicate).catch(() => null); + } + + // keyIdからLD-Signatureのユーザーを取得 + authUser = await dbResolver.getAuthUserFromKeyId(activity.signature.creator); + if (authUser == null) { + return `skip: LD-Signatureのユーザーが取得できませんでした`; + } + + // LD-Signature検証 + const ldSignature = new LdSignature(); + const verified = await ldSignature.verifyRsaSignature2017(activity, authUser.key.keyPem).catch(() => false); + if (!verified) { + return `skip: LD-Signatureの検証に失敗しました`; + } + + // もう一度actorチェック + if (authUser.user.uri !== activity.actor) { + return `skip: LD-Signature user(${authUser.user.uri}) !== activity.actor(${activity.actor})`; } - return; } } - if (!httpSignature.verifySignature(signature, key.keyPem)) { - logger.error('signature verification failed'); - return; + // activity.idがあればホストが署名者のホストであることを確認する + if (typeof activity.id === 'string') { + const signerHost = extractDbHost(authUser.user.uri!); + const activityIdHost = extractDbHost(activity.id); + if (signerHost !== activityIdHost) { + return `skip: signerHost(${signerHost}) !== activity.id host(${activityIdHost}`; + } } // Update stats - registerOrFetchInstanceDoc(user.host).then(i => { + registerOrFetchInstanceDoc(authUser.user.host).then(i => { Instances.update(i.id, { latestRequestReceivedAt: new Date(), lastCommunicatedAt: new Date(), @@ -102,42 +118,6 @@ export default async (job: Bull.Job): Promise => { }); // アクティビティを処理 - await perform(user, activity); + await perform(authUser.user, activity); + return `ok`; }; - -/** - * Validate host in activity - * @param activity Activity - * @param host Expect host - */ -function ValidateActivity(activity: any, host: string) { - // id (if exists) - if (typeof activity.id === 'string') { - const uriHost = toPuny(new URL(activity.id).hostname); - if (host !== uriHost) { - const diag = activity.signature ? '. Has LD-Signature. Forwarded?' : ''; - throw new Error(`activity.id(${activity.id}) has different host(${host})${diag}`); - } - } - - // actor (if exists) - if (typeof activity.actor === 'string') { - const uriHost = toPuny(new URL(activity.actor).hostname); - if (host !== uriHost) throw new Error('activity.actor has different host'); - } - - // For Create activity - if (activity.type === 'Create' && activity.object) { - // object.id (if exists) - if (typeof activity.object.id === 'string') { - const uriHost = toPuny(new URL(activity.object.id).hostname); - if (host !== uriHost) throw new Error('activity.object.id has different host'); - } - - // object.attributedTo (if exists) - if (typeof activity.object.attributedTo === 'string') { - const uriHost = toPuny(new URL(activity.object.attributedTo).hostname); - if (host !== uriHost) throw new Error('activity.object.attributedTo has different host'); - } - } -} diff --git a/src/remote/activitypub/db-resolver.ts b/src/remote/activitypub/db-resolver.ts new file mode 100644 index 000000000..6f1cb1e11 --- /dev/null +++ b/src/remote/activitypub/db-resolver.ts @@ -0,0 +1,122 @@ +import config from '../../config'; +import { Note } from '../../models/entities/note'; +import { User, IRemoteUser } from '../../models/entities/user'; +import { UserPublickey } from '../../models/entities/user-publickey'; +import { Notes, Users, UserPublickeys } from '../../models'; +import { IObject, getApId } from './type'; +import { resolvePerson } from './models/person'; +import { ensure } from '../../prelude/ensure'; +import escapeRegexp = require('escape-regexp'); + +export default class DbResolver { + constructor() { + } + + /** + * AP Note => Misskey Note in DB + */ + public async getNoteFromApId(value: string | IObject): Promise { + const parsed = this.parseUri(value); + + if (parsed.id) { + return (await Notes.findOne({ + id: parsed.id + })) || null; + } + + if (parsed.uri) { + return (await Notes.findOne({ + uri: parsed.uri + })) || null; + } + + return null; + } + + /** + * AP Person => Misskey User in DB + */ + public async getUserFromApId(value: string | IObject): Promise { + const parsed = this.parseUri(value); + + if (parsed.id) { + return (await Users.findOne({ + id: parsed.id + })) || null; + } + + if (parsed.uri) { + return (await Users.findOne({ + uri: parsed.uri + })) || null; + } + + return null; + } + + /** + * AP KeyId => Misskey User and Key + */ + public async getAuthUserFromKeyId(keyId: string): Promise { + const key = await UserPublickeys.findOne({ + keyId + }); + + if (key == null) return null; + + const user = await Users.findOne(key.userId) as IRemoteUser; + + return { + user, + key + }; + } + + /** + * AP Actor id => Misskey User and Key + */ + public async getAuthUserFromApId(uri: string): Promise { + const user = await resolvePerson(uri) as IRemoteUser; + + if (user == null) return null; + + const key = await UserPublickeys.findOne(user.id).then(ensure); + + return { + user, + key + }; + } + + public parseUri(value: string | IObject): UriParseResult { + const uri = getApId(value); + + const localRegex = new RegExp('^' + escapeRegexp(config.url) + '/' + '(\\w+)' + '/' + '(\\w+)'); + const matchLocal = uri.match(localRegex); + + if (matchLocal) { + return { + type: matchLocal[1], + id: matchLocal[2] + }; + } else { + return { + uri + }; + } + } +} + +export type AuthUser = { + user: IRemoteUser; + key: UserPublickey; +}; + +type UriParseResult = { + /** id in DB (local object only) */ + id?: string; + /** uri in DB (remote object only) */ + uri?: string; + /** hint of type (local object only, ex: notes, users) */ + type?: string +}; diff --git a/src/remote/activitypub/kernel/accept/follow.ts b/src/remote/activitypub/kernel/accept/follow.ts index cf6763186..c067f7622 100644 --- a/src/remote/activitypub/kernel/accept/follow.ts +++ b/src/remote/activitypub/kernel/accept/follow.ts @@ -1,28 +1,22 @@ import { IRemoteUser } from '../../../../models/entities/user'; -import config from '../../../../config'; import accept from '../../../../services/following/requests/accept'; import { IFollow } from '../../type'; -import { Users } from '../../../../models'; +import DbResolver from '../../db-resolver'; -export default async (actor: IRemoteUser, activity: IFollow): Promise => { - const id = typeof activity.actor === 'string' ? activity.actor : activity.actor.id; - if (id == null) throw new Error('missing id'); +export default async (actor: IRemoteUser, activity: IFollow): Promise => { + // ※ activityはこっちから投げたフォローリクエストなので、activity.actorは存在するローカルユーザーである必要がある - if (!id.startsWith(config.url + '/')) { - return; - } - - const follower = await Users.findOne({ - id: id.split('/').pop() - }); + const dbResolver = new DbResolver(); + const follower = await dbResolver.getUserFromApId(activity.actor); if (follower == null) { - throw new Error('follower not found'); + return `skip: follower not found`; } if (follower.host != null) { - throw new Error('フォローリクエストしたユーザーはローカルユーザーではありません'); + return `skip: follower is not a local user`; } await accept(actor, follower); + return `ok`; }; diff --git a/src/remote/activitypub/kernel/block/index.ts b/src/remote/activitypub/kernel/block/index.ts index 24bc9d524..6c794e125 100644 --- a/src/remote/activitypub/kernel/block/index.ts +++ b/src/remote/activitypub/kernel/block/index.ts @@ -1,32 +1,22 @@ -import config from '../../../../config'; -import { IBlock, getApId } from '../../type'; +import { IBlock } from '../../type'; import block from '../../../../services/blocking/create'; -import { apLogger } from '../../logger'; -import { Users } from '../../../../models'; import { IRemoteUser } from '../../../../models/entities/user'; +import DbResolver from '../../db-resolver'; -const logger = apLogger; +export default async (actor: IRemoteUser, activity: IBlock): Promise => { + // ※ activity.objectにブロック対象があり、それは存在するローカルユーザーのはず -export default async (actor: IRemoteUser, activity: IBlock): Promise => { - const id = getApId(activity.object); - - const uri = getApId(activity); - - logger.info(`Block: ${uri}`); - - if (!id.startsWith(config.url + '/')) { - return; - } - - const blockee = await Users.findOne(id.split('/').pop()); + const dbResolver = new DbResolver(); + const blockee = await dbResolver.getUserFromApId(activity.object); if (blockee == null) { - throw new Error('blockee not found'); + return `skip: blockee not found`; } if (blockee.host != null) { - throw new Error('ブロックしようとしているユーザーはローカルユーザーではありません'); + return `skip: ブロックしようとしているユーザーはローカルユーザーではありません`; } - block(actor, blockee); + await block(actor, blockee); + return `ok`; }; diff --git a/src/remote/activitypub/kernel/create/note.ts b/src/remote/activitypub/kernel/create/note.ts index d7027b8f3..f4fb8e564 100644 --- a/src/remote/activitypub/kernel/create/note.ts +++ b/src/remote/activitypub/kernel/create/note.ts @@ -3,19 +3,39 @@ import { IRemoteUser } from '../../../../models/entities/user'; import { createNote, fetchNote } from '../../models/note'; import { getApId, IObject, ICreate } from '../../type'; import { getApLock } from '../../../../misc/app-lock'; +import { extractDbHost } from '../../../../misc/convert-host'; /** * 投稿作成アクティビティを捌きます */ -export default async function(resolver: Resolver, actor: IRemoteUser, note: IObject, silent = false, activity?: ICreate): Promise { +export default async function(resolver: Resolver, actor: IRemoteUser, note: IObject, silent = false, activity?: ICreate): Promise { const uri = getApId(note); + if (typeof note === 'object') { + if (actor.uri !== note.attributedTo) { + return `skip: actor.uri !== note.attributedTo`; + } + + if (typeof note.id === 'string') { + if (extractDbHost(actor.uri) !== extractDbHost(note.id)) { + return `skip: host in actor.uri !== note.id`; + } + } + } + const unlock = await getApLock(uri); try { const exist = await fetchNote(note); - if (exist == null) { - await createNote(note, resolver, silent); + if (exist) return 'skip: note exists'; + + await createNote(note, resolver, silent); + return 'ok'; + } catch (e) { + if (e.statusCode >= 400 && e.statusCode < 500) { + return `skip ${e.statusCode}`; + } else { + throw e; } } finally { unlock(); diff --git a/src/remote/activitypub/kernel/delete/note.ts b/src/remote/activitypub/kernel/delete/note.ts index b146e68a0..9d7574c29 100644 --- a/src/remote/activitypub/kernel/delete/note.ts +++ b/src/remote/activitypub/kernel/delete/note.ts @@ -1,22 +1,31 @@ import { IRemoteUser } from '../../../../models/entities/user'; import deleteNode from '../../../../services/note/delete'; import { apLogger } from '../../logger'; -import { Notes } from '../../../../models'; +import DbResolver from '../../db-resolver'; +import { getApLock } from '../../../../misc/app-lock'; const logger = apLogger; -export default async function(actor: IRemoteUser, uri: string): Promise { +export default async function(actor: IRemoteUser, uri: string): Promise { logger.info(`Deleting the Note: ${uri}`); - const note = await Notes.findOne({ uri }); + const unlock = await getApLock(uri); - if (note == null) { - throw new Error('note not found'); + try { + const dbResolver = new DbResolver(); + const note = await dbResolver.getNoteFromApId(uri); + + if (note == null) { + return 'note not found'; + } + + if (note.userId !== actor.id) { + return '投稿を削除しようとしているユーザーは投稿の作成者ではありません'; + } + + await deleteNode(actor, note); + return 'ok: deleted'; + } finally { + unlock(); } - - if (note.userId !== actor.id) { - throw new Error('投稿を削除しようとしているユーザーは投稿の作成者ではありません'); - } - - await deleteNode(actor, note); } diff --git a/src/remote/activitypub/kernel/follow.ts b/src/remote/activitypub/kernel/follow.ts index fca6d26cc..3e2063302 100644 --- a/src/remote/activitypub/kernel/follow.ts +++ b/src/remote/activitypub/kernel/follow.ts @@ -1,26 +1,20 @@ import { IRemoteUser } from '../../../models/entities/user'; -import config from '../../../config'; import follow from '../../../services/following/create'; import { IFollow } from '../type'; -import { Users } from '../../../models'; +import DbResolver from '../db-resolver'; -export default async (actor: IRemoteUser, activity: IFollow): Promise => { - const id = typeof activity.object === 'string' ? activity.object : activity.object.id; - if (id == null) throw new Error('missing id'); - - if (!id.startsWith(config.url + '/')) { - return; - } - - const followee = await Users.findOne(id.split('/').pop()); +export default async (actor: IRemoteUser, activity: IFollow): Promise => { + const dbResolver = new DbResolver(); + const followee = await dbResolver.getUserFromApId(activity.object); if (followee == null) { - throw new Error('followee not found'); + return `skip: followee not found`; } if (followee.host != null) { - throw new Error('フォローしようとしているユーザーはローカルユーザーではありません'); + return `skip: フォローしようとしているユーザーはローカルユーザーではありません`; } await follow(actor, followee, activity.id); + return `ok`; }; diff --git a/src/remote/activitypub/kernel/reject/follow.ts b/src/remote/activitypub/kernel/reject/follow.ts index bc7d03f9a..49e82c7af 100644 --- a/src/remote/activitypub/kernel/reject/follow.ts +++ b/src/remote/activitypub/kernel/reject/follow.ts @@ -1,26 +1,22 @@ import { IRemoteUser } from '../../../../models/entities/user'; -import config from '../../../../config'; import reject from '../../../../services/following/requests/reject'; import { IFollow } from '../../type'; -import { Users } from '../../../../models'; +import DbResolver from '../../db-resolver'; -export default async (actor: IRemoteUser, activity: IFollow): Promise => { - const id = typeof activity.actor === 'string' ? activity.actor : activity.actor.id; - if (id == null) throw new Error('missing id'); +export default async (actor: IRemoteUser, activity: IFollow): Promise => { + // ※ activityはこっちから投げたフォローリクエストなので、activity.actorは存在するローカルユーザーである必要がある - if (!id.startsWith(config.url + '/')) { - return; - } - - const follower = await Users.findOne(id.split('/').pop()); + const dbResolver = new DbResolver(); + const follower = await dbResolver.getUserFromApId(activity.actor); if (follower == null) { - throw new Error('follower not found'); + return `skip: follower not found`; } if (follower.host != null) { - throw new Error('フォローリクエストしたユーザーはローカルユーザーではありません'); + return `skip: follower is not a local user`; } await reject(actor, follower); + return `ok`; }; diff --git a/src/remote/activitypub/kernel/undo/block.ts b/src/remote/activitypub/kernel/undo/block.ts index 17eab0d2d..73000fc3f 100644 --- a/src/remote/activitypub/kernel/undo/block.ts +++ b/src/remote/activitypub/kernel/undo/block.ts @@ -1,33 +1,20 @@ -import config from '../../../../config'; import { IBlock } from '../../type'; import unblock from '../../../../services/blocking/delete'; -import { apLogger } from '../../logger'; import { IRemoteUser } from '../../../../models/entities/user'; -import { Users } from '../../../../models'; +import DbResolver from '../../db-resolver'; -const logger = apLogger; - -export default async (actor: IRemoteUser, activity: IBlock): Promise => { - const id = typeof activity.object === 'string' ? activity.object : activity.object.id; - if (id == null) throw new Error('missing id'); - - const uri = activity.id || activity; - - logger.info(`UnBlock: ${uri}`); - - if (!id.startsWith(config.url + '/')) { - return; - } - - const blockee = await Users.findOne(id.split('/').pop()); +export default async (actor: IRemoteUser, activity: IBlock): Promise => { + const dbResolver = new DbResolver(); + const blockee = await dbResolver.getUserFromApId(activity.object); if (blockee == null) { - throw new Error('blockee not found'); + return `skip: blockee not found`; } if (blockee.host != null) { - throw new Error('ブロック解除しようとしているユーザーはローカルユーザーではありません'); + return `skip: ブロック解除しようとしているユーザーはローカルユーザーではありません`; } - unblock(actor, blockee); + await unblock(actor, blockee); + return `ok`; }; diff --git a/src/remote/activitypub/kernel/undo/follow.ts b/src/remote/activitypub/kernel/undo/follow.ts index 2a42f8390..73a164030 100644 --- a/src/remote/activitypub/kernel/undo/follow.ts +++ b/src/remote/activitypub/kernel/undo/follow.ts @@ -1,26 +1,20 @@ -import config from '../../../../config'; import unfollow from '../../../../services/following/delete'; import cancelRequest from '../../../../services/following/requests/cancel'; import { IFollow } from '../../type'; import { IRemoteUser } from '../../../../models/entities/user'; -import { Users, FollowRequests, Followings } from '../../../../models'; +import { FollowRequests, Followings } from '../../../../models'; +import DbResolver from '../../db-resolver'; -export default async (actor: IRemoteUser, activity: IFollow): Promise => { - const id = typeof activity.object === 'string' ? activity.object : activity.object.id; - if (id == null) throw new Error('missing id'); - - if (!id.startsWith(config.url + '/')) { - return; - } - - const followee = await Users.findOne(id.split('/').pop()); +export default async (actor: IRemoteUser, activity: IFollow): Promise => { + const dbResolver = new DbResolver(); + const followee = await dbResolver.getUserFromApId(activity.object); if (followee == null) { - throw new Error('followee not found'); + return `skip: followee not found`; } if (followee.host != null) { - throw new Error('フォロー解除しようとしているユーザーはローカルユーザーではありません'); + return `skip: フォロー解除しようとしているユーザーはローカルユーザーではありません`; } const req = await FollowRequests.findOne({ @@ -35,9 +29,13 @@ export default async (actor: IRemoteUser, activity: IFollow): Promise => { if (req) { await cancelRequest(followee, actor); + return `ok: follow request canceled`; } if (following) { await unfollow(actor, followee); + return `ok: unfollowed`; } + + return `skip: リクエストもフォローもされていない`; }; diff --git a/src/remote/activitypub/kernel/update/index.ts b/src/remote/activitypub/kernel/update/index.ts index b8dff7339..ea7e6a063 100644 --- a/src/remote/activitypub/kernel/update/index.ts +++ b/src/remote/activitypub/kernel/update/index.ts @@ -1,28 +1,34 @@ import { IRemoteUser } from '../../../../models/entities/user'; -import { IUpdate, IObject } from '../../type'; +import { IUpdate, validActor } from '../../type'; import { apLogger } from '../../logger'; import { updateQuestion } from '../../models/question'; +import Resolver from '../../resolver'; +import { updatePerson } from '../../models/person'; /** * Updateアクティビティを捌きます */ -export default async (actor: IRemoteUser, activity: IUpdate): Promise => { +export default async (actor: IRemoteUser, activity: IUpdate): Promise => { if ('actor' in activity && actor.uri !== activity.actor) { - throw new Error('invalid actor'); + return `skip: invalid actor`; } apLogger.debug('Update'); - const object = activity.object as IObject; + const resolver = new Resolver(); - switch (object.type) { - case 'Question': - apLogger.debug('Question'); - await updateQuestion(object).catch(e => console.log(e)); - break; + const object = await resolver.resolve(activity.object).catch(e => { + apLogger.error(`Resolution failed: ${e}`); + throw e; + }); - default: - apLogger.warn(`Unknown type: ${object.type}`); - break; + if (validActor.includes(object.type)) { + await updatePerson(actor.uri!, resolver, object); + return `ok: Person updated`; + } else if (object.type === 'Question') { + await updateQuestion(object).catch(e => console.log(e)); + return `ok: Question updated`; + } else { + return `skip: Unknown type: ${object.type}`; } }; diff --git a/src/remote/activitypub/misc/contexts.ts b/src/remote/activitypub/misc/contexts.ts new file mode 100644 index 000000000..999e3ea5d --- /dev/null +++ b/src/remote/activitypub/misc/contexts.ts @@ -0,0 +1,522 @@ +/* tslint:disable:quotemark indent */ +const id_v1 = { + "@context": { + "id": "@id", + "type": "@type", + + "cred": "https://w3id.org/credentials#", + "dc": "http://purl.org/dc/terms/", + "identity": "https://w3id.org/identity#", + "perm": "https://w3id.org/permissions#", + "ps": "https://w3id.org/payswarm#", + "rdf": "http://www.w3.org/1999/02/22-rdf-syntax-ns#", + "rdfs": "http://www.w3.org/2000/01/rdf-schema#", + "sec": "https://w3id.org/security#", + "schema": "http://schema.org/", + "xsd": "http://www.w3.org/2001/XMLSchema#", + + "Group": "https://www.w3.org/ns/activitystreams#Group", + + "claim": {"@id": "cred:claim", "@type": "@id"}, + "credential": {"@id": "cred:credential", "@type": "@id"}, + "issued": {"@id": "cred:issued", "@type": "xsd:dateTime"}, + "issuer": {"@id": "cred:issuer", "@type": "@id"}, + "recipient": {"@id": "cred:recipient", "@type": "@id"}, + "Credential": "cred:Credential", + "CryptographicKeyCredential": "cred:CryptographicKeyCredential", + + "about": {"@id": "schema:about", "@type": "@id"}, + "address": {"@id": "schema:address", "@type": "@id"}, + "addressCountry": "schema:addressCountry", + "addressLocality": "schema:addressLocality", + "addressRegion": "schema:addressRegion", + "comment": "rdfs:comment", + "created": {"@id": "dc:created", "@type": "xsd:dateTime"}, + "creator": {"@id": "dc:creator", "@type": "@id"}, + "description": "schema:description", + "email": "schema:email", + "familyName": "schema:familyName", + "givenName": "schema:givenName", + "image": {"@id": "schema:image", "@type": "@id"}, + "label": "rdfs:label", + "name": "schema:name", + "postalCode": "schema:postalCode", + "streetAddress": "schema:streetAddress", + "title": "dc:title", + "url": {"@id": "schema:url", "@type": "@id"}, + "Person": "schema:Person", + "PostalAddress": "schema:PostalAddress", + "Organization": "schema:Organization", + + "identityService": {"@id": "identity:identityService", "@type": "@id"}, + "idp": {"@id": "identity:idp", "@type": "@id"}, + "Identity": "identity:Identity", + + "paymentProcessor": "ps:processor", + "preferences": {"@id": "ps:preferences", "@type": "@vocab"}, + + "cipherAlgorithm": "sec:cipherAlgorithm", + "cipherData": "sec:cipherData", + "cipherKey": "sec:cipherKey", + "digestAlgorithm": "sec:digestAlgorithm", + "digestValue": "sec:digestValue", + "domain": "sec:domain", + "expires": {"@id": "sec:expiration", "@type": "xsd:dateTime"}, + "initializationVector": "sec:initializationVector", + "member": {"@id": "schema:member", "@type": "@id"}, + "memberOf": {"@id": "schema:memberOf", "@type": "@id"}, + "nonce": "sec:nonce", + "normalizationAlgorithm": "sec:normalizationAlgorithm", + "owner": {"@id": "sec:owner", "@type": "@id"}, + "password": "sec:password", + "privateKey": {"@id": "sec:privateKey", "@type": "@id"}, + "privateKeyPem": "sec:privateKeyPem", + "publicKey": {"@id": "sec:publicKey", "@type": "@id"}, + "publicKeyPem": "sec:publicKeyPem", + "publicKeyService": {"@id": "sec:publicKeyService", "@type": "@id"}, + "revoked": {"@id": "sec:revoked", "@type": "xsd:dateTime"}, + "signature": "sec:signature", + "signatureAlgorithm": "sec:signatureAlgorithm", + "signatureValue": "sec:signatureValue", + "CryptographicKey": "sec:Key", + "EncryptedMessage": "sec:EncryptedMessage", + "GraphSignature2012": "sec:GraphSignature2012", + "LinkedDataSignature2015": "sec:LinkedDataSignature2015", + + "accessControl": {"@id": "perm:accessControl", "@type": "@id"}, + "writePermission": {"@id": "perm:writePermission", "@type": "@id"} + } +}; + +const security_v1 = { + "@context": { + "id": "@id", + "type": "@type", + + "dc": "http://purl.org/dc/terms/", + "sec": "https://w3id.org/security#", + "xsd": "http://www.w3.org/2001/XMLSchema#", + + "EcdsaKoblitzSignature2016": "sec:EcdsaKoblitzSignature2016", + "Ed25519Signature2018": "sec:Ed25519Signature2018", + "EncryptedMessage": "sec:EncryptedMessage", + "GraphSignature2012": "sec:GraphSignature2012", + "LinkedDataSignature2015": "sec:LinkedDataSignature2015", + "LinkedDataSignature2016": "sec:LinkedDataSignature2016", + "CryptographicKey": "sec:Key", + + "authenticationTag": "sec:authenticationTag", + "canonicalizationAlgorithm": "sec:canonicalizationAlgorithm", + "cipherAlgorithm": "sec:cipherAlgorithm", + "cipherData": "sec:cipherData", + "cipherKey": "sec:cipherKey", + "created": {"@id": "dc:created", "@type": "xsd:dateTime"}, + "creator": {"@id": "dc:creator", "@type": "@id"}, + "digestAlgorithm": "sec:digestAlgorithm", + "digestValue": "sec:digestValue", + "domain": "sec:domain", + "encryptionKey": "sec:encryptionKey", + "expiration": {"@id": "sec:expiration", "@type": "xsd:dateTime"}, + "expires": {"@id": "sec:expiration", "@type": "xsd:dateTime"}, + "initializationVector": "sec:initializationVector", + "iterationCount": "sec:iterationCount", + "nonce": "sec:nonce", + "normalizationAlgorithm": "sec:normalizationAlgorithm", + "owner": {"@id": "sec:owner", "@type": "@id"}, + "password": "sec:password", + "privateKey": {"@id": "sec:privateKey", "@type": "@id"}, + "privateKeyPem": "sec:privateKeyPem", + "publicKey": {"@id": "sec:publicKey", "@type": "@id"}, + "publicKeyBase58": "sec:publicKeyBase58", + "publicKeyPem": "sec:publicKeyPem", + "publicKeyWif": "sec:publicKeyWif", + "publicKeyService": {"@id": "sec:publicKeyService", "@type": "@id"}, + "revoked": {"@id": "sec:revoked", "@type": "xsd:dateTime"}, + "salt": "sec:salt", + "signature": "sec:signature", + "signatureAlgorithm": "sec:signingAlgorithm", + "signatureValue": "sec:signatureValue" + } +}; + +const activitystreams = { + "@context": { + "@vocab": "_:", + "xsd": "http://www.w3.org/2001/XMLSchema#", + "as": "https://www.w3.org/ns/activitystreams#", + "ldp": "http://www.w3.org/ns/ldp#", + "vcard": "http://www.w3.org/2006/vcard/ns#", + "id": "@id", + "type": "@type", + "Accept": "as:Accept", + "Activity": "as:Activity", + "IntransitiveActivity": "as:IntransitiveActivity", + "Add": "as:Add", + "Announce": "as:Announce", + "Application": "as:Application", + "Arrive": "as:Arrive", + "Article": "as:Article", + "Audio": "as:Audio", + "Block": "as:Block", + "Collection": "as:Collection", + "CollectionPage": "as:CollectionPage", + "Relationship": "as:Relationship", + "Create": "as:Create", + "Delete": "as:Delete", + "Dislike": "as:Dislike", + "Document": "as:Document", + "Event": "as:Event", + "Follow": "as:Follow", + "Flag": "as:Flag", + "Group": "as:Group", + "Ignore": "as:Ignore", + "Image": "as:Image", + "Invite": "as:Invite", + "Join": "as:Join", + "Leave": "as:Leave", + "Like": "as:Like", + "Link": "as:Link", + "Mention": "as:Mention", + "Note": "as:Note", + "Object": "as:Object", + "Offer": "as:Offer", + "OrderedCollection": "as:OrderedCollection", + "OrderedCollectionPage": "as:OrderedCollectionPage", + "Organization": "as:Organization", + "Page": "as:Page", + "Person": "as:Person", + "Place": "as:Place", + "Profile": "as:Profile", + "Question": "as:Question", + "Reject": "as:Reject", + "Remove": "as:Remove", + "Service": "as:Service", + "TentativeAccept": "as:TentativeAccept", + "TentativeReject": "as:TentativeReject", + "Tombstone": "as:Tombstone", + "Undo": "as:Undo", + "Update": "as:Update", + "Video": "as:Video", + "View": "as:View", + "Listen": "as:Listen", + "Read": "as:Read", + "Move": "as:Move", + "Travel": "as:Travel", + "IsFollowing": "as:IsFollowing", + "IsFollowedBy": "as:IsFollowedBy", + "IsContact": "as:IsContact", + "IsMember": "as:IsMember", + "subject": { + "@id": "as:subject", + "@type": "@id" + }, + "relationship": { + "@id": "as:relationship", + "@type": "@id" + }, + "actor": { + "@id": "as:actor", + "@type": "@id" + }, + "attributedTo": { + "@id": "as:attributedTo", + "@type": "@id" + }, + "attachment": { + "@id": "as:attachment", + "@type": "@id" + }, + "bcc": { + "@id": "as:bcc", + "@type": "@id" + }, + "bto": { + "@id": "as:bto", + "@type": "@id" + }, + "cc": { + "@id": "as:cc", + "@type": "@id" + }, + "context": { + "@id": "as:context", + "@type": "@id" + }, + "current": { + "@id": "as:current", + "@type": "@id" + }, + "first": { + "@id": "as:first", + "@type": "@id" + }, + "generator": { + "@id": "as:generator", + "@type": "@id" + }, + "icon": { + "@id": "as:icon", + "@type": "@id" + }, + "image": { + "@id": "as:image", + "@type": "@id" + }, + "inReplyTo": { + "@id": "as:inReplyTo", + "@type": "@id" + }, + "items": { + "@id": "as:items", + "@type": "@id" + }, + "instrument": { + "@id": "as:instrument", + "@type": "@id" + }, + "orderedItems": { + "@id": "as:items", + "@type": "@id", + "@container": "@list" + }, + "last": { + "@id": "as:last", + "@type": "@id" + }, + "location": { + "@id": "as:location", + "@type": "@id" + }, + "next": { + "@id": "as:next", + "@type": "@id" + }, + "object": { + "@id": "as:object", + "@type": "@id" + }, + "oneOf": { + "@id": "as:oneOf", + "@type": "@id" + }, + "anyOf": { + "@id": "as:anyOf", + "@type": "@id" + }, + "closed": { + "@id": "as:closed", + "@type": "xsd:dateTime" + }, + "origin": { + "@id": "as:origin", + "@type": "@id" + }, + "accuracy": { + "@id": "as:accuracy", + "@type": "xsd:float" + }, + "prev": { + "@id": "as:prev", + "@type": "@id" + }, + "preview": { + "@id": "as:preview", + "@type": "@id" + }, + "replies": { + "@id": "as:replies", + "@type": "@id" + }, + "result": { + "@id": "as:result", + "@type": "@id" + }, + "audience": { + "@id": "as:audience", + "@type": "@id" + }, + "partOf": { + "@id": "as:partOf", + "@type": "@id" + }, + "tag": { + "@id": "as:tag", + "@type": "@id" + }, + "target": { + "@id": "as:target", + "@type": "@id" + }, + "to": { + "@id": "as:to", + "@type": "@id" + }, + "url": { + "@id": "as:url", + "@type": "@id" + }, + "altitude": { + "@id": "as:altitude", + "@type": "xsd:float" + }, + "content": "as:content", + "contentMap": { + "@id": "as:content", + "@container": "@language" + }, + "name": "as:name", + "nameMap": { + "@id": "as:name", + "@container": "@language" + }, + "duration": { + "@id": "as:duration", + "@type": "xsd:duration" + }, + "endTime": { + "@id": "as:endTime", + "@type": "xsd:dateTime" + }, + "height": { + "@id": "as:height", + "@type": "xsd:nonNegativeInteger" + }, + "href": { + "@id": "as:href", + "@type": "@id" + }, + "hreflang": "as:hreflang", + "latitude": { + "@id": "as:latitude", + "@type": "xsd:float" + }, + "longitude": { + "@id": "as:longitude", + "@type": "xsd:float" + }, + "mediaType": "as:mediaType", + "published": { + "@id": "as:published", + "@type": "xsd:dateTime" + }, + "radius": { + "@id": "as:radius", + "@type": "xsd:float" + }, + "rel": "as:rel", + "startIndex": { + "@id": "as:startIndex", + "@type": "xsd:nonNegativeInteger" + }, + "startTime": { + "@id": "as:startTime", + "@type": "xsd:dateTime" + }, + "summary": "as:summary", + "summaryMap": { + "@id": "as:summary", + "@container": "@language" + }, + "totalItems": { + "@id": "as:totalItems", + "@type": "xsd:nonNegativeInteger" + }, + "units": "as:units", + "updated": { + "@id": "as:updated", + "@type": "xsd:dateTime" + }, + "width": { + "@id": "as:width", + "@type": "xsd:nonNegativeInteger" + }, + "describes": { + "@id": "as:describes", + "@type": "@id" + }, + "formerType": { + "@id": "as:formerType", + "@type": "@id" + }, + "deleted": { + "@id": "as:deleted", + "@type": "xsd:dateTime" + }, + "inbox": { + "@id": "ldp:inbox", + "@type": "@id" + }, + "outbox": { + "@id": "as:outbox", + "@type": "@id" + }, + "following": { + "@id": "as:following", + "@type": "@id" + }, + "followers": { + "@id": "as:followers", + "@type": "@id" + }, + "streams": { + "@id": "as:streams", + "@type": "@id" + }, + "preferredUsername": "as:preferredUsername", + "endpoints": { + "@id": "as:endpoints", + "@type": "@id" + }, + "uploadMedia": { + "@id": "as:uploadMedia", + "@type": "@id" + }, + "proxyUrl": { + "@id": "as:proxyUrl", + "@type": "@id" + }, + "liked": { + "@id": "as:liked", + "@type": "@id" + }, + "oauthAuthorizationEndpoint": { + "@id": "as:oauthAuthorizationEndpoint", + "@type": "@id" + }, + "oauthTokenEndpoint": { + "@id": "as:oauthTokenEndpoint", + "@type": "@id" + }, + "provideClientKey": { + "@id": "as:provideClientKey", + "@type": "@id" + }, + "signClientKey": { + "@id": "as:signClientKey", + "@type": "@id" + }, + "sharedInbox": { + "@id": "as:sharedInbox", + "@type": "@id" + }, + "Public": { + "@id": "as:Public", + "@type": "@id" + }, + "source": "as:source", + "likes": { + "@id": "as:likes", + "@type": "@id" + }, + "shares": { + "@id": "as:shares", + "@type": "@id" + } + } +}; + +export const CONTEXTS: Record = { + "https://w3id.org/identity/v1": id_v1, + "https://w3id.org/security/v1": security_v1, + "https://www.w3.org/ns/activitystreams": activitystreams, +}; diff --git a/src/remote/activitypub/misc/ld-signature.ts b/src/remote/activitypub/misc/ld-signature.ts new file mode 100644 index 000000000..d61b430f7 --- /dev/null +++ b/src/remote/activitypub/misc/ld-signature.ts @@ -0,0 +1,133 @@ +import * as crypto from 'crypto'; +import * as jsonld from 'jsonld'; +import { CONTEXTS } from './contexts'; +import fetch from 'node-fetch'; +import { httpAgent, httpsAgent } from '../../../misc/fetch'; + +// RsaSignature2017 based from https://github.com/transmute-industries/RsaSignature2017 + +export class LdSignature { + public debug = false; + public preLoad = true; + public loderTimeout = 10 * 1000; + + constructor() { + } + + public async signRsaSignature2017(data: any, privateKey: string, creator: string, domain?: string, created?: Date): Promise { + const options = { + type: 'RsaSignature2017', + creator, + domain, + nonce: crypto.randomBytes(16).toString('hex'), + created: (created || new Date()).toISOString() + } as { + type: string; + creator: string; + domain: string; + nonce: string; + created: string; + }; + + if (!domain) { + delete options.domain; + } + + const toBeSigned = await this.createVerifyData(data, options); + + const signer = crypto.createSign('sha256'); + signer.update(toBeSigned); + signer.end(); + + const signature = signer.sign(privateKey); + + return { + ...data, + signature: { + ...options, + signatureValue: signature.toString('base64') + } + }; + } + + public async verifyRsaSignature2017(data: any, publicKey: string): Promise { + const toBeSigned = await this.createVerifyData(data, data.signature); + const verifier = crypto.createVerify('sha256'); + verifier.update(toBeSigned); + return verifier.verify(publicKey, data.signature.signatureValue, 'base64'); + } + + public async createVerifyData(data: any, options: any) { + const transformedOptions = { + ...options, + '@context': 'https://w3id.org/identity/v1' + }; + delete transformedOptions['type']; + delete transformedOptions['id']; + delete transformedOptions['signatureValue']; + const canonizedOptions = await this.normalize(transformedOptions); + const optionsHash = this.sha256(canonizedOptions); + const transformedData = { ...data }; + delete transformedData['signature']; + const cannonidedData = await this.normalize(transformedData); + const documentHash = this.sha256(cannonidedData); + const verifyData = `${optionsHash}${documentHash}`; + return verifyData; + } + + public async normalize(data: any) { + const customLoader = this.getLoader(); + return await jsonld.normalize(data, { + documentLoader: customLoader + }); + } + + private getLoader() { + return async (url: string): Promise => { + if (!url.match('^https?\:\/\/')) throw `Invalid URL ${url}`; + + if (this.preLoad) { + if (url in CONTEXTS) { + if (this.debug) console.debug(`HIT: ${url}`); + return { + contextUrl: null, + document: CONTEXTS[url], + documentUrl: url + }; + } + } + + if (this.debug) console.debug(`MISS: ${url}`); + const document = await this.fetchDocument(url); + return { + contextUrl: null, + document: document, + documentUrl: url + }; + }; + } + + private async fetchDocument(url: string) { + const json = await fetch(url, { + headers: { + Accept: 'application/ld+json, application/json', + }, + timeout: this.loderTimeout, + agent: u => u.protocol == 'http:' ? httpAgent : httpsAgent, + }).then(res => { + if (!res.ok) { + throw `${res.status} ${res.statusText}`; + } else { + return res.json(); + } + }); + + return json; + } + + public sha256(data: string): string { + const hash = crypto.createHash('sha256'); + hash.update(data); + return hash.digest('hex'); + } +} diff --git a/src/remote/activitypub/models/note.ts b/src/remote/activitypub/models/note.ts index 155630767..feaee2f63 100644 --- a/src/remote/activitypub/models/note.ts +++ b/src/remote/activitypub/models/note.ts @@ -15,7 +15,7 @@ import { apLogger } from '../logger'; import { DriveFile } from '../../../models/entities/drive-file'; import { deliverQuestionUpdate } from '../../../services/note/polls/update'; import { extractDbHost, toPuny } from '../../../misc/convert-host'; -import { Notes, Emojis, Polls, MessagingMessages } from '../../../models'; +import { Emojis, Polls, MessagingMessages } from '../../../models'; import { Note } from '../../../models/entities/note'; import { IObject, getOneApId, getApId, getOneApHrefNullable, validPost, IPost, isEmoji } from '../type'; import { Emoji } from '../../../models/entities/emoji'; @@ -26,6 +26,7 @@ import { getApLock } from '../../../misc/app-lock'; import { createMessage } from '../../../services/messages/create'; import { parseAudience } from '../audience'; import { extractApMentions } from './mention'; +import DbResolver from '../db-resolver'; const logger = apLogger; @@ -56,24 +57,9 @@ export function validateNote(object: any, uri: string) { * * Misskeyに対象のNoteが登録されていればそれを返します。 */ -export async function fetchNote(value: string | IObject, resolver?: Resolver): Promise { - const uri = getApId(value); - - // URIがこのサーバーを指しているならデータベースからフェッチ - if (uri.startsWith(config.url + '/')) { - const id = uri.split('/').pop(); - return await Notes.findOne(id).then(x => x || null); - } - - //#region このサーバーに既に登録されていたらそれを返す - const exist = await Notes.findOne({ uri }); - - if (exist) { - return exist; - } - //#endregion - - return null; +export async function fetchNote(object: string | IObject): Promise { + const dbResolver = new DbResolver(); + return await dbResolver.getNoteFromApId(object); } /** diff --git a/src/remote/activitypub/type.ts b/src/remote/activitypub/type.ts index ac2b01c47..1a1a9d647 100644 --- a/src/remote/activitypub/type.ts +++ b/src/remote/activitypub/type.ts @@ -67,6 +67,15 @@ export interface IActivity extends IObject { actor: IObject | string; object: IObject | string; target?: IObject | string; + /** LD-Signature */ + signature?: { + type: string; + created: Date; + creator: string; + domain?: string; + nonce?: string; + signatureValue: string; + }; } export interface ICollection extends IObject { diff --git a/yarn.lock b/yarn.lock index 95e5dda48..1660853a0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -293,6 +293,11 @@ resolved "https://registry.yarnpkg.com/@types/double-ended-queue/-/double-ended-queue-2.1.1.tgz#f077386134f0f736d927812c85c43a04f21ddc27" integrity sha512-O2+umEIlHBVyi+ePmucPjpINqTvSnsz+hAok0D4IpvrOsIsDr6c34B0AbNXW2UDVYuxbv51z5dxnrRt23ohgWg== +"@types/escape-regexp@0.0.0": + version "0.0.0" + resolved "https://registry.yarnpkg.com/@types/escape-regexp/-/escape-regexp-0.0.0.tgz#bff0225f9ef30d0dbdbe0e2a24283ee5342990c3" + integrity sha512-HTansGo4tJ7K7W9I9LBdQqnHtPB/Y7tlS+EMrkboaAQLsRPhRpHaqAHe01K1HVXM5e1u1IplRd8EBh+pJrp7Dg== + "@types/eslint-visitor-keys@^1.0.0": version "1.0.0" resolved "https://registry.yarnpkg.com/@types/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz#1ee30d79544ca84d68d4b3cdb0af4f205663dd2d" @@ -414,6 +419,11 @@ resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.4.tgz#38fd73ddfd9b55abb1e1b2ed578cb55bd7b7d339" integrity sha512-8+KAKzEvSUdeo+kmqnKrqgeE+LcA0tjYWFY7RPProVYwnqDjukzO+3b6dLD56rYX5TdWejnEOLJYOIeh4CXKuA== +"@types/jsonld@1.5.1": + version "1.5.1" + resolved "https://registry.yarnpkg.com/@types/jsonld/-/jsonld-1.5.1.tgz#361e98bdc07814f5c98a42b4063430b243a8fa9b" + integrity sha512-8XI88iiCBVqmNCMBqPOgJhJPPuiIW1Tp2sXqe3NwD137ljhQVkDWY8cuYBBDZQoBYfGzUJvja527bbwqVbRnHQ== + "@types/katex@0.11.0": version "0.11.0" resolved "https://registry.yarnpkg.com/@types/katex/-/katex-0.11.0.tgz#b16c54ee670925ffef0616beae9e90c557e17334" @@ -1867,6 +1877,11 @@ caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001043: resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001048.tgz#4bb4f1bc2eb304e5e1154da80b93dee3f1cf447e" integrity sha512-g1iSHKVxornw0K8LG9LLdf+Fxnv7T1Z+mMsf0/YYLclQX4Cd522Ap0Lrw6NFqHgezit78dtyWxzlV2Xfc7vgRg== +canonicalize@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/canonicalize/-/canonicalize-1.0.1.tgz#657b4f3fa38a6ecb97a9e5b7b26d7a19cc6e0da9" + integrity sha512-N3cmB3QLhS5TJ5smKFf1w42rJXWe6C1qP01z4dxJiI5v269buii4fLHWETDyf7yEd0azGLNC63VxNMiPd2u0Cg== + caseless@~0.12.0: version "0.12.0" resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" @@ -5178,6 +5193,19 @@ json5@^1.0.1: dependencies: minimist "^1.2.0" +jsonld@3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/jsonld/-/jsonld-3.1.0.tgz#826a7a598942a3969d41301388c51b812a73c6d0" + integrity sha512-9x/AbUsXMMZBPxGy98Y8qMz7CU3WCq1n0KcNfR1P4RZml5oZiEQM+53/VtStOHUTUyC6fX9Sml5olUOZRARTZw== + dependencies: + canonicalize "^1.0.1" + lru-cache "^5.1.1" + object.fromentries "^2.0.2" + rdf-canonize "^1.0.2" + request "^2.88.0" + semver "^6.3.0" + xmldom "0.1.19" + jsprim@^1.2.2: version "1.4.1" resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.1.tgz#313e66bc1e5cc06e438bc1b7499c2e5c56acb6a2" @@ -6258,6 +6286,11 @@ node-fetch@2.6.0: resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.0.tgz#e633456386d4aa55863f676a7ab0daa8fdecb0fd" integrity sha512-8dG4H5ujfvFiqDmVu9fQ5bOHUC15JMjMY/Zumv26oOvvVJjM67KF8koCWIabKQ1GJIa9r2mMZscBq/TbdOcmNA== +node-forge@^0.9.1: + version "0.9.1" + resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.9.1.tgz#775368e6846558ab6676858a4d8c6e8d16c677b5" + integrity sha512-G6RlQt5Sb4GMBzXvhfkeFmbqR6MzhtnT7VTHuLadjkii3rdYHNdw0m8zA4BTxVIh68FicCQ2NSUANpsqkr9jvQ== + node-object-hash@^1.2.0: version "1.4.2" resolved "https://registry.yarnpkg.com/node-object-hash/-/node-object-hash-1.4.2.tgz#385833d85b229902b75826224f6077be969a9e94" @@ -6435,6 +6468,16 @@ object.defaults@^1.0.0, object.defaults@^1.1.0: for-own "^1.0.0" isobject "^3.0.0" +object.fromentries@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/object.fromentries/-/object.fromentries-2.0.2.tgz#4a09c9b9bb3843dd0f89acdb517a794d4f355ac9" + integrity sha512-r3ZiBH7MQppDJVLx6fhD618GKNG40CZYH9wgwdhKxBDDbQgjeWGGd4AtkZad84d291YxvWe7bJGuE65Anh0dxQ== + dependencies: + define-properties "^1.1.3" + es-abstract "^1.17.0-next.1" + function-bind "^1.1.1" + has "^1.0.3" + object.getownpropertydescriptors@^2.0.3, object.getownpropertydescriptors@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.0.tgz#369bf1f9592d8ab89d712dced5cb81c7c5352649" @@ -7690,6 +7733,14 @@ rc@^1.2.7: minimist "^1.2.0" strip-json-comments "~2.0.1" +rdf-canonize@^1.0.2: + version "1.1.0" + resolved "https://registry.yarnpkg.com/rdf-canonize/-/rdf-canonize-1.1.0.tgz#61d1609bbdb3234b8f38c9c34ad889bf670e089d" + integrity sha512-DV06OnhVfl2zcZJQCt+YvU+hoZVgpyQpNFLeAmghq8RJybUxD3B4LRzlBquYS5k+LLd8/c3g5Gnhkqjw5qRMvg== + dependencies: + node-forge "^0.9.1" + semver "^6.3.0" + read-pkg-up@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-1.0.1.tgz#9d63c13276c065918d57f002a57f40a1b643fb02" @@ -10225,6 +10276,11 @@ xmlchars@^2.2.0: resolved "https://registry.yarnpkg.com/xmlchars/-/xmlchars-2.2.0.tgz#060fe1bcb7f9c76fe2a17db86a9bc3ab894210cb" integrity sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw== +xmldom@0.1.19: + version "0.1.19" + resolved "https://registry.yarnpkg.com/xmldom/-/xmldom-0.1.19.tgz#631fc07776efd84118bf25171b37ed4d075a0abc" + integrity sha1-Yx/Ad3bv2EEYvyUXGzftTQdaCrw= + xtend@^4.0.0, xtend@~4.0.0, xtend@~4.0.1: version "4.0.2" resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54"