summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMeiMei <30769358+mei23@users.noreply.github.com>2020-05-09 08:21:42 +0900
committerGitHub <noreply@github.com>2020-05-09 08:21:42 +0900
commit070f1f3c6ee3188e1b16236366877c1c243601c1 (patch)
treef77fa43be8524698bc9e46246748a3163dbd4a7a
parentWebAuthnでログインできないのを修正 (#6327) (diff)
downloadsharkey-070f1f3c6ee3188e1b16236366877c1c243601c1.tar.gz
sharkey-070f1f3c6ee3188e1b16236366877c1c243601c1.tar.bz2
sharkey-070f1f3c6ee3188e1b16236366877c1c243601c1.zip
APリファクタとLD-Signatureの検証に対応 (#6300)
* DbResolver * inbox types * 認証順を変更 * User/Keyあたりをまとめる * LD-Signatue * Validate contexts url * LD-Signature DocumentLoaderにProxyとTimeout
-rw-r--r--COPYING4
-rw-r--r--package.json4
-rw-r--r--src/@types/http-signature.d.ts4
-rw-r--r--src/queue/index.ts7
-rw-r--r--src/queue/processors/inbox.ts164
-rw-r--r--src/remote/activitypub/db-resolver.ts122
-rw-r--r--src/remote/activitypub/kernel/accept/follow.ts22
-rw-r--r--src/remote/activitypub/kernel/block/index.ts30
-rw-r--r--src/remote/activitypub/kernel/create/note.ts26
-rw-r--r--src/remote/activitypub/kernel/delete/note.ts29
-rw-r--r--src/remote/activitypub/kernel/follow.ts20
-rw-r--r--src/remote/activitypub/kernel/reject/follow.ts20
-rw-r--r--src/remote/activitypub/kernel/undo/block.ts29
-rw-r--r--src/remote/activitypub/kernel/undo/follow.ts24
-rw-r--r--src/remote/activitypub/kernel/update/index.ts30
-rw-r--r--src/remote/activitypub/misc/contexts.ts522
-rw-r--r--src/remote/activitypub/misc/ld-signature.ts133
-rw-r--r--src/remote/activitypub/models/note.ts24
-rw-r--r--src/remote/activitypub/type.ts9
-rw-r--r--yarn.lock56
20 files changed, 1049 insertions, 230 deletions
diff --git a/COPYING b/COPYING
index 0a2e4cd470..5abc3e9895 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 29146a8d50..366a98d559 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 6366b2addd..8d484312dc 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 76e26d8e46..163c57d691 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 74108f354b..6eea2e5646 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<void> => {
- const signature = job.data.signature;
+export default async (job: Bull.Job<InboxJobData>): Promise<string> => {
+ 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
- });
+ const keyIdLower = signature.keyId.toLowerCase();
+ if (keyIdLower.startsWith('acct:')) {
+ return `Old keyId is no longer supported. ${keyIdLower}`;
+ }
- 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');
- }
+ const dbResolver = new DbResolver();
+
+ // HTTP-Signature keyIdを元にDBから取得
+ let authUser = await dbResolver.getAuthUserFromKeyId(signature.keyId);
- key = await UserPublickeys.findOne(user.id).then(ensure);
+ // keyIdでわからなければ、activity.actorを元にDBから取得 || activity.actorを元にリモートから取得
+ if (authUser == null) {
+ authUser = await dbResolver.getAuthUserFromApId(getApId(activity.actor));
}
- // 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);
+ // それでもわからなければ終了
+ 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<void> => {
});
// アクティビティを処理
- 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 0000000000..6f1cb1e110
--- /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<Note | null> {
+ 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<User | null> {
+ 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<AuthUser | null> {
+ 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<AuthUser | null> {
+ 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 cf6763186e..c067f7622a 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<void> => {
- 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<string> => {
+ // ※ 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 24bc9d524f..6c794e1250 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<string> => {
+ // ※ activity.objectにブロック対象があり、それは存在するローカルユーザーのはず
-export default async (actor: IRemoteUser, activity: IBlock): Promise<void> => {
- 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 d7027b8f3f..f4fb8e5647 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<void> {
+export default async function(resolver: Resolver, actor: IRemoteUser, note: IObject, silent = false, activity?: ICreate): Promise<string> {
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 b146e68a07..9d7574c29b 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<void> {
+export default async function(actor: IRemoteUser, uri: string): Promise<string> {
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.userId !== actor.id) {
- throw new Error('投稿を削除しようとしているユーザーは投稿の作成者ではありません');
- }
+ if (note == null) {
+ return 'note not found';
+ }
- await deleteNode(actor, note);
+ if (note.userId !== actor.id) {
+ return '投稿を削除しようとしているユーザーは投稿の作成者ではありません';
+ }
+
+ await deleteNode(actor, note);
+ return 'ok: deleted';
+ } finally {
+ unlock();
+ }
}
diff --git a/src/remote/activitypub/kernel/follow.ts b/src/remote/activitypub/kernel/follow.ts
index fca6d26ccc..3e2063302a 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<void> => {
- 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<string> => {
+ 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 bc7d03f9a9..49e82c7afc 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<void> => {
- 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<string> => {
+ // ※ 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 17eab0d2d0..73000fc3f1 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<void> => {
- 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<string> => {
+ 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 2a42f83907..73a164030b 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<void> => {
- 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<string> => {
+ 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<void> => {
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 b8dff73395..ea7e6a063e 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<void> => {
+export default async (actor: IRemoteUser, activity: IUpdate): Promise<string> => {
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 0000000000..999e3ea5d7
--- /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<string, any> = {
+ "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 0000000000..d61b430f7a
--- /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<any> {
+ 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<boolean> {
+ 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<any> => {
+ 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 1556307670..feaee2f630 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<Note | null> {
- 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<Note | null> {
+ const dbResolver = new DbResolver();
+ return await dbResolver.getNoteFromApId(object);
}
/**
diff --git a/src/remote/activitypub/type.ts b/src/remote/activitypub/type.ts
index ac2b01c474..1a1a9d6475 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 95e5dda483..1660853a00 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"