temp commit

This commit is contained in:
41666 2019-04-20 04:46:46 -05:00
parent e86f7ff68e
commit 6fb39d6c4d
No known key found for this signature in database
GPG key ID: BC51D07640DC10AF
22 changed files with 282 additions and 39 deletions

View file

@ -0,0 +1,11 @@
{
"presets": [ ["@babel/preset-env", { "targets": {"node": "current"} }], "@babel/preset-flow" ],
"plugins": [
"@babel/plugin-syntax-dynamic-import",
"@babel/plugin-proposal-class-properties",
"@babel/plugin-proposal-optional-chaining",
"@babel/plugin-proposal-export-namespace-from",
["@babel/plugin-transform-runtime",
{ "helpers": false }]
]
}

View file

@ -0,0 +1,24 @@
{
"private": true,
"name": "@roleypoly/rpc-server",
"version": "2.0.0",
"scripts": {
"build": "babel -d ./lib ./src",
"dev": "yarn build --watch",
"postinstall": "test -d lib || npm run build"
},
"types": "src/index.js",
"main": "lib/index.js",
"files": [
"lib"
],
"dependencies": {
"async-retry": "^1.2.3",
"glob": "^7.1.3",
"nats": "^1.2.10"
},
"devDependencies": {
"@babel/cli": "^7.4.3",
"@babel/plugin-proposal-export-namespace-from": "^7.2.0"
}
}

View file

@ -0,0 +1,53 @@
// @flow
import logger from './logger'
import glob from 'glob'
import path from 'path'
const log = logger(__filename)
const PROD: boolean = process.env.NODE_ENV === 'production'
export default (globString: string, ctx: any, forceClear: ?boolean = false): {
[rpc: string]: Function
} => {
let map = {}
const apis = glob.sync(globString)
log.debug('found rpcs', apis)
for (let a of apis) {
const filename = path.basename(a)
// const dirname = path.dirname(a)
const pathname = a
delete require.cache[require.resolve(pathname)]
// internal stuff
if (filename.startsWith('_')) {
log.debug(`skipping ${a}`)
continue
}
// testing only
if (filename.endsWith('_test.js') && PROD) {
log.debug(`skipping ${a}`)
continue
}
log.debug(`mounting ${a}`)
try {
const r = require(pathname)
let o = r
if (o.default) {
o = r.default
}
map = {
...map,
...o(ctx)
}
} catch (e) {
log.error(`couldn't mount ${a}`, e)
}
}
return map
}

View file

@ -0,0 +1,143 @@
// @flow
import fnv from 'fnv-plus'
import autoloader from './autoloader'
import { RPCError } from '@roleypoly/rpc-client'
import type Roleypoly, { Router } from '@roleypoly/server/Roleypoly'
import type { Context } from 'koa'
export * as secureAs from './security'
// import logger from '../logger'
// const log = logger(__filename)
export type RPCIncoming = {
fn: string,
args: any[]
}
export type RPCOutgoing = {
hash: string,
response: any
}
export default class RPCServer {
ctx: Roleypoly
rpcMap: {
[rpc: string]: Function
}
mapHash: string
rpcCalls: { name: string, args: number }[]
constructor (ctx: Roleypoly) {
this.ctx = ctx
this.reload()
ctx.addRouteHook(this.hookRoutes)
}
reload () {
// actual function map
this.rpcMap = autoloader(this.ctx.ctx)
// hash of the map
// used for known-reloads in the client.
this.mapHash = fnv.hash(Object.keys(this.rpcMap)).str()
// call map for the client.
this.rpcCalls = Object.keys(this.rpcMap).map(fn => ({ name: fn, args: 0 }))
}
hookRoutes = (router: Router) => {
// RPC call reporter.
// this is NEVER called in prod.
// it is used to generate errors if RPC calls don't exist or are malformed in dev.
router.get('/api/_rpc', async (ctx: Context) => {
ctx.body = ({
hash: this.mapHash,
available: this.rpcCalls
}: any)
ctx.status = 200
return true
})
router.post('/api/_rpc', this.handleRPC)
}
handleRPC = async (ctx: Context) => {
// handle an impossible situation
if (!(ctx.request.body instanceof Object)) {
return this.rpcError(ctx, null, new RPCError('RPC format was very incorrect.', 400))
}
// check if RPC exists
const { fn, args } = ((ctx.request.body: any): RPCIncoming)
if (!(fn in this.rpcMap)) {
return this.rpcError(ctx, null, new RPCError(`RPC call ${fn}(...) not found.`, 404))
}
const call = this.rpcMap[fn]
// if call.length (which is the solid args list)
// is longer than args, we have too little to call the function.
// if (args.length < call.length) {
// return this.rpcError(ctx, null, new RPCError(`RPC call ${fn}() with ${args.length} arguments does not exist.`, 400))
// }
try {
const response = await call(ctx, ...args)
ctx.body = {
hash: this.mapHash,
response
}
ctx.status = 200
} catch (err) {
return this.rpcError(ctx, 'RPC call errored', err, 500)
}
}
/**
* For internally called stuff, such as from a bot shard.
*/
async call (fn: string, ...args: any[]) {
if (!(fn in this.rpcMap)) {
throw new RPCError(`RPC call ${fn}(...) not found.`, 404)
}
const call = this.rpcMap[fn]
return call(...args)
}
rpcError (ctx: Context & {body: any}, msg: ?string, err: ?Error = null, code: ?number = null) {
// log.error('rpc error', { msg, err })
ctx.body = {
msg: err?.message || msg,
error: true
}
ctx.status = code || 500
if (err != null) {
if (err instanceof RPCError || err.constructor.name === 'RPCError') {
// $FlowFixMe
ctx.status = err.code
}
}
// if (err != null && err.constructor.name === 'RPCError') {
// console.log({ status: err.code })
// // this is one of our own errors, so we have a lot of data on this.
// ctx.status = err.code // || code || 500
// ctx.body.msg = `${err.message || msg || 'RPC Error'}`
// } else {
// if (msg == null && err != null) {
// ctx.body.msg = err.message
// }
// // if not, just play cloudflare, say something was really weird, and move on.
// ctx.status = code || 520
// ctx.message = 'Unknown Error'
// }
}
}

View file

@ -0,0 +1,70 @@
// @flow
import chalk from 'chalk'
export class Logger {
debugOn: boolean
name: string
quietSql: boolean
constructor (name: string, debugOverride: boolean = false) {
this.name = name
this.debugOn = (process.env.DEBUG === 'true' || process.env.DEBUG === '*') || debugOverride
this.quietSql = (process.env.DEBUG_SQL !== 'true')
}
fatal (text: string, ...data: any) {
this.error(text, data)
if (typeof data[data.length - 1] === 'number') {
process.exit(data[data.length - 1])
} else {
process.exit(1)
}
}
error (text: string, ...data: any) {
console.error(chalk.red.bold(`ERR ${this.name}:`) + `\n ${text}`, data)
}
warn (text: string, ...data: any) {
console.warn(chalk.yellow.bold(`WARN ${this.name}:`) + `\n ${text}`, data)
}
notice (text: string, ...data: any) {
console.log(chalk.cyan.bold(`NOTICE ${this.name}:`) + `\n ${text}`, data)
}
info (text: string, ...data: any) {
console.info(chalk.blue.bold(`INFO ${this.name}:`) + `\n ${text}`, data)
}
deprecated (text: string, ...data: any) {
console.warn(chalk.yellowBright(`DEPRECATED ${this.name}:`) + `\n ${text}`, data)
console.trace()
}
request (text: string, ...data: any) {
console.info(chalk.green.bold(`HTTP ${this.name}:`) + `\n ${text}`)
}
debug (text: string, ...data: any) {
if (this.debugOn) {
console.log(chalk.gray.bold(`DEBUG ${this.name}:`) + `\n ${text}`, data)
}
}
rpc (call: string, ...data: any) {
console.log(chalk.redBright.bold`RPC` + chalk.redBright(` ${call}():`), data)
}
sql (logger: Logger, ...data: any) {
if (logger.debugOn && !logger.quietSql) {
console.log(chalk.bold('DEBUG SQL:\n '), data)
}
}
}
export default (pathname: string) => {
const name = pathname.replace(__dirname, '').replace('.js', '')
return new Logger(name)
}

View file

@ -0,0 +1,181 @@
// @flow
import { type AppContext } from '@roleypoly/server/Roleypoly'
import { type Context } from 'koa'
import { RPCError } from '@roleypoly/rpc-client'
import logger from '../../roleypoly-server/logger'
const log = logger(__filename)
const PermissionError = new RPCError('User does not have permission to call this RPC.', 403)
const logFacts = (
ctx: Context,
extra: { [x:string]: any } = {}
) => ({
fn: (ctx.request.body: any).fn,
ip: ctx.ip,
user: ctx.session.userId,
...extra
})
export const authed = (
$: AppContext,
fn: (ctx: Context, ...args: any[]) => any,
silent: boolean = false
) => async (
ctx: Context,
...args: any[]
) => {
if (await $.auth.isLoggedIn(ctx)) {
return fn(ctx, ...args)
}
if ($.config.dev) {
log.debug('authed failed')
throw new RPCError('User is not logged in', 403)
}
if (!silent) {
log.info('RPC call authed check fail', logFacts(ctx))
}
throw PermissionError
}
export const root = (
$: AppContext,
fn: (ctx: Context, ...args: any[]) => any,
silent: boolean = false
) => authed($, (
ctx: Context,
...args: any[]
) => {
if ($.discord.isRoot(ctx.session.userId)) {
return fn(ctx, ...args)
}
if ($.config.dev) {
log.debug('root failed')
throw new RPCError('User is not root', 403)
}
if (!silent) {
log.info('RPC call root check fail', logFacts(ctx))
}
throw PermissionError
})
export const manager = (
$: AppContext,
fn: (ctx: Context, server: string, ...args: any[]) => any,
silent: boolean = false
) => member($, (
ctx: Context,
server: string,
...args: any[]
) => {
if ($.discord.canManageRoles(server, ctx.session.userId)) {
return fn(ctx, server, ...args)
}
if ($.config.dev) {
log.debug('manager failed')
throw new RPCError('User is not a role manager', 403)
}
if (!silent) {
log.info('RPC call manager check fail', logFacts(ctx, { server }))
}
throw PermissionError
})
export const member = (
$: AppContext,
fn: (ctx: Context, server: string, ...args: any[]) => any,
silent: boolean = false
) => authed($, async (
ctx: Context,
server: string,
...args: any[]
) => {
if (await $.discord.isMember(server, ctx.session.userId)) {
return fn(ctx, server, ...args)
}
if ($.config.dev) {
log.debug('member failed')
throw new RPCError('User is not a member of this server', 403)
}
if (!silent) {
log.info('RPC call member check fail', logFacts(ctx, { server }))
}
throw PermissionError
})
export const any = (
$: AppContext,
fn: (ctx: Context, ...args: any[]) => any,
silent: boolean = false
) => (...args: any) => fn(...args)
export const bot = (
$: AppContext,
fn: (ctx: Context, ...args: any[]) => any,
silent: boolean = false
) => (
ctx: Context,
...args: any
) => {
const authToken: ?string = ctx.request.headers['authorization']
if (authToken != null && authToken.startsWith('Bot ')) {
if (authToken === `Bot ${$.config.sharedSecret}`) {
return fn(ctx, ...args)
}
}
if (!silent) {
log.info('RPC bot check failed', logFacts(ctx))
}
throw PermissionError
}
type Handler = (ctx: Context, ...args: any[]) => any
type Strategy = (
$: AppContext,
fn: Handler,
silent?: boolean
) => any
type StrategyPair = [ Strategy, Handler ]
/**
* Weird func but ok -- test that a strategy doesn't fail, and run the first that doesn't.
*/
export const decide = (
$: AppContext,
...strategies: StrategyPair[]
) => async (...args: any) => {
for (let [ strat, handler ] of strategies) {
if (strat === null) {
strat = any
}
try {
return await strat($, handler, true)(...args)
} catch (e) {
continue
}
}
// if we reach the end, just throw
if ($.config.dev) {
log.info('decide failed for', strategies.map(v => v[0].name))
}
throw PermissionError
}