From bd3d57a67f6d7c6a01516410d2322e6ffbd2f5ad Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Wed, 11 Apr 2018 17:40:01 +0900
Subject: [PATCH] =?UTF-8?q?=E3=82=B9=E3=83=88=E3=83=AA=E3=83=BC=E3=83=A0?=
 =?UTF-8?q?=E7=B5=8C=E7=94=B1=E3=81=A7API=E3=81=AB=E3=83=AA=E3=82=AF?=
 =?UTF-8?q?=E3=82=A8=E3=82=B9=E3=83=88=E3=81=A7=E3=81=8D=E3=82=8B=E3=82=88?=
 =?UTF-8?q?=E3=81=86=E3=81=AB?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 src/client/app/common/mios.ts           | 15 ++++---
 src/models/app.ts                       |  2 +-
 src/server/api/api-handler.ts           | 60 ++++++++-----------------
 src/server/api/authenticate.ts          | 46 ++++---------------
 src/server/api/call.ts                  | 55 +++++++++++++++++++++++
 src/server/api/endpoints/app/show.ts    | 18 +++-----
 src/server/api/endpoints/i.ts           |  4 +-
 src/server/api/endpoints/i/update.ts    | 10 ++---
 src/server/api/endpoints/meta.ts        |  3 --
 src/server/api/endpoints/sw/register.ts |  8 +---
 src/server/api/index.ts                 |  1 -
 src/server/api/limitter.ts              | 12 ++---
 src/server/api/reply.ts                 | 13 ------
 src/server/api/stream/home.ts           | 24 ++++++++--
 src/server/api/streaming.ts             | 45 ++-----------------
 15 files changed, 137 insertions(+), 179 deletions(-)
 create mode 100644 src/server/api/call.ts
 delete mode 100644 src/server/api/reply.ts

diff --git a/src/client/app/common/mios.ts b/src/client/app/common/mios.ts
index 7baf974adf..5e0c7d2f3b 100644
--- a/src/client/app/common/mios.ts
+++ b/src/client/app/common/mios.ts
@@ -444,23 +444,28 @@ export default class MiOS extends EventEmitter {
 		// Append a credential
 		if (this.isSignedIn) (data as any).i = this.i.token;
 
-		// TODO
-		//const viaStream = localStorage.getItem('enableExperimental') == 'true';
+		const viaStream = localStorage.getItem('enableExperimental') == 'true';
 
 		return new Promise((resolve, reject) => {
-			/*if (viaStream) {
+			if (viaStream) {
 				const stream = this.stream.borrow();
 				const id = Math.random().toString();
+
 				stream.once(`api-res:${id}`, res => {
-					resolve(res);
+					if (res.res) {
+						resolve(res.res);
+					} else {
+						reject(res.e);
+					}
 				});
+
 				stream.send({
 					type: 'api',
 					id,
 					endpoint,
 					data
 				});
-			} else {*/
+			} else {
 				const req = {
 					id: uuid(),
 					date: new Date(),
diff --git a/src/models/app.ts b/src/models/app.ts
index 446f0c62f4..45c95d92d8 100644
--- a/src/models/app.ts
+++ b/src/models/app.ts
@@ -19,7 +19,7 @@ export type IApp = {
 	nameId: string;
 	nameIdLower: string;
 	description: string;
-	permission: string;
+	permission: string[];
 	callbackUrl: string;
 };
 
diff --git a/src/server/api/api-handler.ts b/src/server/api/api-handler.ts
index fb603a0e2a..409069b6a0 100644
--- a/src/server/api/api-handler.ts
+++ b/src/server/api/api-handler.ts
@@ -2,55 +2,33 @@ import * as express from 'express';
 
 import { Endpoint } from './endpoints';
 import authenticate from './authenticate';
-import { IAuthContext } from './authenticate';
-import _reply from './reply';
-import limitter from './limitter';
+import call from './call';
+import { IUser } from '../../models/user';
+import { IApp } from '../../models/app';
 
 export default async (endpoint: Endpoint, req: express.Request, res: express.Response) => {
-	const reply = _reply.bind(null, res);
-	let ctx: IAuthContext;
+	const reply = (x?: any, y?: any) => {
+		if (x === undefined) {
+			res.sendStatus(204);
+		} else if (typeof x === 'number') {
+			res.status(x).send({
+				error: x === 500 ? 'INTERNAL_ERROR' : y
+			});
+		} else {
+			res.send(x);
+		}
+	};
+
+	let user: IUser;
+	let app: IApp;
 
 	// Authentication
 	try {
-		ctx = await authenticate(req);
+		[user, app] = await authenticate(req.body['i']);
 	} catch (e) {
 		return reply(403, 'AUTHENTICATION_FAILED');
 	}
 
-	if (endpoint.secure && !ctx.isSecure) {
-		return reply(403, 'ACCESS_DENIED');
-	}
-
-	if (endpoint.withCredential && ctx.user == null) {
-		return reply(401, 'PLZ_SIGNIN');
-	}
-
-	if (ctx.app && endpoint.kind) {
-		if (!ctx.app.permission.some(p => p === endpoint.kind)) {
-			return reply(403, 'ACCESS_DENIED');
-		}
-	}
-
-	if (endpoint.withCredential && endpoint.limit) {
-		try {
-			await limitter(endpoint, ctx); // Rate limit
-		} catch (e) {
-			// drop request if limit exceeded
-			return reply(429);
-		}
-	}
-
-	let exec = require(`${__dirname}/endpoints/${endpoint.name}`);
-
-	if (endpoint.withFile) {
-		exec = exec.bind(null, req.file);
-	}
-
 	// API invoking
-	try {
-		const res = await exec(req.body, ctx.user, ctx.app, ctx.isSecure);
-		reply(res);
-	} catch (e) {
-		reply(400, e);
-	}
+	call(endpoint, user, app, req.body, req).then(reply).catch(e => reply(400, e));
 };
diff --git a/src/server/api/authenticate.ts b/src/server/api/authenticate.ts
index adbeeb3b34..836fb7cfe8 100644
--- a/src/server/api/authenticate.ts
+++ b/src/server/api/authenticate.ts
@@ -1,50 +1,24 @@
-import * as express from 'express';
-import App from '../../models/app';
+import App, { IApp } from '../../models/app';
 import { default as User, IUser } from '../../models/user';
 import AccessToken from '../../models/access-token';
 import isNativeToken from './common/is-native-token';
 
-export interface IAuthContext {
-	/**
-	 * App which requested
-	 */
-	app: any;
-
-	/**
-	 * Authenticated user
-	 */
-	user: IUser;
-
-	/**
-	 * Whether requested with a User-Native Token
-	 */
-	isSecure: boolean;
-}
-
-export default (req: express.Request) => new Promise<IAuthContext>(async (resolve, reject) => {
-	const token = req.body['i'] as string;
-
+export default (token: string) => new Promise<[IUser, IApp]>(async (resolve, reject) => {
 	if (token == null) {
-		return resolve({
-			app: null,
-			user: null,
-			isSecure: false
-		});
+		resolve([null, null]);
+		return;
 	}
 
 	if (isNativeToken(token)) {
+		// Fetch user
 		const user: IUser = await User
-			.findOne({ 'token': token });
+			.findOne({ token });
 
 		if (user === null) {
 			return reject('user not found');
 		}
 
-		return resolve({
-			app: null,
-			user: user,
-			isSecure: true
-		});
+		resolve([user, null]);
 	} else {
 		const accessToken = await AccessToken.findOne({
 			hash: token.toLowerCase()
@@ -60,10 +34,6 @@ export default (req: express.Request) => new Promise<IAuthContext>(async (resolv
 		const user = await User
 			.findOne({ _id: accessToken.userId });
 
-		return resolve({
-			app: app,
-			user: user,
-			isSecure: false
-		});
+		resolve([user, app]);
 	}
 });
diff --git a/src/server/api/call.ts b/src/server/api/call.ts
new file mode 100644
index 0000000000..1bfe94bb74
--- /dev/null
+++ b/src/server/api/call.ts
@@ -0,0 +1,55 @@
+import * as express from 'express';
+
+import endpoints, { Endpoint } from './endpoints';
+import limitter from './limitter';
+import { IUser } from '../../models/user';
+import { IApp } from '../../models/app';
+
+export default (endpoint: string | Endpoint, user: IUser, app: IApp, data: any, req?: express.Request) => new Promise(async (ok, rej) => {
+	const isSecure = user != null && app == null;
+
+	//console.log(endpoint, user, app, data);
+
+	const ep = typeof endpoint == 'string' ? endpoints.find(e => e.name == endpoint) : endpoint;
+
+	if (ep.secure && !isSecure) {
+		return rej('ACCESS_DENIED');
+	}
+
+	if (ep.withCredential && user == null) {
+		return rej('SIGNIN_REQUIRED');
+	}
+
+	if (app && ep.kind) {
+		if (!app.permission.some(p => p === ep.kind)) {
+			return rej('PERMISSION_DENIED');
+		}
+	}
+
+	if (ep.withCredential && ep.limit) {
+		try {
+			await limitter(ep, user); // Rate limit
+		} catch (e) {
+			// drop request if limit exceeded
+			return rej('RATE_LIMIT_EXCEEDED');
+		}
+	}
+
+	let exec = require(`${__dirname}/endpoints/${ep.name}`);
+
+	if (ep.withFile && req) {
+		exec = exec.bind(null, req.file);
+	}
+
+	let res;
+
+	// API invoking
+	try {
+		res = await exec(data, user, app);
+	} catch (e) {
+		rej(e);
+		return;
+	}
+
+	ok(res);
+});
diff --git a/src/server/api/endpoints/app/show.ts b/src/server/api/endpoints/app/show.ts
index 3a3c25f47c..99a2093b68 100644
--- a/src/server/api/endpoints/app/show.ts
+++ b/src/server/api/endpoints/app/show.ts
@@ -36,14 +36,10 @@ import App, { pack } from '../../../../models/app';
 
 /**
  * Show an app
- *
- * @param {any} params
- * @param {any} user
- * @param {any} _
- * @param {any} isSecure
- * @return {Promise<any>}
  */
-module.exports = (params, user, _, isSecure) => new Promise(async (res, rej) => {
+module.exports = (params, user, app) => new Promise(async (res, rej) => {
+	const isSecure = user != null && app == null;
+
 	// Get 'appId' parameter
 	const [appId, appIdErr] = $(params.appId).optional.id().$;
 	if (appIdErr) return rej('invalid appId param');
@@ -57,16 +53,16 @@ module.exports = (params, user, _, isSecure) => new Promise(async (res, rej) =>
 	}
 
 	// Lookup app
-	const app = appId !== undefined
+	const ap = appId !== undefined
 		? await App.findOne({ _id: appId })
 		: await App.findOne({ nameIdLower: nameId.toLowerCase() });
 
-	if (app === null) {
+	if (ap === null) {
 		return rej('app not found');
 	}
 
 	// Send response
-	res(await pack(app, user, {
-		includeSecret: isSecure && app.userId.equals(user._id)
+	res(await pack(ap, user, {
+		includeSecret: isSecure && ap.userId.equals(user._id)
 	}));
 });
diff --git a/src/server/api/endpoints/i.ts b/src/server/api/endpoints/i.ts
index 0be30500c4..379c3c4d88 100644
--- a/src/server/api/endpoints/i.ts
+++ b/src/server/api/endpoints/i.ts
@@ -6,7 +6,9 @@ import User, { pack } from '../../../models/user';
 /**
  * Show myself
  */
-module.exports = (params, user, _, isSecure) => new Promise(async (res, rej) => {
+module.exports = (params, user, app) => new Promise(async (res, rej) => {
+	const isSecure = user != null && app == null;
+
 	// Serialize
 	res(await pack(user, user, {
 		detail: true,
diff --git a/src/server/api/endpoints/i/update.ts b/src/server/api/endpoints/i/update.ts
index 36be2774f6..f3c9d777b5 100644
--- a/src/server/api/endpoints/i/update.ts
+++ b/src/server/api/endpoints/i/update.ts
@@ -7,14 +7,10 @@ import event from '../../../../publishers/stream';
 
 /**
  * Update myself
- *
- * @param {any} params
- * @param {any} user
- * @param {any} _
- * @param {boolean} isSecure
- * @return {Promise<any>}
  */
-module.exports = async (params, user, _, isSecure) => new Promise(async (res, rej) => {
+module.exports = async (params, user, app) => new Promise(async (res, rej) => {
+	const isSecure = user != null && app == null;
+
 	// Get 'name' parameter
 	const [name, nameErr] = $(params.name).optional.nullable.string().pipe(isValidName).$;
 	if (nameErr) return rej('invalid name param');
diff --git a/src/server/api/endpoints/meta.ts b/src/server/api/endpoints/meta.ts
index 8574362fc8..f6a276a2b7 100644
--- a/src/server/api/endpoints/meta.ts
+++ b/src/server/api/endpoints/meta.ts
@@ -35,9 +35,6 @@ import Meta from '../../../models/meta';
 
 /**
  * Show core info
- *
- * @param {any} params
- * @return {Promise<any>}
  */
 module.exports = (params) => new Promise(async (res, rej) => {
 	const meta: any = (await Meta.findOne()) || {};
diff --git a/src/server/api/endpoints/sw/register.ts b/src/server/api/endpoints/sw/register.ts
index ef3428057d..3fe0bda4ee 100644
--- a/src/server/api/endpoints/sw/register.ts
+++ b/src/server/api/endpoints/sw/register.ts
@@ -6,14 +6,8 @@ import Subscription from '../../../../models/sw-subscription';
 
 /**
  * subscribe service worker
- *
- * @param {any} params
- * @param {any} user
- * @param {any} _
- * @param {boolean} isSecure
- * @return {Promise<any>}
  */
-module.exports = async (params, user, _, isSecure) => new Promise(async (res, rej) => {
+module.exports = async (params, user, app) => new Promise(async (res, rej) => {
 	// Get 'endpoint' parameter
 	const [endpoint, endpointErr] = $(params.endpoint).string().$;
 	if (endpointErr) return rej('invalid endpoint param');
diff --git a/src/server/api/index.ts b/src/server/api/index.ts
index e89d196096..5fbacd8a0e 100644
--- a/src/server/api/index.ts
+++ b/src/server/api/index.ts
@@ -7,7 +7,6 @@ import * as bodyParser from 'body-parser';
 import * as cors from 'cors';
 import * as multer from 'multer';
 
-// import authenticate from './authenticate';
 import endpoints from './endpoints';
 
 /**
diff --git a/src/server/api/limitter.ts b/src/server/api/limitter.ts
index 638fac78be..b84e16ecde 100644
--- a/src/server/api/limitter.ts
+++ b/src/server/api/limitter.ts
@@ -2,12 +2,12 @@ import * as Limiter from 'ratelimiter';
 import * as debug from 'debug';
 import limiterDB from '../../db/redis';
 import { Endpoint } from './endpoints';
-import { IAuthContext } from './authenticate';
 import getAcct from '../../acct/render';
+import { IUser } from '../../models/user';
 
 const log = debug('misskey:limitter');
 
-export default (endpoint: Endpoint, ctx: IAuthContext) => new Promise((ok, reject) => {
+export default (endpoint: Endpoint, user: IUser) => new Promise((ok, reject) => {
 	const limitation = endpoint.limit;
 
 	const key = limitation.hasOwnProperty('key')
@@ -32,7 +32,7 @@ export default (endpoint: Endpoint, ctx: IAuthContext) => new Promise((ok, rejec
 	// Short-term limit
 	function min() {
 		const minIntervalLimiter = new Limiter({
-			id: `${ctx.user._id}:${key}:min`,
+			id: `${user._id}:${key}:min`,
 			duration: limitation.minInterval,
 			max: 1,
 			db: limiterDB
@@ -43,7 +43,7 @@ export default (endpoint: Endpoint, ctx: IAuthContext) => new Promise((ok, rejec
 				return reject('ERR');
 			}
 
-			log(`@${getAcct(ctx.user)} ${endpoint.name} min remaining: ${info.remaining}`);
+			log(`@${getAcct(user)} ${endpoint.name} min remaining: ${info.remaining}`);
 
 			if (info.remaining === 0) {
 				reject('BRIEF_REQUEST_INTERVAL');
@@ -60,7 +60,7 @@ export default (endpoint: Endpoint, ctx: IAuthContext) => new Promise((ok, rejec
 	// Long term limit
 	function max() {
 		const limiter = new Limiter({
-			id: `${ctx.user._id}:${key}`,
+			id: `${user._id}:${key}`,
 			duration: limitation.duration,
 			max: limitation.max,
 			db: limiterDB
@@ -71,7 +71,7 @@ export default (endpoint: Endpoint, ctx: IAuthContext) => new Promise((ok, rejec
 				return reject('ERR');
 			}
 
-			log(`@${getAcct(ctx.user)} ${endpoint.name} max remaining: ${info.remaining}`);
+			log(`@${getAcct(user)} ${endpoint.name} max remaining: ${info.remaining}`);
 
 			if (info.remaining === 0) {
 				reject('RATE_LIMIT_EXCEEDED');
diff --git a/src/server/api/reply.ts b/src/server/api/reply.ts
deleted file mode 100644
index e47fc85b9b..0000000000
--- a/src/server/api/reply.ts
+++ /dev/null
@@ -1,13 +0,0 @@
-import * as express from 'express';
-
-export default (res: express.Response, x?: any, y?: any) => {
-	if (x === undefined) {
-		res.sendStatus(204);
-	} else if (typeof x === 'number') {
-		res.status(x).send({
-			error: x === 500 ? 'INTERNAL_ERROR' : y
-		});
-	} else {
-		res.send(x);
-	}
-};
diff --git a/src/server/api/stream/home.ts b/src/server/api/stream/home.ts
index 359ef74aff..e9c0924f31 100644
--- a/src/server/api/stream/home.ts
+++ b/src/server/api/stream/home.ts
@@ -2,14 +2,22 @@ import * as websocket from 'websocket';
 import * as redis from 'redis';
 import * as debug from 'debug';
 
-import User from '../../../models/user';
+import User, { IUser } from '../../../models/user';
 import Mute from '../../../models/mute';
 import { pack as packNote } from '../../../models/note';
 import readNotification from '../common/read-notification';
+import call from '../call';
+import { IApp } from '../../../models/app';
 
 const log = debug('misskey');
 
-export default async function(request: websocket.request, connection: websocket.connection, subscriber: redis.RedisClient, user: any) {
+export default async function(
+	request: websocket.request,
+	connection: websocket.connection,
+	subscriber: redis.RedisClient,
+	user: IUser,
+	app: IApp
+) {
 	// Subscribe Home stream channel
 	subscriber.subscribe(`misskey:user-stream:${user._id}`);
 
@@ -67,7 +75,17 @@ export default async function(request: websocket.request, connection: websocket.
 
 		switch (msg.type) {
 			case 'api':
-				// TODO
+				call(msg.endpoint, user, app, msg.data).then(res => {
+					connection.send(JSON.stringify({
+						type: `api-res:${msg.id}`,
+						body: { res }
+					}));
+				}).catch(e => {
+					connection.send(JSON.stringify({
+						type: `api-res:${msg.id}`,
+						body: { e }
+					}));
+				});
 				break;
 
 			case 'alive':
diff --git a/src/server/api/streaming.ts b/src/server/api/streaming.ts
index 26946b524e..d586d7c08f 100644
--- a/src/server/api/streaming.ts
+++ b/src/server/api/streaming.ts
@@ -2,9 +2,6 @@ import * as http from 'http';
 import * as websocket from 'websocket';
 import * as redis from 'redis';
 import config from '../../config';
-import { default as User, IUser } from '../../models/user';
-import AccessToken from '../../models/access-token';
-import isNativeToken from './common/is-native-token';
 
 import homeStream from './stream/home';
 import driveStream from './stream/drive';
@@ -16,6 +13,7 @@ import serverStream from './stream/server';
 import requestsStream from './stream/requests';
 import channelStream from './stream/channel';
 import { ParsedUrlQuery } from 'querystring';
+import authenticate from './authenticate';
 
 module.exports = (server: http.Server) => {
 	/**
@@ -53,7 +51,7 @@ module.exports = (server: http.Server) => {
 		}
 
 		const q = request.resourceURL.query as ParsedUrlQuery;
-		const user = await authenticate(q.i as string);
+		const [user, app] = await authenticate(q.i as string);
 
 		if (request.resourceURL.pathname === '/othello-game') {
 			othelloGameStream(request, connection, subscriber, user);
@@ -75,46 +73,9 @@ module.exports = (server: http.Server) => {
 			null;
 
 		if (channel !== null) {
-			channel(request, connection, subscriber, user);
+			channel(request, connection, subscriber, user, app);
 		} else {
 			connection.close();
 		}
 	});
 };
-
-/**
- * 接続してきたユーザーを取得します
- * @param token 送信されてきたトークン
- */
-function authenticate(token: string): Promise<IUser> {
-	if (token == null) {
-		return Promise.resolve(null);
-	}
-
-	return new Promise(async (resolve, reject) => {
-		if (isNativeToken(token)) {
-			// Fetch user
-			const user: IUser = await User
-				.findOne({
-					host: null,
-					'token': token
-				});
-
-			resolve(user);
-		} else {
-			const accessToken = await AccessToken.findOne({
-				hash: token
-			});
-
-			if (accessToken == null) {
-				return reject('invalid signature');
-			}
-
-			// Fetch user
-			const user: IUser = await User
-				.findOne({ _id: accessToken.userId });
-
-			resolve(user);
-		}
-	});
-}