From 90f8fe7e538bb7e52d2558152a0390e693f39b11 Mon Sep 17 00:00:00 2001 From: Akihiko Odaki Date: Thu, 29 Mar 2018 01:20:40 +0900 Subject: Introduce processor --- src/server/api/api-handler.ts | 56 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 src/server/api/api-handler.ts (limited to 'src/server/api/api-handler.ts') diff --git a/src/server/api/api-handler.ts b/src/server/api/api-handler.ts new file mode 100644 index 0000000000..fb603a0e2a --- /dev/null +++ b/src/server/api/api-handler.ts @@ -0,0 +1,56 @@ +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'; + +export default async (endpoint: Endpoint, req: express.Request, res: express.Response) => { + const reply = _reply.bind(null, res); + let ctx: IAuthContext; + + // Authentication + try { + ctx = await authenticate(req); + } 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); + } +}; -- cgit v1.2.3-freya From bd3d57a67f6d7c6a01516410d2322e6ffbd2f5ad Mon Sep 17 00:00:00 2001 From: syuilo Date: Wed, 11 Apr 2018 17:40:01 +0900 Subject: ストリーム経由でAPIにリクエストできるように 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 (limited to 'src/server/api/api-handler.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); - } - }); -} -- cgit v1.2.3-freya From 3368fe855249f45bdf1e4c1e509d325d44e80fbe Mon Sep 17 00:00:00 2001 From: syuilo Date: Fri, 13 Apr 2018 06:06:18 +0900 Subject: wip --- package.json | 10 ++ src/client/app/boot.js | 2 + src/client/assets/404.js | 25 ---- src/index.ts | 6 - src/server/api/api-handler.ts | 17 +-- src/server/api/bot/interfaces/line.ts | 103 +++++++------- src/server/api/call.ts | 7 +- src/server/api/common/signin.ts | 15 ++- src/server/api/index.ts | 60 ++++----- src/server/api/private/signin.ts | 36 ++--- src/server/api/private/signup.ts | 20 +-- src/server/api/service/github.ts | 244 ++++++++++++++++++---------------- src/server/api/service/twitter.ts | 173 ++++++++++++------------ src/server/file/index.ts | 174 +++--------------------- src/server/file/pour.ts | 93 +++++++++++++ src/server/file/send-drive-file.ts | 30 +++++ src/server/index.ts | 6 +- src/server/web/docs.ts | 25 ++-- src/server/web/index.ts | 77 ++++++----- src/server/web/url-preview.ts | 8 +- 20 files changed, 552 insertions(+), 579 deletions(-) delete mode 100644 src/client/assets/404.js create mode 100644 src/server/file/pour.ts create mode 100644 src/server/file/send-drive-file.ts (limited to 'src/server/api/api-handler.ts') diff --git a/package.json b/package.json index e5180fddbd..3e349203fc 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,7 @@ "@fortawesome/fontawesome-free-brands": "5.0.2", "@fortawesome/fontawesome-free-regular": "5.0.2", "@fortawesome/fontawesome-free-solid": "5.0.2", + "@koa/cors": "^2.2.1", "@prezzemolo/rap": "0.1.2", "@prezzemolo/zip": "0.0.3", "@types/bcryptjs": "2.4.1", @@ -58,7 +59,12 @@ "@types/js-yaml": "3.11.1", "@types/koa": "^2.0.45", "@types/koa-bodyparser": "^4.2.0", + "@types/koa-favicon": "^2.0.19", + "@types/koa-mount": "^3.0.1", + "@types/koa-multer": "^1.0.0", "@types/koa-router": "^7.0.27", + "@types/koa-send": "^4.1.1", + "@types/koa__cors": "^2.2.2", "@types/kue": "^0.11.8", "@types/license-checker": "15.0.0", "@types/mkdirp": "0.5.2", @@ -144,7 +150,11 @@ "js-yaml": "3.11.0", "jsdom": "11.7.0", "koa": "^2.5.0", + "koa-favicon": "^2.0.1", + "koa-mount": "^3.0.0", + "koa-multer": "^1.0.2", "koa-router": "^7.4.0", + "koa-send": "^4.1.3", "kue": "0.11.6", "license-checker": "18.0.0", "loader-utils": "1.1.0", diff --git a/src/client/app/boot.js b/src/client/app/boot.js index 0846e4bd55..ef828d9637 100644 --- a/src/client/app/boot.js +++ b/src/client/app/boot.js @@ -97,6 +97,8 @@ // Compare versions if (meta.version != ver) { + localStorage.setItem('v', meta.version); + alert( 'Misskeyの新しいバージョンがあります。ページを再度読み込みします。' + '\n\n' + diff --git a/src/client/assets/404.js b/src/client/assets/404.js deleted file mode 100644 index 9e498fe7c2..0000000000 --- a/src/client/assets/404.js +++ /dev/null @@ -1,25 +0,0 @@ -const yn = window.confirm( - 'サーバー上に存在しないスクリプトがリクエストされました。お使いのMisskeyのバージョンが古いことが原因の可能性があります。Misskeyを更新しますか?\n\nA script that does not exist on the server was requested. It may be caused by an old version of Misskey you’re using. Do you want to delete the cache?'); - -const langYn = window.confirm('また、言語を日本語に設定すると解決する場合があります。日本語に設定しますか?\n\nAlso, setting the language to Japanese may solve the problem. Would you like to set it to Japanese?'); - -if (langYn) { - localStorage.setItem('lang', 'ja'); -} - -if (yn) { - // Clear cache (serive worker) - try { - navigator.serviceWorker.controller.postMessage('clear'); - - navigator.serviceWorker.getRegistrations().then(registrations => { - registrations.forEach(registration => registration.unregister()); - }); - } catch (e) { - console.error(e); - } - - localStorage.removeItem('v'); - - location.reload(true); -} diff --git a/src/index.ts b/src/index.ts index 68b289793b..d633fcbbcb 100644 --- a/src/index.ts +++ b/src/index.ts @@ -10,7 +10,6 @@ import * as debug from 'debug'; import chalk from 'chalk'; // import portUsed = require('tcp-port-used'); import isRoot = require('is-root'); -import { master } from 'accesses'; import Xev from 'xev'; import Logger from './utils/logger'; @@ -73,11 +72,6 @@ async function masterMain(opt) { Logger.info(chalk.green('Successfully initialized :)')); - // Init accesses - if (config.accesses && config.accesses.enable) { - master(); - } - spawnWorkers(() => { if (!opt['only-processor']) { Logger.info(chalk.bold.green( diff --git a/src/server/api/api-handler.ts b/src/server/api/api-handler.ts index 409069b6a0..2c50234317 100644 --- a/src/server/api/api-handler.ts +++ b/src/server/api/api-handler.ts @@ -1,4 +1,4 @@ -import * as express from 'express'; +import * as Koa from 'koa'; import { Endpoint } from './endpoints'; import authenticate from './authenticate'; @@ -6,16 +6,17 @@ 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) => { +export default async (endpoint: Endpoint, ctx: Koa.Context) => { const reply = (x?: any, y?: any) => { if (x === undefined) { - res.sendStatus(204); + ctx.status = 204; } else if (typeof x === 'number') { - res.status(x).send({ + ctx.status = x; + ctx.body = { error: x === 500 ? 'INTERNAL_ERROR' : y - }); + }; } else { - res.send(x); + ctx.body = x; } }; @@ -24,11 +25,11 @@ export default async (endpoint: Endpoint, req: express.Request, res: express.Res // Authentication try { - [user, app] = await authenticate(req.body['i']); + [user, app] = await authenticate(ctx.body['i']); } catch (e) { return reply(403, 'AUTHENTICATION_FAILED'); } // API invoking - call(endpoint, user, app, req.body, req).then(reply).catch(e => reply(400, e)); + call(endpoint, user, app, ctx.body, ctx.req).then(reply).catch(e => reply(400, e)); }; diff --git a/src/server/api/bot/interfaces/line.ts b/src/server/api/bot/interfaces/line.ts index be3bfe33d3..454630161a 100644 --- a/src/server/api/bot/interfaces/line.ts +++ b/src/server/api/bot/interfaces/line.ts @@ -1,5 +1,5 @@ import * as EventEmitter from 'events'; -import * as express from 'express'; +import * as Router from 'koa-router'; import * as request from 'request'; import * as crypto from 'crypto'; import User from '../../../../models/user'; @@ -158,82 +158,81 @@ class LineBot extends BotCore { } } -module.exports = async (app: express.Application) => { - if (config.line_bot == null) return; +const handler = new EventEmitter(); - const handler = new EventEmitter(); +handler.on('event', async (ev) => { - handler.on('event', async (ev) => { + const sourceId = ev.source.userId; + const sessionId = `line-bot-sessions:${sourceId}`; - const sourceId = ev.source.userId; - const sessionId = `line-bot-sessions:${sourceId}`; + const session = await redis.get(sessionId); + let bot: LineBot; - const session = await redis.get(sessionId); - let bot: LineBot; - - if (session == null) { - const user = await User.findOne({ - host: null, - 'line': { - userId: sourceId - } - }); + if (session == null) { + const user = await User.findOne({ + host: null, + 'line': { + userId: sourceId + } + }); - bot = new LineBot(user); + bot = new LineBot(user); - bot.on('signin', user => { - User.update(user._id, { - $set: { - 'line': { - userId: sourceId - } + bot.on('signin', user => { + User.update(user._id, { + $set: { + 'line': { + userId: sourceId } - }); + } }); + }); - bot.on('signout', user => { - User.update(user._id, { - $set: { - 'line': { - userId: null - } + bot.on('signout', user => { + User.update(user._id, { + $set: { + 'line': { + userId: null } - }); + } }); - - redis.set(sessionId, JSON.stringify(bot.export())); - } else { - bot = LineBot.import(JSON.parse(session)); - } - - bot.on('updated', () => { - redis.set(sessionId, JSON.stringify(bot.export())); }); - if (session != null) bot.refreshUser(); + redis.set(sessionId, JSON.stringify(bot.export())); + } else { + bot = LineBot.import(JSON.parse(session)); + } - bot.react(ev); + bot.on('updated', () => { + redis.set(sessionId, JSON.stringify(bot.export())); }); - app.post('/hooks/line', (req, res, next) => { - // req.headers['x-line-signature'] は常に string ですが、型定義の都合上 - // string | string[] になっているので string を明示しています - const sig1 = req.headers['x-line-signature'] as string; + if (session != null) bot.refreshUser(); + + bot.react(ev); +}); + +// Init router +const router = new Router(); + +if (config.line_bot) { + router.post('/hooks/line', ctx => { + const sig1 = ctx.headers['x-line-signature']; const hash = crypto.createHmac('SHA256', config.line_bot.channel_secret) - .update((req as any).rawBody); + .update(ctx.request.rawBody); const sig2 = hash.digest('base64'); // シグネチャ比較 if (sig1 === sig2) { - req.body.events.forEach(ev => { + ctx.body.events.forEach(ev => { handler.emit('event', ev); }); - - res.sendStatus(200); } else { - res.sendStatus(400); + ctx.status = 400; } }); -}; +} + +module.exports = router; diff --git a/src/server/api/call.ts b/src/server/api/call.ts index 1bfe94bb74..c25f55ed3f 100644 --- a/src/server/api/call.ts +++ b/src/server/api/call.ts @@ -1,11 +1,12 @@ -import * as express from 'express'; +import * as http from 'http'; +import * as multer from 'koa-multer'; 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) => { +export default (endpoint: string | Endpoint, user: IUser, app: IApp, data: any, req?: http.IncomingMessage) => new Promise(async (ok, rej) => { const isSecure = user != null && app == null; //console.log(endpoint, user, app, data); @@ -38,7 +39,7 @@ export default (endpoint: string | Endpoint, user: IUser, app: IApp, data: any, let exec = require(`${__dirname}/endpoints/${ep.name}`); if (ep.withFile && req) { - exec = exec.bind(null, req.file); + exec = exec.bind(null, (req as multer.MulterIncomingMessage).file); } let res; diff --git a/src/server/api/common/signin.ts b/src/server/api/common/signin.ts index 8bb327694d..f57c38414c 100644 --- a/src/server/api/common/signin.ts +++ b/src/server/api/common/signin.ts @@ -1,19 +1,20 @@ +import * as Koa from 'koa'; + import config from '../../../config'; +import { ILocalUser } from '../../../models/user'; -export default function(res, user, redirect: boolean) { +export default function(ctx: Koa.Context, user: ILocalUser, redirect: boolean) { const expires = 1000 * 60 * 60 * 24 * 365; // One Year - res.cookie('i', user.token, { + ctx.cookies.set('i', user.token, { path: '/', - domain: `.${config.hostname}`, - secure: config.url.substr(0, 5) === 'https', + domain: config.hostname, + secure: config.url.startsWith('https'), httpOnly: false, expires: new Date(Date.now() + expires), maxAge: expires }); if (redirect) { - res.redirect(config.url); - } else { - res.sendStatus(204); + ctx.redirect(config.url); } } diff --git a/src/server/api/index.ts b/src/server/api/index.ts index 5fbacd8a0e..d2427d30ae 100644 --- a/src/server/api/index.ts +++ b/src/server/api/index.ts @@ -2,53 +2,41 @@ * API Server */ -import * as express from 'express'; -import * as bodyParser from 'body-parser'; -import * as cors from 'cors'; -import * as multer from 'multer'; +import * as Koa from 'koa'; +import * as Router from 'koa-router'; +import * as multer from 'koa-multer'; import endpoints from './endpoints'; -/** - * Init app - */ -const app = express(); - -app.disable('x-powered-by'); -app.set('etag', false); -app.use(bodyParser.urlencoded({ extended: true })); -app.use(bodyParser.json({ - type: ['application/json', 'text/plain'], - verify: (req, res, buf, encoding) => { - if (buf && buf.length) { - (req as any).rawBody = buf.toString(encoding || 'utf8'); - } - } -})); -app.use(cors()); - -app.get('/', (req, res) => { - res.send('YEE HAW'); +const handler = require('./api-handler').default; + +// Init app +const app = new Koa(); + +// Init multer instance +const upload = multer({ + storage: multer.diskStorage({}) }); +// Init router +const router = new Router(); + /** * Register endpoint handlers */ -endpoints.forEach(endpoint => - endpoint.withFile ? - app.post(`/${endpoint.name}`, - endpoint.withFile ? multer({ storage: multer.diskStorage({}) }).single('file') : null, - require('./api-handler').default.bind(null, endpoint)) : - app.post(`/${endpoint.name}`, - require('./api-handler').default.bind(null, endpoint)) +endpoints.forEach(endpoint => endpoint.withFile + ? router.post(`/${endpoint.name}`, upload.single('file'), handler.bind(null, endpoint)) + : router.post(`/${endpoint.name}`, handler.bind(null, endpoint)) ); -app.post('/signup', require('./private/signup').default); -app.post('/signin', require('./private/signin').default); +router.post('/signup', require('./private/signup').default); +router.post('/signin', require('./private/signin').default); -require('./service/github')(app); -require('./service/twitter')(app); +router.use(require('./service/github').routes()); +router.use(require('./service/twitter').routes()); +router.use(require('./bot/interfaces/line').routes()); -require('./bot/interfaces/line')(app); +// Register router +app.use(router.routes()); module.exports = app; diff --git a/src/server/api/private/signin.ts b/src/server/api/private/signin.ts index 665ee21ebd..55326deeaf 100644 --- a/src/server/api/private/signin.ts +++ b/src/server/api/private/signin.ts @@ -1,4 +1,4 @@ -import * as express from 'express'; +import * as Koa from 'koa'; import * as bcrypt from 'bcryptjs'; import * as speakeasy from 'speakeasy'; import User, { ILocalUser } from '../../../models/user'; @@ -7,26 +7,26 @@ import event from '../../../publishers/stream'; import signin from '../common/signin'; import config from '../../../config'; -export default async (req: express.Request, res: express.Response) => { - res.header('Access-Control-Allow-Origin', config.url); - res.header('Access-Control-Allow-Credentials', 'true'); +export default async (ctx: Koa.Context) => { + ctx.set('Access-Control-Allow-Origin', config.url); + ctx.set('Access-Control-Allow-Credentials', 'true'); - const username = req.body['username']; - const password = req.body['password']; - const token = req.body['token']; + const username = ctx.body['username']; + const password = ctx.body['password']; + const token = ctx.body['token']; if (typeof username != 'string') { - res.sendStatus(400); + ctx.status = 400; return; } if (typeof password != 'string') { - res.sendStatus(400); + ctx.status = 400; return; } if (token != null && typeof token != 'string') { - res.sendStatus(400); + ctx.status = 400; return; } @@ -37,12 +37,12 @@ export default async (req: express.Request, res: express.Response) => { }, { fields: { data: false, - 'profile': false + profile: false } }) as ILocalUser; if (user === null) { - res.status(404).send({ + ctx.throw(404, { error: 'user not found' }); return; @@ -60,17 +60,17 @@ export default async (req: express.Request, res: express.Response) => { }); if (verified) { - signin(res, user, false); + signin(ctx, user, false); } else { - res.status(400).send({ + ctx.throw(400, { error: 'invalid token' }); } } else { - signin(res, user, false); + signin(ctx, user, false); } } else { - res.status(400).send({ + ctx.throw(400, { error: 'incorrect password' }); } @@ -79,8 +79,8 @@ export default async (req: express.Request, res: express.Response) => { const record = await Signin.insert({ createdAt: new Date(), userId: user._id, - ip: req.ip, - headers: req.headers, + ip: ctx.ip, + headers: ctx.headers, success: same }); diff --git a/src/server/api/private/signup.ts b/src/server/api/private/signup.ts index f441e1b754..a4554be4ae 100644 --- a/src/server/api/private/signup.ts +++ b/src/server/api/private/signup.ts @@ -1,5 +1,5 @@ import * as uuid from 'uuid'; -import * as express from 'express'; +import * as Koa from 'koa'; import * as bcrypt from 'bcryptjs'; import { generate as generateKeypair } from '../../../crypto_key'; import recaptcha = require('recaptcha-promise'); @@ -33,30 +33,30 @@ const home = { ] }; -export default async (req: express.Request, res: express.Response) => { +export default async (ctx: Koa.Context) => { // Verify recaptcha // ただしテスト時はこの機構は障害となるため無効にする if (process.env.NODE_ENV !== 'test') { - const success = await recaptcha(req.body['g-recaptcha-response']); + const success = await recaptcha(ctx.body['g-recaptcha-response']); if (!success) { - res.status(400).send('recaptcha-failed'); + ctx.throw(400, 'recaptcha-failed'); return; } } - const username = req.body['username']; - const password = req.body['password']; + const username = ctx.body['username']; + const password = ctx.body['password']; // Validate username if (!validateUsername(username)) { - res.sendStatus(400); + ctx.status = 400; return; } // Validate password if (!validatePassword(password)) { - res.sendStatus(400); + ctx.status = 400; return; } @@ -71,7 +71,7 @@ export default async (req: express.Request, res: express.Response) => { // Check username already used if (usernameExist !== 0) { - res.sendStatus(400); + ctx.status = 400; return; } @@ -143,5 +143,5 @@ export default async (req: express.Request, res: express.Response) => { }); // Response - res.send(await pack(account)); + ctx.body = await pack(account); }; diff --git a/src/server/api/service/github.ts b/src/server/api/service/github.ts index bc8d3c6a7d..ee226cc5cc 100644 --- a/src/server/api/service/github.ts +++ b/src/server/api/service/github.ts @@ -1,140 +1,152 @@ import * as EventEmitter from 'events'; -import * as express from 'express'; +import * as Router from 'koa-router'; import * as request from 'request'; const crypto = require('crypto'); -import User from '../../../models/user'; +import User, { IUser } from '../../../models/user'; import createNote from '../../../services/note/create'; import config from '../../../config'; -module.exports = async (app: express.Application) => { - if (config.github_bot == null) return; +const handler = new EventEmitter(); - const bot = await User.findOne({ - usernameLower: config.github_bot.username.toLowerCase() - }); +let bot: IUser; +const post = async text => { if (bot == null) { - console.warn(`GitHub hook bot specified, but not found: @${config.github_bot.username}`); - return; + const account = await User.findOne({ + usernameLower: config.github_bot.username.toLowerCase() + }); + + if (account == null) { + console.warn(`GitHub hook bot specified, but not found: @${config.github_bot.username}`); + return; + } else { + bot = account; + } } - const post = text => createNote(bot, { text }); + createNote(bot, { text }); +}; + +// Init router +const router = new Router(); - const handler = new EventEmitter(); +if (config.github_bot != null) { + const secret = config.github_bot.hook_secret; - app.post('/hooks/github', (req, res, next) => { - // req.headers['x-hub-signature'] および - // req.headers['x-github-event'] は常に string ですが、型定義の都合上 - // string | string[] になっているので string を明示しています - if ((new Buffer(req.headers['x-hub-signature'] as string)).equals(new Buffer(`sha1=${crypto.createHmac('sha1', config.github_bot.hook_secret).update(JSON.stringify(req.body)).digest('hex')}`))) { - handler.emit(req.headers['x-github-event'] as string, req.body); - res.sendStatus(200); + router.post('/hooks/github', ctx => { + const sig1 = new Buffer(ctx.headers['x-hub-signature']); + const sig2 = new Buffer(`sha1=${crypto.createHmac('sha1', secret).update(JSON.stringify(ctx.body)).digest('hex')}`); + if (sig1.equals(sig2)) { + handler.emit(ctx.headers['x-github-event'], ctx.body); + ctx.status = 204; } else { - res.sendStatus(400); + ctx.status = 400; } }); +} - handler.on('status', event => { - const state = event.state; - switch (state) { - case 'error': - case 'failure': - const commit = event.commit; - const parent = commit.parents[0]; - - // Fetch parent status - request({ - url: `${parent.url}/statuses`, - headers: { - 'User-Agent': 'misskey' - } - }, (err, res, body) => { - if (err) { - console.error(err); - return; - } - const parentStatuses = JSON.parse(body); - const parentState = parentStatuses[0].state; - const stillFailed = parentState == 'failure' || parentState == 'error'; - if (stillFailed) { - post(`**⚠️BUILD STILL FAILED⚠️**: ?[${commit.commit.message}](${commit.html_url})`); - } else { - post(`**🚨BUILD FAILED🚨**: →→→?[${commit.commit.message}](${commit.html_url})←←←`); - } - }); - break; - } - }); +module.exports = router; - handler.on('push', event => { - const ref = event.ref; - switch (ref) { - case 'refs/heads/master': - const pusher = event.pusher; - const compare = event.compare; - const commits = event.commits; - post([ - `Pushed by **${pusher.name}** with ?[${commits.length} commit${commits.length > 1 ? 's' : ''}](${compare}):`, - commits.reverse().map(commit => `・[?[${commit.id.substr(0, 7)}](${commit.url})] ${commit.message.split('\n')[0]}`).join('\n'), - ].join('\n')); - break; - case 'refs/heads/release': - const commit = event.commits[0]; - post(`RELEASED: ${commit.message}`); - break; - } - }); +handler.on('status', event => { + const state = event.state; + switch (state) { + case 'error': + case 'failure': + const commit = event.commit; + const parent = commit.parents[0]; - handler.on('issues', event => { - const issue = event.issue; - const action = event.action; - let title: string; - switch (action) { - case 'opened': title = 'Issue opened'; break; - case 'closed': title = 'Issue closed'; break; - case 'reopened': title = 'Issue reopened'; break; - default: return; - } - post(`${title}: <${issue.number}>「${issue.title}」\n${issue.html_url}`); - }); + // Fetch parent status + request({ + url: `${parent.url}/statuses`, + headers: { + 'User-Agent': 'misskey' + } + }, (err, res, body) => { + if (err) { + console.error(err); + return; + } + const parentStatuses = JSON.parse(body); + const parentState = parentStatuses[0].state; + const stillFailed = parentState == 'failure' || parentState == 'error'; + if (stillFailed) { + post(`**⚠️BUILD STILL FAILED⚠️**: ?[${commit.commit.message}](${commit.html_url})`); + } else { + post(`**🚨BUILD FAILED🚨**: →→→?[${commit.commit.message}](${commit.html_url})←←←`); + } + }); + break; + } +}); - handler.on('issue_comment', event => { - const issue = event.issue; - const comment = event.comment; - const action = event.action; - let text: string; - switch (action) { - case 'created': text = `Commented to「${issue.title}」:${comment.user.login}「${comment.body}」\n${comment.html_url}`; break; - default: return; - } - post(text); - }); +handler.on('push', event => { + const ref = event.ref; + switch (ref) { + case 'refs/heads/master': + const pusher = event.pusher; + const compare = event.compare; + const commits = event.commits; + post([ + `Pushed by **${pusher.name}** with ?[${commits.length} commit${commits.length > 1 ? 's' : ''}](${compare}):`, + commits.reverse().map(commit => `・[?[${commit.id.substr(0, 7)}](${commit.url})] ${commit.message.split('\n')[0]}`).join('\n'), + ].join('\n')); + break; + case 'refs/heads/release': + const commit = event.commits[0]; + post(`RELEASED: ${commit.message}`); + break; + } +}); - handler.on('watch', event => { - const sender = event.sender; - post(`⭐️ Starred by **${sender.login}** ⭐️`); - }); +handler.on('issues', event => { + const issue = event.issue; + const action = event.action; + let title: string; + switch (action) { + case 'opened': title = 'Issue opened'; break; + case 'closed': title = 'Issue closed'; break; + case 'reopened': title = 'Issue reopened'; break; + default: return; + } + post(`${title}: <${issue.number}>「${issue.title}」\n${issue.html_url}`); +}); - handler.on('fork', event => { - const repo = event.forkee; - post(`🍴 Forked:\n${repo.html_url} 🍴`); - }); +handler.on('issue_comment', event => { + const issue = event.issue; + const comment = event.comment; + const action = event.action; + let text: string; + switch (action) { + case 'created': text = `Commented to「${issue.title}」:${comment.user.login}「${comment.body}」\n${comment.html_url}`; break; + default: return; + } + post(text); +}); - handler.on('pull_request', event => { - const pr = event.pull_request; - const action = event.action; - let text: string; - switch (action) { - case 'opened': text = `New Pull Request:「${pr.title}」\n${pr.html_url}`; break; - case 'reopened': text = `Pull Request Reopened:「${pr.title}」\n${pr.html_url}`; break; - case 'closed': - text = pr.merged - ? `Pull Request Merged!:「${pr.title}」\n${pr.html_url}` - : `Pull Request Closed:「${pr.title}」\n${pr.html_url}`; - break; - default: return; - } - post(text); - }); -}; +handler.on('watch', event => { + const sender = event.sender; + post(`⭐️ Starred by **${sender.login}** ⭐️`); +}); + +handler.on('fork', event => { + const repo = event.forkee; + post(`🍴 Forked:\n${repo.html_url} 🍴`); +}); + +handler.on('pull_request', event => { + const pr = event.pull_request; + const action = event.action; + let text: string; + switch (action) { + case 'opened': text = `New Pull Request:「${pr.title}」\n${pr.html_url}`; break; + case 'reopened': text = `Pull Request Reopened:「${pr.title}」\n${pr.html_url}`; break; + case 'closed': + text = pr.merged + ? `Pull Request Merged!:「${pr.title}」\n${pr.html_url}` + : `Pull Request Closed:「${pr.title}」\n${pr.html_url}`; + break; + default: return; + } + post(text); +}); diff --git a/src/server/api/service/twitter.ts b/src/server/api/service/twitter.ts index e5239fa171..9fb01b44ef 100644 --- a/src/server/api/service/twitter.ts +++ b/src/server/api/service/twitter.ts @@ -1,160 +1,155 @@ -import * as express from 'express'; -import * as cookie from 'cookie'; +import * as Koa from 'koa'; +import * as Router from 'koa-router'; import * as uuid from 'uuid'; -// import * as Twitter from 'twitter'; -// const Twitter = require('twitter'); import autwh from 'autwh'; import redis from '../../../db/redis'; -import User, { pack } from '../../../models/user'; +import User, { pack, ILocalUser } from '../../../models/user'; import event from '../../../publishers/stream'; import config from '../../../config'; import signin from '../common/signin'; -module.exports = (app: express.Application) => { - function getUserToken(req: express.Request) { - // req.headers['cookie'] は常に string ですが、型定義の都合上 - // string | string[] になっているので string を明示しています - return ((req.headers['cookie'] as string || '').match(/i=(!\w+)/) || [null, null])[1]; - } - - function compareOrigin(req: express.Request) { - function normalizeUrl(url: string) { - return url[url.length - 1] === '/' ? url.substr(0, url.length - 1) : url; - } - - // req.headers['referer'] は常に string ですが、型定義の都合上 - // string | string[] になっているので string を明示しています - const referer = req.headers['referer'] as string; +function getUserToken(ctx: Koa.Context) { + return ((ctx.headers['cookie'] || '').match(/i=(!\w+)/) || [null, null])[1]; +} - return (normalizeUrl(referer) == normalizeUrl(config.url)); +function compareOrigin(ctx: Koa.Context) { + function normalizeUrl(url: string) { + return url[url.length - 1] === '/' ? url.substr(0, url.length - 1) : url; } - app.get('/disconnect/twitter', async (req, res): Promise => { - if (!compareOrigin(req)) { - res.status(400).send('invalid origin'); - return; - } + const referer = ctx.headers['referer']; - const userToken = getUserToken(req); - if (userToken == null) return res.send('plz signin'); + return (normalizeUrl(referer) == normalizeUrl(config.url)); +} - const user = await User.findOneAndUpdate({ - host: null, - 'token': userToken - }, { - $set: { - 'twitter': null - } - }); +// Init router +const router = new Router(); + +router.get('/disconnect/twitter', async ctx => { + if (!compareOrigin(ctx)) { + ctx.throw(400, 'invalid origin'); + return; + } - res.send(`Twitterの連携を解除しました :v:`); + const userToken = getUserToken(ctx); + if (userToken == null) { + ctx.throw(400, 'signin required'); + return; + } - // Publish i updated event - event(user._id, 'i_updated', await pack(user, user, { - detail: true, - includeSecrets: true - })); + const user = await User.findOneAndUpdate({ + host: null, + 'token': userToken + }, { + $set: { + 'twitter': null + } }); - if (config.twitter == null) { - app.get('/connect/twitter', (req, res) => { - res.send('現在Twitterへ接続できません (このインスタンスではTwitterはサポートされていません)'); - }); + ctx.body = `Twitterの連携を解除しました :v:`; - app.get('/signin/twitter', (req, res) => { - res.send('現在Twitterへ接続できません (このインスタンスではTwitterはサポートされていません)'); - }); + // Publish i updated event + event(user._id, 'i_updated', await pack(user, user, { + detail: true, + includeSecrets: true + })); +}); - return; - } +if (config.twitter == null) { + router.get('/connect/twitter', ctx => { + ctx.body = '現在Twitterへ接続できません (このインスタンスではTwitterはサポートされていません)'; + }); + router.get('/signin/twitter', ctx => { + ctx.body = '現在Twitterへ接続できません (このインスタンスではTwitterはサポートされていません)'; + }); +} else { const twAuth = autwh({ consumerKey: config.twitter.consumer_key, consumerSecret: config.twitter.consumer_secret, callbackUrl: `${config.url}/api/tw/cb` }); - app.get('/connect/twitter', async (req, res): Promise => { - if (!compareOrigin(req)) { - res.status(400).send('invalid origin'); + router.get('/connect/twitter', async ctx => { + if (!compareOrigin(ctx)) { + ctx.throw(400, 'invalid origin'); return; } - const userToken = getUserToken(req); - if (userToken == null) return res.send('plz signin'); + const userToken = getUserToken(ctx); + if (userToken == null) { + ctx.throw(400, 'signin required'); + return; + } - const ctx = await twAuth.begin(); - redis.set(userToken, JSON.stringify(ctx)); - res.redirect(ctx.url); + const twCtx = await twAuth.begin(); + redis.set(userToken, JSON.stringify(twCtx)); + ctx.redirect(twCtx.url); }); - app.get('/signin/twitter', async (req, res): Promise => { - const ctx = await twAuth.begin(); + router.get('/signin/twitter', async ctx => { + const twCtx = await twAuth.begin(); const sessid = uuid(); - redis.set(sessid, JSON.stringify(ctx)); + redis.set(sessid, JSON.stringify(twCtx)); const expires = 1000 * 60 * 60; // 1h - res.cookie('signin_with_twitter_session_id', sessid, { + ctx.cookies.set('signin_with_twitter_session_id', sessid, { path: '/', - domain: `.${config.host}`, - secure: config.url.substr(0, 5) === 'https', + domain: config.host, + secure: config.url.startsWith('https'), httpOnly: true, expires: new Date(Date.now() + expires), maxAge: expires }); - res.redirect(ctx.url); + ctx.redirect(twCtx.url); }); - app.get('/tw/cb', (req, res): any => { - const userToken = getUserToken(req); + router.get('/tw/cb', ctx => { + const userToken = getUserToken(ctx); if (userToken == null) { - // req.headers['cookie'] は常に string ですが、型定義の都合上 - // string | string[] になっているので string を明示しています - const cookies = cookie.parse((req.headers['cookie'] as string || '')); + const sessid = ctx.cookies.get('signin_with_twitter_session_id'); - const sessid = cookies['signin_with_twitter_session_id']; - - if (sessid == undefined) { - res.status(400).send('invalid session'); + if (sessid == null) { + ctx.throw(400, 'invalid session'); return; } - redis.get(sessid, async (_, ctx) => { - const result = await twAuth.done(JSON.parse(ctx), req.query.oauth_verifier); + redis.get(sessid, async (_, twCtx) => { + const result = await twAuth.done(JSON.parse(twCtx), ctx.query.oauth_verifier); const user = await User.findOne({ host: null, 'twitter.userId': result.userId - }); + }) as ILocalUser; if (user == null) { - res.status(404).send(`@${result.screenName}と連携しているMisskeyアカウントはありませんでした...`); + ctx.throw(404, `@${result.screenName}と連携しているMisskeyアカウントはありませんでした...`); return; } - signin(res, user, true); + signin(ctx, user, true); }); } else { - const verifier = req.query.oauth_verifier; + const verifier = ctx.query.oauth_verifier; if (verifier == null) { - res.status(400).send('invalid session'); + ctx.throw(400, 'invalid session'); return; } - redis.get(userToken, async (_, ctx) => { - const result = await twAuth.done(JSON.parse(ctx), verifier); + redis.get(userToken, async (_, twCtx) => { + const result = await twAuth.done(JSON.parse(twCtx), verifier); const user = await User.findOneAndUpdate({ host: null, - 'token': userToken + token: userToken }, { $set: { - 'twitter': { + twitter: { accessToken: result.accessToken, accessTokenSecret: result.accessTokenSecret, userId: result.userId, @@ -163,7 +158,7 @@ module.exports = (app: express.Application) => { } }); - res.send(`Twitter: @${result.screenName} を、Misskey: @${user.username} に接続しました!`); + ctx.body = `Twitter: @${result.screenName} を、Misskey: @${user.username} に接続しました!`; // Publish i updated event event(user._id, 'i_updated', await pack(user, user, { @@ -173,4 +168,6 @@ module.exports = (app: express.Application) => { }); } }); -}; +} + +module.exports = router; diff --git a/src/server/file/index.ts b/src/server/file/index.ts index 95e7867f01..d58939f1be 100644 --- a/src/server/file/index.ts +++ b/src/server/file/index.ts @@ -3,171 +3,33 @@ */ import * as fs from 'fs'; -import * as express from 'express'; -import * as bodyParser from 'body-parser'; -import * as cors from 'cors'; -import * as mongodb from 'mongodb'; -import * as _gm from 'gm'; -import * as stream from 'stream'; - -import DriveFile, { getGridFSBucket } from '../../models/drive-file'; - -const gm = _gm.subClass({ - imageMagick: true -}); - -/** - * Init app - */ -const app = express(); - -app.disable('x-powered-by'); -app.locals.cache = true; -app.use(bodyParser.urlencoded({ extended: true })); +import * as Koa from 'koa'; +import * as cors from '@koa/cors'; +import * as Router from 'koa-router'; +import pour from './pour'; +import sendDriveFile from './send-drive-file'; + +// Init app +const app = new Koa(); app.use(cors()); -/** - * Statics - */ -app.use('/assets', express.static(`${__dirname}/assets`, { - maxAge: 1000 * 60 * 60 * 24 * 365 // 一年 -})); - -app.get('/', (req, res) => { - res.send('yee haw'); -}); +// Init router +const router = new Router(); -app.get('/default-avatar.jpg', (req, res) => { +router.get('/default-avatar.jpg', ctx => { const file = fs.createReadStream(`${__dirname}/assets/avatar.jpg`); - send(file, 'image/jpeg', req, res); + pour(file, 'image/jpeg', ctx); }); -app.get('/app-default.jpg', (req, res) => { +router.get('/app-default.jpg', ctx => { const file = fs.createReadStream(`${__dirname}/assets/dummy.png`); - send(file, 'image/png', req, res); + pour(file, 'image/png', ctx); }); -interface ISend { - contentType: string; - stream: stream.Readable; -} - -function thumbnail(data: stream.Readable, type: string, resize: number): ISend { - const readable: stream.Readable = (() => { - // 動画であれば - if (/^video\/.*$/.test(type)) { - // TODO - // 使わないことになったストリームはしっかり取り壊す - data.destroy(); - return fs.createReadStream(`${__dirname}/assets/thumbnail-not-available.png`); - // 画像であれば - // Note: SVGはapplication/xml - } else if (/^image\/.*$/.test(type) || type == 'application/xml') { - // 0フレーム目を送る - try { - return gm(data).selectFrame(0).stream(); - // だめだったら - } catch (e) { - // 使わないことになったストリームはしっかり取り壊す - data.destroy(); - return fs.createReadStream(`${__dirname}/assets/thumbnail-not-available.png`); - } - // 動画か画像以外 - } else { - data.destroy(); - return fs.createReadStream(`${__dirname}/assets/not-an-image.png`); - } - })(); - - let g = gm(readable); - - if (resize) { - g = g.resize(resize, resize); - } - - const stream = g - .compress('jpeg') - .quality(80) - .interlace('line') - .stream(); - - return { - contentType: 'image/jpeg', - stream - }; -} - -const commonReadableHandlerGenerator = (req: express.Request, res: express.Response) => (e: Error): void => { - console.dir(e); - req.destroy(); - res.destroy(e); -}; - -function send(readable: stream.Readable, type: string, req: express.Request, res: express.Response): void { - readable.on('error', commonReadableHandlerGenerator(req, res)); - - const data = ((): ISend => { - if (req.query.thumbnail !== undefined) { - return thumbnail(readable, type, req.query.size); - } - return { - contentType: type, - stream: readable - }; - })(); - - if (readable !== data.stream) { - data.stream.on('error', commonReadableHandlerGenerator(req, res)); - } - - if (req.query.download !== undefined) { - res.header('Content-Disposition', 'attachment'); - } - - res.header('Content-Type', data.contentType); - - data.stream.pipe(res); - - data.stream.on('end', () => { - res.end(); - }); -} - -async function sendFileById(req: express.Request, res: express.Response): Promise { - // Validate id - if (!mongodb.ObjectID.isValid(req.params.id)) { - res.status(400).send('incorrect id'); - return; - } - - const fileId = new mongodb.ObjectID(req.params.id); - - // Fetch (drive) file - const file = await DriveFile.findOne({ _id: fileId }); - - // validate name - if (req.params.name !== undefined && req.params.name !== file.filename) { - res.status(404).send('there is no file has given name'); - return; - } - - if (file == null) { - res.status(404).sendFile(`${__dirname}/assets/dummy.png`); - return; - } - - const bucket = await getGridFSBucket(); - - const readable = bucket.openDownloadStream(fileId); - - send(readable, file.contentType, req, res); -} - -/** - * Routing - */ +router.get('/:id', sendDriveFile); +router.get('/:id/:name', sendDriveFile); -app.get('/:id', sendFileById); -app.get('/:id/:name', sendFileById); +// Register router +app.use(router.routes()); module.exports = app; diff --git a/src/server/file/pour.ts b/src/server/file/pour.ts new file mode 100644 index 0000000000..2a31cb5898 --- /dev/null +++ b/src/server/file/pour.ts @@ -0,0 +1,93 @@ +import * as fs from 'fs'; +import * as stream from 'stream'; +import * as Koa from 'koa'; +import * as Gm from 'gm'; + +const gm = Gm.subClass({ + imageMagick: true +}); + +interface ISend { + contentType: string; + stream: stream.Readable; +} + +function thumbnail(data: stream.Readable, type: string, resize: number): ISend { + const readable: stream.Readable = (() => { + // 動画であれば + if (/^video\/.*$/.test(type)) { + // TODO + // 使わないことになったストリームはしっかり取り壊す + data.destroy(); + return fs.createReadStream(`${__dirname}/assets/thumbnail-not-available.png`); + // 画像であれば + // Note: SVGはapplication/xml + } else if (/^image\/.*$/.test(type) || type == 'application/xml') { + // 0フレーム目を送る + try { + return gm(data).selectFrame(0).stream(); + // だめだったら + } catch (e) { + // 使わないことになったストリームはしっかり取り壊す + data.destroy(); + return fs.createReadStream(`${__dirname}/assets/thumbnail-not-available.png`); + } + // 動画か画像以外 + } else { + data.destroy(); + return fs.createReadStream(`${__dirname}/assets/not-an-image.png`); + } + })(); + + let g = gm(readable); + + if (resize) { + g = g.resize(resize, resize); + } + + const stream = g + .compress('jpeg') + .quality(80) + .interlace('line') + .stream(); + + return { + contentType: 'image/jpeg', + stream + }; +} + +const commonReadableHandlerGenerator = (ctx: Koa.Context) => (e: Error): void => { + console.error(e); + ctx.status = 500; +}; + +export default function(readable: stream.Readable, type: string, ctx: Koa.Context): void { + readable.on('error', commonReadableHandlerGenerator(ctx)); + + const data = ((): ISend => { + if (ctx.query.thumbnail !== undefined) { + return thumbnail(readable, type, ctx.query.size); + } + return { + contentType: type, + stream: readable + }; + })(); + + if (readable !== data.stream) { + data.stream.on('error', commonReadableHandlerGenerator(ctx)); + } + + if (ctx.query.download !== undefined) { + ctx.header('Content-Disposition', 'attachment'); + } + + ctx.header('Content-Type', data.contentType); + + data.stream.pipe(ctx.res); + + data.stream.on('end', () => { + ctx.res.end(); + }); +} diff --git a/src/server/file/send-drive-file.ts b/src/server/file/send-drive-file.ts new file mode 100644 index 0000000000..e6ee19ff1d --- /dev/null +++ b/src/server/file/send-drive-file.ts @@ -0,0 +1,30 @@ +import * as Koa from 'koa'; +import * as send from 'koa-send'; +import * as mongodb from 'mongodb'; +import DriveFile, { getGridFSBucket } from '../../models/drive-file'; +import pour from './pour'; + +export default async function(ctx: Koa.Context) { + // Validate id + if (!mongodb.ObjectID.isValid(ctx.params.id)) { + ctx.throw(400, 'incorrect id'); + return; + } + + const fileId = new mongodb.ObjectID(ctx.params.id); + + // Fetch drive file + const file = await DriveFile.findOne({ _id: fileId }); + + if (file == null) { + ctx.status = 404; + await send(ctx, `${__dirname}/assets/dummy.png`); + return; + } + + const bucket = await getGridFSBucket(); + + const readable = bucket.openDownloadStream(fileId); + + pour(readable, file.contentType, ctx); +} diff --git a/src/server/index.ts b/src/server/index.ts index e9bfa9e10b..f0d60fa651 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -13,7 +13,7 @@ import activityPub from './activitypub'; import webFinger from './webfinger'; import config from '../config'; -// Init server +// Init app const app = new Koa(); app.proxy = true; app.use(bodyParser); @@ -46,9 +46,9 @@ function createServer() { Object.keys(config.https).forEach(k => { certs[k] = fs.readFileSync(config.https[k]); }); - return https.createServer(certs, app); + return https.createServer(certs, app.callback); } else { - return http.createServer(app); + return http.createServer(app.callback); } } diff --git a/src/server/web/docs.ts b/src/server/web/docs.ts index 889532e17e..a546d1e88c 100644 --- a/src/server/web/docs.ts +++ b/src/server/web/docs.ts @@ -1,24 +1,21 @@ /** - * Docs Server + * Docs */ import * as path from 'path'; -import * as express from 'express'; +import * as Router from 'koa-router'; +import * as send from 'koa-send'; const docs = path.resolve(`${__dirname}/../../client/docs/`); -/** - * Init app - */ -const app = express(); -app.disable('x-powered-by'); +const router = new Router(); -app.use('/assets', express.static(`${docs}/assets`)); +router.get('/assets', async ctx => { + await send(ctx, `${docs}/assets`); +}); -/** - * Routing - */ -app.get(/^\/([a-z_\-\/]+?)$/, (req, res) => - res.sendFile(`${docs}/${req.params[0]}.html`)); +router.get(/^\/([a-z_\-\/]+?)$/, async ctx => { + await send(ctx, `${docs}/${ctx.params[0]}.html`); +}); -module.exports = app; +module.exports = router; diff --git a/src/server/web/index.ts b/src/server/web/index.ts index 5b1b6409b9..b28ad5592c 100644 --- a/src/server/web/index.ts +++ b/src/server/web/index.ts @@ -5,60 +5,71 @@ import * as path from 'path'; import ms = require('ms'); -// express modules -import * as express from 'express'; -import * as bodyParser from 'body-parser'; -import * as favicon from 'serve-favicon'; -import * as compression from 'compression'; +import * as Koa from 'koa'; +import * as Router from 'koa-router'; +import * as send from 'koa-send'; +import * as favicon from 'koa-favicon'; const client = path.resolve(`${__dirname}/../../client/`); -// Create server -const app = express(); -app.disable('x-powered-by'); +// Init app +const app = new Koa(); -app.use('/docs', require('./docs')); - -app.use(bodyParser.urlencoded({ extended: true })); -app.use(bodyParser.json({ - type: ['application/json', 'text/plain'] -})); -app.use(compression()); +// Serve favicon +app.use(favicon(`${client}/assets/favicon.ico`)); -app.use((req, res, next) => { - res.header('X-Frame-Options', 'DENY'); +// Common request handler +app.use((ctx, next) => { + // IFrameの中に入れられないようにする + ctx.set('X-Frame-Options', 'DENY'); next(); }); +// Init router +const router = new Router(); + //#region static assets -app.use(favicon(`${client}/assets/favicon.ico`)); -app.get('/apple-touch-icon.png', (req, res) => res.sendFile(`${client}/assets/apple-touch-icon.png`)); -app.use('/assets', express.static(`${client}/assets`, { - maxAge: ms('7 days') -})); -app.use('/assets/*.js', (req, res) => res.sendFile(`${client}/assets/404.js`)); -app.use('/assets', (req, res) => { - res.sendStatus(404); +router.get('/assets', async ctx => { + await send(ctx, ctx.path, { + root: `${client}/assets`, + maxage: ms('7 days'), + immutable: true + }); +}); + +// Apple touch icon +router.get('/apple-touch-icon.png', async ctx => { + await send(ctx, `${client}/assets/apple-touch-icon.png`); }); // ServiceWroker -app.get(/^\/sw\.(.+?)\.js$/, (req, res) => - res.sendFile(`${client}/assets/sw.${req.params[0]}.js`)); +router.get(/^\/sw\.(.+?)\.js$/, async ctx => { + await send(ctx, `${client}/assets/sw.${ctx.params[0]}.js`); +}); // Manifest -app.get('/manifest.json', (req, res) => - res.sendFile(`${client}/assets/manifest.json`)); +router.get('/manifest.json', async ctx => { + await send(ctx, `${client}/assets/manifest.json`); +}); //#endregion -app.get(/\/api:url/, require('./url-preview')); +// Docs +router.use('/docs', require('./docs').routes()); + +// URL preview endpoint +router.get('url', require('./url-preview')); // Render base html for all requests -app.get('*', (req, res) => { - res.sendFile(path.resolve(`${client}/app/base.html`), { - maxAge: ms('7 days') +router.get('*', async ctx => { + await send(ctx, `${client}/app/base.html`, { + maxage: ms('7 days'), + immutable: true }); }); +// Register router +app.use(router.routes()); + module.exports = app; diff --git a/src/server/web/url-preview.ts b/src/server/web/url-preview.ts index 0c5fd8a78e..4b3f44a5da 100644 --- a/src/server/web/url-preview.ts +++ b/src/server/web/url-preview.ts @@ -1,11 +1,11 @@ -import * as express from 'express'; +import * as Koa from 'koa'; import summaly from 'summaly'; -module.exports = async (req: express.Request, res: express.Response) => { - const summary = await summaly(req.query.url); +module.exports = async (ctx: Koa.Context) => { + const summary = await summaly(ctx.query.url); summary.icon = wrap(summary.icon); summary.thumbnail = wrap(summary.thumbnail); - res.send(summary); + ctx.body = summary; }; function wrap(url: string): string { -- cgit v1.2.3-freya From 22d2f2051c4cbe3da5b9ece674f36a6555f8c953 Mon Sep 17 00:00:00 2001 From: syuilo Date: Fri, 13 Apr 2018 09:44:00 +0900 Subject: wip --- src/client/app/common/mios.ts | 5 +++-- src/server/api/api-handler.ts | 16 +++++++++++++--- src/server/api/bot/interfaces/line.ts | 2 +- src/server/api/call.ts | 4 +--- src/server/api/index.ts | 4 +++- src/server/api/private/signin.ts | 6 +++--- src/server/api/private/signup.ts | 6 +++--- src/server/api/service/github.ts | 8 ++++++-- src/server/file/index.ts | 7 ++++++- src/server/file/pour.ts | 8 +------- 10 files changed, 40 insertions(+), 26 deletions(-) (limited to 'src/server/api/api-handler.ts') diff --git a/src/client/app/common/mios.ts b/src/client/app/common/mios.ts index a09af799be..ccc73eebc3 100644 --- a/src/client/app/common/mios.ts +++ b/src/client/app/common/mios.ts @@ -444,9 +444,10 @@ export default class MiOS extends EventEmitter { // Append a credential if (this.isSignedIn) (data as any).i = this.i.token; - const viaStream = localStorage.getItem('apiViaStream') ? localStorage.getItem('apiViaStream') == 'true' : true; - return new Promise((resolve, reject) => { + const viaStream = this.stream.hasConnection && + (localStorage.getItem('apiViaStream') ? localStorage.getItem('apiViaStream') == 'true' : true); + if (viaStream) { const stream = this.stream.borrow(); const id = Math.random().toString(); diff --git a/src/server/api/api-handler.ts b/src/server/api/api-handler.ts index 2c50234317..947794a20e 100644 --- a/src/server/api/api-handler.ts +++ b/src/server/api/api-handler.ts @@ -25,11 +25,21 @@ export default async (endpoint: Endpoint, ctx: Koa.Context) => { // Authentication try { - [user, app] = await authenticate(ctx.body['i']); + [user, app] = await authenticate(ctx.request.body['i']); } catch (e) { - return reply(403, 'AUTHENTICATION_FAILED'); + reply(403, 'AUTHENTICATION_FAILED'); + return; } + let res; + // API invoking - call(endpoint, user, app, ctx.body, ctx.req).then(reply).catch(e => reply(400, e)); + try { + res = await call(endpoint, user, app, ctx.request.body, ctx.req); + } catch (e) { + reply(400, e); + return; + } + + reply(res); }; diff --git a/src/server/api/bot/interfaces/line.ts b/src/server/api/bot/interfaces/line.ts index 454630161a..733315391d 100644 --- a/src/server/api/bot/interfaces/line.ts +++ b/src/server/api/bot/interfaces/line.ts @@ -226,7 +226,7 @@ if (config.line_bot) { // シグネチャ比較 if (sig1 === sig2) { - ctx.body.events.forEach(ev => { + ctx.request.body.events.forEach(ev => { handler.emit('event', ev); }); } else { diff --git a/src/server/api/call.ts b/src/server/api/call.ts index c25f55ed3f..cc40294657 100644 --- a/src/server/api/call.ts +++ b/src/server/api/call.ts @@ -6,11 +6,9 @@ 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?: http.IncomingMessage) => new Promise(async (ok, rej) => { +export default (endpoint: string | Endpoint, user: IUser, app: IApp, data: any, req?: http.IncomingMessage) => 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) { diff --git a/src/server/api/index.ts b/src/server/api/index.ts index c383e1cf8d..2ea5fccb5b 100644 --- a/src/server/api/index.ts +++ b/src/server/api/index.ts @@ -13,7 +13,9 @@ const handler = require('./api-handler').default; // Init app const app = new Koa(); -app.use(bodyParser); +app.use(bodyParser({ + detectJSON: () => true +})); // Init multer instance const upload = multer({ diff --git a/src/server/api/private/signin.ts b/src/server/api/private/signin.ts index 55326deeaf..1737007206 100644 --- a/src/server/api/private/signin.ts +++ b/src/server/api/private/signin.ts @@ -11,9 +11,9 @@ export default async (ctx: Koa.Context) => { ctx.set('Access-Control-Allow-Origin', config.url); ctx.set('Access-Control-Allow-Credentials', 'true'); - const username = ctx.body['username']; - const password = ctx.body['password']; - const token = ctx.body['token']; + const username = ctx.request.body['username']; + const password = ctx.request.body['password']; + const token = ctx.request.body['token']; if (typeof username != 'string') { ctx.status = 400; diff --git a/src/server/api/private/signup.ts b/src/server/api/private/signup.ts index a4554be4ae..15257b869f 100644 --- a/src/server/api/private/signup.ts +++ b/src/server/api/private/signup.ts @@ -37,7 +37,7 @@ export default async (ctx: Koa.Context) => { // Verify recaptcha // ただしテスト時はこの機構は障害となるため無効にする if (process.env.NODE_ENV !== 'test') { - const success = await recaptcha(ctx.body['g-recaptcha-response']); + const success = await recaptcha(ctx.request.body['g-recaptcha-response']); if (!success) { ctx.throw(400, 'recaptcha-failed'); @@ -45,8 +45,8 @@ export default async (ctx: Koa.Context) => { } } - const username = ctx.body['username']; - const password = ctx.body['password']; + const username = ctx.request.body['username']; + const password = ctx.request.body['password']; // Validate username if (!validateUsername(username)) { diff --git a/src/server/api/service/github.ts b/src/server/api/service/github.ts index ee226cc5cc..cd9760a36d 100644 --- a/src/server/api/service/github.ts +++ b/src/server/api/service/github.ts @@ -35,10 +35,14 @@ if (config.github_bot != null) { const secret = config.github_bot.hook_secret; router.post('/hooks/github', ctx => { + const body = JSON.stringify(ctx.request.body); + const hash = crypto.createHmac('sha1', secret).update(body).digest('hex'); const sig1 = new Buffer(ctx.headers['x-hub-signature']); - const sig2 = new Buffer(`sha1=${crypto.createHmac('sha1', secret).update(JSON.stringify(ctx.body)).digest('hex')}`); + const sig2 = new Buffer(`sha1=${hash}`); + + // シグネチャ比較 if (sig1.equals(sig2)) { - handler.emit(ctx.headers['x-github-event'], ctx.body); + handler.emit(ctx.headers['x-github-event'], ctx.request.body); ctx.status = 204; } else { ctx.status = 400; diff --git a/src/server/file/index.ts b/src/server/file/index.ts index d58939f1be..d305286d12 100644 --- a/src/server/file/index.ts +++ b/src/server/file/index.ts @@ -13,6 +13,11 @@ import sendDriveFile from './send-drive-file'; const app = new Koa(); app.use(cors()); +app.use(async (ctx, next) => { + ctx.set('Cache-Control', 'max-age=31536000, immutable'); + await next(); +}); + // Init router const router = new Router(); @@ -27,7 +32,7 @@ router.get('/app-default.jpg', ctx => { }); router.get('/:id', sendDriveFile); -router.get('/:id/:name', sendDriveFile); +router.get('/:id/*', sendDriveFile); // Register router app.use(router.routes()); diff --git a/src/server/file/pour.ts b/src/server/file/pour.ts index b38b969c2d..0fd0ad0e60 100644 --- a/src/server/file/pour.ts +++ b/src/server/file/pour.ts @@ -83,12 +83,6 @@ export default function(readable: stream.Readable, type: string, ctx: Koa.Contex ctx.set('Content-Disposition', 'attachment'); } - ctx.set('Cache-Control', 'max-age=31536000, immutable'); ctx.set('Content-Type', data.contentType); - - data.stream.pipe(ctx.res); - - data.stream.on('end', () => { - ctx.res.end(); - }); + ctx.body = data.stream; } -- cgit v1.2.3-freya From e2e7babee0de35385eb74830c82eaccdb28f013a Mon Sep 17 00:00:00 2001 From: syuilo Date: Fri, 13 Apr 2018 11:44:39 +0900 Subject: wip --- src/server/api/api-handler.ts | 6 ++++-- src/server/api/call.ts | 7 +++---- src/server/api/common/signin.ts | 4 +++- src/server/api/index.ts | 3 ++- src/server/api/private/signin.ts | 4 ++-- 5 files changed, 14 insertions(+), 10 deletions(-) (limited to 'src/server/api/api-handler.ts') diff --git a/src/server/api/api-handler.ts b/src/server/api/api-handler.ts index 947794a20e..e716dcdc01 100644 --- a/src/server/api/api-handler.ts +++ b/src/server/api/api-handler.ts @@ -7,6 +7,8 @@ import { IUser } from '../../models/user'; import { IApp } from '../../models/app'; export default async (endpoint: Endpoint, ctx: Koa.Context) => { + const body = ctx.is('multipart/form-data') ? (ctx.req as any).body : ctx.request.body; + const reply = (x?: any, y?: any) => { if (x === undefined) { ctx.status = 204; @@ -25,7 +27,7 @@ export default async (endpoint: Endpoint, ctx: Koa.Context) => { // Authentication try { - [user, app] = await authenticate(ctx.request.body['i']); + [user, app] = await authenticate(body['i']); } catch (e) { reply(403, 'AUTHENTICATION_FAILED'); return; @@ -35,7 +37,7 @@ export default async (endpoint: Endpoint, ctx: Koa.Context) => { // API invoking try { - res = await call(endpoint, user, app, ctx.request.body, ctx.req); + res = await call(endpoint, user, app, body, (ctx.req as any).file); } catch (e) { reply(400, e); return; diff --git a/src/server/api/call.ts b/src/server/api/call.ts index cc40294657..713add566a 100644 --- a/src/server/api/call.ts +++ b/src/server/api/call.ts @@ -1,4 +1,3 @@ -import * as http from 'http'; import * as multer from 'koa-multer'; import endpoints, { Endpoint } from './endpoints'; @@ -6,7 +5,7 @@ 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?: http.IncomingMessage) => new Promise(async (ok, rej) => { +export default (endpoint: string | Endpoint, user: IUser, app: IApp, data: any, file?: any) => new Promise(async (ok, rej) => { const isSecure = user != null && app == null; const ep = typeof endpoint == 'string' ? endpoints.find(e => e.name == endpoint) : endpoint; @@ -36,8 +35,8 @@ export default (endpoint: string | Endpoint, user: IUser, app: IApp, data: any, let exec = require(`${__dirname}/endpoints/${ep.name}`); - if (ep.withFile && req) { - exec = exec.bind(null, (req as multer.MulterIncomingMessage).file); + if (ep.withFile && file) { + exec = exec.bind(null, file); } let res; diff --git a/src/server/api/common/signin.ts b/src/server/api/common/signin.ts index f57c38414c..44e1336f27 100644 --- a/src/server/api/common/signin.ts +++ b/src/server/api/common/signin.ts @@ -3,7 +3,7 @@ import * as Koa from 'koa'; import config from '../../../config'; import { ILocalUser } from '../../../models/user'; -export default function(ctx: Koa.Context, user: ILocalUser, redirect: boolean) { +export default function(ctx: Koa.Context, user: ILocalUser, redirect = false) { const expires = 1000 * 60 * 60 * 24 * 365; // One Year ctx.cookies.set('i', user.token, { path: '/', @@ -16,5 +16,7 @@ export default function(ctx: Koa.Context, user: ILocalUser, redirect: boolean) { if (redirect) { ctx.redirect(config.url); + } else { + ctx.status = 204; } } diff --git a/src/server/api/index.ts b/src/server/api/index.ts index 2ea5fccb5b..009c99acae 100644 --- a/src/server/api/index.ts +++ b/src/server/api/index.ts @@ -14,7 +14,8 @@ const handler = require('./api-handler').default; // Init app const app = new Koa(); app.use(bodyParser({ - detectJSON: () => true + // リクエストが multipart/form-data でない限りはJSONだと見なす + detectJSON: ctx => !ctx.is('multipart/form-data') })); // Init multer instance diff --git a/src/server/api/private/signin.ts b/src/server/api/private/signin.ts index 1737007206..5450c7ad27 100644 --- a/src/server/api/private/signin.ts +++ b/src/server/api/private/signin.ts @@ -60,14 +60,14 @@ export default async (ctx: Koa.Context) => { }); if (verified) { - signin(ctx, user, false); + signin(ctx, user); } else { ctx.throw(400, { error: 'invalid token' }); } } else { - signin(ctx, user, false); + signin(ctx, user); } } else { ctx.throw(400, { -- cgit v1.2.3-freya