From 69763ac32b4e79e84d8338ba8e20b83add9d8560 Mon Sep 17 00:00:00 2001
From: Akihiko Odaki <nekomanma@pixiv.co.jp>
Date: Mon, 2 Apr 2018 18:36:47 +0900
Subject: [PATCH] Resolve account by signature in inbox

---
 src/processor/http/index.ts               |  2 ++
 src/processor/http/perform-activitypub.ts |  2 +-
 src/processor/http/process-inbox.ts       | 38 ++++++++++++++++++++
 src/remote/activitypub/resolve-person.ts  | 16 ++++-----
 src/remote/resolve-user.ts                |  2 +-
 src/remote/webfinger.ts                   | 26 +++++++++-----
 src/server/activitypub/inbox.ts           | 42 ++++-------------------
 7 files changed, 72 insertions(+), 56 deletions(-)
 create mode 100644 src/processor/http/process-inbox.ts

diff --git a/src/processor/http/index.ts b/src/processor/http/index.ts
index a001cf11f7..b3161cb992 100644
--- a/src/processor/http/index.ts
+++ b/src/processor/http/index.ts
@@ -1,10 +1,12 @@
 import follow from './follow';
 import performActivityPub from './perform-activitypub';
+import processInbox from './process-inbox';
 import reportGitHubFailure from './report-github-failure';
 
 const handlers = {
   follow,
   performActivityPub,
+  processInbox,
   reportGitHubFailure,
 };
 
diff --git a/src/processor/http/perform-activitypub.ts b/src/processor/http/perform-activitypub.ts
index d8981ea126..420ed9ec75 100644
--- a/src/processor/http/perform-activitypub.ts
+++ b/src/processor/http/perform-activitypub.ts
@@ -2,5 +2,5 @@ import User from '../../models/user';
 import act from '../../remote/activitypub/act';
 
 export default ({ data }, done) => User.findOne({ _id: data.actor })
-	.then(actor => act(actor, data.outbox, data.distribute))
+	.then(actor => act(actor, data.outbox, false))
 	.then(() => done(), done);
diff --git a/src/processor/http/process-inbox.ts b/src/processor/http/process-inbox.ts
new file mode 100644
index 0000000000..78c20f8a7e
--- /dev/null
+++ b/src/processor/http/process-inbox.ts
@@ -0,0 +1,38 @@
+import { verifySignature } from 'http-signature';
+import parseAcct from '../../acct/parse';
+import User, { IRemoteUser } from '../../models/user';
+import act from '../../remote/activitypub/act';
+import resolvePerson from '../../remote/activitypub/resolve-person';
+
+export default ({ data }, done) => (async () => {
+	const keyIdLower = data.signature.keyId.toLowerCase();
+	let user;
+
+	if (keyIdLower.startsWith('acct:')) {
+		const { username, host } = parseAcct(keyIdLower.slice('acct:'.length));
+		if (host === null) {
+			throw 'request was made by local user';
+		}
+
+		user = await User.findOne({ usernameLower: username, hostLower: host }) as IRemoteUser;
+	} else {
+		user = await User.findOne({
+			host: { $ne: null },
+			'account.publicKey.id': data.signature.keyId
+		}) as IRemoteUser;
+
+		if (user === null) {
+			user = await resolvePerson(data.signature.keyId);
+		}
+	}
+
+	if (user === null) {
+		throw 'failed to resolve user';
+	}
+
+	if (!verifySignature(data.signature, user.account.publicKey.publicKeyPem)) {
+		throw 'signature verification failed';
+	}
+
+	await act(user, data.inbox, true);
+})().then(done, done);
diff --git a/src/remote/activitypub/resolve-person.ts b/src/remote/activitypub/resolve-person.ts
index 4a2636b2f7..59be65908e 100644
--- a/src/remote/activitypub/resolve-person.ts
+++ b/src/remote/activitypub/resolve-person.ts
@@ -10,18 +10,14 @@ async function isCollection(collection) {
 	return ['Collection', 'OrderedCollection'].includes(collection.type);
 }
 
-export default async (value, usernameLower, hostLower, acctLower) => {
-	if (!validateUsername(usernameLower)) {
-		throw new Error();
-	}
-
+export default async (value, verifier?: string) => {
 	const { resolver, object } = await new Resolver().resolveOne(value);
 
 	if (
 		object === null ||
 		object.type !== 'Person' ||
 		typeof object.preferredUsername !== 'string' ||
-		object.preferredUsername.toLowerCase() !== usernameLower ||
+		!validateUsername(object.preferredUsername) ||
 		!isValidName(object.name) ||
 		!isValidDescription(object.summary)
 	) {
@@ -41,9 +37,11 @@ export default async (value, usernameLower, hostLower, acctLower) => {
 			resolved => isCollection(resolved.object) ? resolved.object : null,
 			() => null
 		),
-		webFinger(object.id, acctLower),
+		webFinger(object.id, verifier),
 	]);
 
+	const host = toUnicode(finger.subject.replace(/^.*?@/, ''));
+	const hostLower = host.replace(/[A-Z]+/, matched => matched.toLowerCase());
 	const summaryDOM = JSDOM.fragment(object.summary);
 
 	// Create user
@@ -58,8 +56,8 @@ export default async (value, usernameLower, hostLower, acctLower) => {
 		postsCount: outbox ? outbox.totalItem || 0 : 0,
 		driveCapacity: 1024 * 1024 * 8, // 8MiB
 		username: object.preferredUsername,
-		usernameLower,
-		host: toUnicode(finger.subject.replace(/^.*?@/, '')),
+		usernameLower: object.preferredUsername.toLowerCase(),
+		host,
 		hostLower,
 		account: {
 			publicKey: {
diff --git a/src/remote/resolve-user.ts b/src/remote/resolve-user.ts
index a393092839..48219e8cb3 100644
--- a/src/remote/resolve-user.ts
+++ b/src/remote/resolve-user.ts
@@ -19,7 +19,7 @@ export default async (username, host, option) => {
 			throw new Error();
 		}
 
-		user = await resolvePerson(self.href, usernameLower, hostLower, acctLower);
+		user = await resolvePerson(self.href, acctLower);
 	}
 
 	return user;
diff --git a/src/remote/webfinger.ts b/src/remote/webfinger.ts
index fec5da689c..4c0304e3f1 100644
--- a/src/remote/webfinger.ts
+++ b/src/remote/webfinger.ts
@@ -12,14 +12,22 @@ type IWebFinger = {
   subject: string;
 };
 
-export default (query, verifier): Promise<IWebFinger> => new Promise((res, rej) => webFinger.lookup(query, (error, result) => {
-	if (error) {
-		return rej(error);
+export default async function resolve(query, verifier?: string): Promise<IWebFinger> {
+	const finger = await new Promise((res, rej) => webFinger.lookup(query, (error, result) => {
+		if (error) {
+			return rej(error);
+		}
+
+		res(result.object);
+	})) as IWebFinger;
+
+	if (verifier) {
+		if (finger.subject.toLowerCase().replace(/^acct:/, '') !== verifier) {
+			throw 'WebFinger verfification failed';
+		}
+
+		return finger;
 	}
 
-	if (result.object.subject.toLowerCase().replace(/^acct:/, '') !== verifier) {
-		return rej('WebFinger verfification failed');
-	}
-
-	res(result.object);
-}));
+	return resolve(finger.subject, finger.subject.toLowerCase());
+}
diff --git a/src/server/activitypub/inbox.ts b/src/server/activitypub/inbox.ts
index 2de2bd9646..5de8433850 100644
--- a/src/server/activitypub/inbox.ts
+++ b/src/server/activitypub/inbox.ts
@@ -1,9 +1,7 @@
 import * as bodyParser from 'body-parser';
 import * as express from 'express';
-import { parseRequest, verifySignature } from 'http-signature';
-import User, { IRemoteUser } from '../../models/user';
+import { parseRequest } from 'http-signature';
 import queue from '../../queue';
-import parseAcct from '../../acct/parse';
 
 const app = express();
 
@@ -14,48 +12,20 @@ app.post('/@:user/inbox', bodyParser.json({
 		return true;
 	}
 }), async (req, res) => {
-	let parsed;
+	let signature;
 
 	req.headers.authorization = 'Signature ' + req.headers.signature;
 
 	try {
-		parsed = parseRequest(req);
+		signature = parseRequest(req);
 	} catch (exception) {
 		return res.sendStatus(401);
 	}
 
-	const keyIdLower = parsed.keyId.toLowerCase();
-	let query;
-
-	if (keyIdLower.startsWith('acct:')) {
-		const { username, host } = parseAcct(keyIdLower.slice('acct:'.length));
-		if (host === null) {
-			return res.sendStatus(401);
-		}
-
-		query = { usernameLower: username, hostLower: host };
-	} else {
-		query = {
-			host: { $ne: null },
-			'account.publicKey.id': parsed.keyId
-		};
-	}
-
-	const user = await User.findOne(query) as IRemoteUser;
-
-	if (user === null) {
-		return res.sendStatus(401);
-	}
-
-	if (!verifySignature(parsed, user.account.publicKey.publicKeyPem)) {
-		return res.sendStatus(401);
-	}
-
 	queue.create('http', {
-		type: 'performActivityPub',
-		actor: user._id,
-		outbox: req.body,
-		distribute: true,
+		type: 'processInbox',
+		inbox: req.body,
+		signature,
 	}).save();
 
 	return res.status(202).end();