ストリーム経由でAPIにリクエストできるように
This commit is contained in:
		
							parent
							
								
									91791834be
								
							
						
					
					
						commit
						bd3d57a67f
					
				|  | @ -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(), | ||||
|  |  | |||
|  | @ -19,7 +19,7 @@ export type IApp = { | |||
| 	nameId: string; | ||||
| 	nameIdLower: string; | ||||
| 	description: string; | ||||
| 	permission: string; | ||||
| 	permission: string[]; | ||||
| 	callbackUrl: string; | ||||
| }; | ||||
| 
 | ||||
|  |  | |||
|  | @ -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)); | ||||
| }; | ||||
|  |  | |||
|  | @ -1,50 +1,24 @@ | |||
| import * as express from 'express'; | ||||
| import App from '../../models/app'; | ||||
| import App, { IApp } from '../../models/app'; | ||||
| import { default as User, IUser } from '../../models/user'; | ||||
| import AccessToken from '../../models/access-token'; | ||||
| import isNativeToken from './common/is-native-token'; | ||||
| 
 | ||||
| export interface IAuthContext { | ||||
| 	/** | ||||
| 	 * App which requested | ||||
| 	 */ | ||||
| 	app: any; | ||||
| 
 | ||||
| 	/** | ||||
| 	 * Authenticated user | ||||
| 	 */ | ||||
| 	user: IUser; | ||||
| 
 | ||||
| 	/** | ||||
| 	 * Whether requested with a User-Native Token | ||||
| 	 */ | ||||
| 	isSecure: boolean; | ||||
| } | ||||
| 
 | ||||
| export default (req: express.Request) => new Promise<IAuthContext>(async (resolve, reject) => { | ||||
| 	const token = req.body['i'] as string; | ||||
| 
 | ||||
| export default (token: string) => new Promise<[IUser, IApp]>(async (resolve, reject) => { | ||||
| 	if (token == null) { | ||||
| 		return resolve({ | ||||
| 			app: null, | ||||
| 			user: null, | ||||
| 			isSecure: false | ||||
| 		}); | ||||
| 		resolve([null, null]); | ||||
| 		return; | ||||
| 	} | ||||
| 
 | ||||
| 	if (isNativeToken(token)) { | ||||
| 		// Fetch user
 | ||||
| 		const user: IUser = await User | ||||
| 			.findOne({ 'token': token }); | ||||
| 			.findOne({ token }); | ||||
| 
 | ||||
| 		if (user === null) { | ||||
| 			return reject('user not found'); | ||||
| 		} | ||||
| 
 | ||||
| 		return resolve({ | ||||
| 			app: null, | ||||
| 			user: user, | ||||
| 			isSecure: true | ||||
| 		}); | ||||
| 		resolve([user, null]); | ||||
| 	} else { | ||||
| 		const accessToken = await AccessToken.findOne({ | ||||
| 			hash: token.toLowerCase() | ||||
|  | @ -60,10 +34,6 @@ export default (req: express.Request) => new Promise<IAuthContext>(async (resolv | |||
| 		const user = await User | ||||
| 			.findOne({ _id: accessToken.userId }); | ||||
| 
 | ||||
| 		return resolve({ | ||||
| 			app: app, | ||||
| 			user: user, | ||||
| 			isSecure: false | ||||
| 		}); | ||||
| 		resolve([user, app]); | ||||
| 	} | ||||
| }); | ||||
|  |  | |||
|  | @ -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); | ||||
| }); | ||||
|  | @ -36,14 +36,10 @@ import App, { pack } from '../../../../models/app'; | |||
| 
 | ||||
| /** | ||||
|  * Show an app | ||||
|  * | ||||
|  * @param {any} params | ||||
|  * @param {any} user | ||||
|  * @param {any} _ | ||||
|  * @param {any} isSecure | ||||
|  * @return {Promise<any>} | ||||
|  */ | ||||
| module.exports = (params, user, _, isSecure) => new Promise(async (res, rej) => { | ||||
| module.exports = (params, user, app) => new Promise(async (res, rej) => { | ||||
| 	const isSecure = user != null && app == null; | ||||
| 
 | ||||
| 	// Get 'appId' parameter
 | ||||
| 	const [appId, appIdErr] = $(params.appId).optional.id().$; | ||||
| 	if (appIdErr) return rej('invalid appId param'); | ||||
|  | @ -57,16 +53,16 @@ module.exports = (params, user, _, isSecure) => new Promise(async (res, rej) => | |||
| 	} | ||||
| 
 | ||||
| 	// Lookup app
 | ||||
| 	const app = appId !== undefined | ||||
| 	const ap = appId !== undefined | ||||
| 		? await App.findOne({ _id: appId }) | ||||
| 		: await App.findOne({ nameIdLower: nameId.toLowerCase() }); | ||||
| 
 | ||||
| 	if (app === null) { | ||||
| 	if (ap === null) { | ||||
| 		return rej('app not found'); | ||||
| 	} | ||||
| 
 | ||||
| 	// Send response
 | ||||
| 	res(await pack(app, user, { | ||||
| 		includeSecret: isSecure && app.userId.equals(user._id) | ||||
| 	res(await pack(ap, user, { | ||||
| 		includeSecret: isSecure && ap.userId.equals(user._id) | ||||
| 	})); | ||||
| }); | ||||
|  |  | |||
|  | @ -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, | ||||
|  |  | |||
|  | @ -7,14 +7,10 @@ import event from '../../../../publishers/stream'; | |||
| 
 | ||||
| /** | ||||
|  * Update myself | ||||
|  * | ||||
|  * @param {any} params | ||||
|  * @param {any} user | ||||
|  * @param {any} _ | ||||
|  * @param {boolean} isSecure | ||||
|  * @return {Promise<any>} | ||||
|  */ | ||||
| module.exports = async (params, user, _, isSecure) => new Promise(async (res, rej) => { | ||||
| module.exports = async (params, user, app) => new Promise(async (res, rej) => { | ||||
| 	const isSecure = user != null && app == null; | ||||
| 
 | ||||
| 	// Get 'name' parameter
 | ||||
| 	const [name, nameErr] = $(params.name).optional.nullable.string().pipe(isValidName).$; | ||||
| 	if (nameErr) return rej('invalid name param'); | ||||
|  |  | |||
|  | @ -35,9 +35,6 @@ import Meta from '../../../models/meta'; | |||
| 
 | ||||
| /** | ||||
|  * Show core info | ||||
|  * | ||||
|  * @param {any} params | ||||
|  * @return {Promise<any>} | ||||
|  */ | ||||
| module.exports = (params) => new Promise(async (res, rej) => { | ||||
| 	const meta: any = (await Meta.findOne()) || {}; | ||||
|  |  | |||
|  | @ -6,14 +6,8 @@ import Subscription from '../../../../models/sw-subscription'; | |||
| 
 | ||||
| /** | ||||
|  * subscribe service worker | ||||
|  * | ||||
|  * @param {any} params | ||||
|  * @param {any} user | ||||
|  * @param {any} _ | ||||
|  * @param {boolean} isSecure | ||||
|  * @return {Promise<any>} | ||||
|  */ | ||||
| module.exports = async (params, user, _, isSecure) => new Promise(async (res, rej) => { | ||||
| module.exports = async (params, user, app) => new Promise(async (res, rej) => { | ||||
| 	// Get 'endpoint' parameter
 | ||||
| 	const [endpoint, endpointErr] = $(params.endpoint).string().$; | ||||
| 	if (endpointErr) return rej('invalid endpoint param'); | ||||
|  |  | |||
|  | @ -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'; | ||||
| 
 | ||||
| /** | ||||
|  |  | |||
|  | @ -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'); | ||||
|  |  | |||
|  | @ -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); | ||||
| 	} | ||||
| }; | ||||
|  | @ -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': | ||||
|  |  | |||
|  | @ -2,9 +2,6 @@ import * as http from 'http'; | |||
| import * as websocket from 'websocket'; | ||||
| import * as redis from 'redis'; | ||||
| import config from '../../config'; | ||||
| import { default as User, IUser } from '../../models/user'; | ||||
| import AccessToken from '../../models/access-token'; | ||||
| import isNativeToken from './common/is-native-token'; | ||||
| 
 | ||||
| import homeStream from './stream/home'; | ||||
| import driveStream from './stream/drive'; | ||||
|  | @ -16,6 +13,7 @@ import serverStream from './stream/server'; | |||
| import requestsStream from './stream/requests'; | ||||
| import channelStream from './stream/channel'; | ||||
| import { ParsedUrlQuery } from 'querystring'; | ||||
| import authenticate from './authenticate'; | ||||
| 
 | ||||
| module.exports = (server: http.Server) => { | ||||
| 	/** | ||||
|  | @ -53,7 +51,7 @@ module.exports = (server: http.Server) => { | |||
| 		} | ||||
| 
 | ||||
| 		const q = request.resourceURL.query as ParsedUrlQuery; | ||||
| 		const user = await authenticate(q.i as string); | ||||
| 		const [user, app] = await authenticate(q.i as string); | ||||
| 
 | ||||
| 		if (request.resourceURL.pathname === '/othello-game') { | ||||
| 			othelloGameStream(request, connection, subscriber, user); | ||||
|  | @ -75,46 +73,9 @@ module.exports = (server: http.Server) => { | |||
| 			null; | ||||
| 
 | ||||
| 		if (channel !== null) { | ||||
| 			channel(request, connection, subscriber, user); | ||||
| 			channel(request, connection, subscriber, user, app); | ||||
| 		} else { | ||||
| 			connection.close(); | ||||
| 		} | ||||
| 	}); | ||||
| }; | ||||
| 
 | ||||
| /** | ||||
|  * 接続してきたユーザーを取得します | ||||
|  * @param token 送信されてきたトークン | ||||
|  */ | ||||
| function authenticate(token: string): Promise<IUser> { | ||||
| 	if (token == null) { | ||||
| 		return Promise.resolve(null); | ||||
| 	} | ||||
| 
 | ||||
| 	return new Promise(async (resolve, reject) => { | ||||
| 		if (isNativeToken(token)) { | ||||
| 			// Fetch user
 | ||||
| 			const user: IUser = await User | ||||
| 				.findOne({ | ||||
| 					host: null, | ||||
| 					'token': token | ||||
| 				}); | ||||
| 
 | ||||
| 			resolve(user); | ||||
| 		} else { | ||||
| 			const accessToken = await AccessToken.findOne({ | ||||
| 				hash: token | ||||
| 			}); | ||||
| 
 | ||||
| 			if (accessToken == null) { | ||||
| 				return reject('invalid signature'); | ||||
| 			} | ||||
| 
 | ||||
| 			// Fetch user
 | ||||
| 			const user: IUser = await User | ||||
| 				.findOne({ _id: accessToken.userId }); | ||||
| 
 | ||||
| 			resolve(user); | ||||
| 		} | ||||
| 	}); | ||||
| } | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue