From bd3d57a67f6d7c6a01516410d2322e6ffbd2f5ad Mon Sep 17 00:00:00 2001 From: syuilo 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(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(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} */ -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} */ -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} */ 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} */ -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 { - 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); - } - }); -}