团队内部开发了很多 Web 平台的工具链,由于服务端逻辑比较复杂,生产环境上运行的服务涉及到的一些问题,排查起来相对困难,需要进入到当前服务内部查看运行情况,本文记录了一套对 NodeJS 服务通过包装 Node VM 沙盒模式,支持远程运行在前端编辑器上编写的代码
首先前端用 vue 支持了一个代码编辑器,通过 axios api 请求将代码发送到 node server 端,在 vm 执行,然后返回执行结果给前端
前端代码比较简单,主要是基于 vue 技术栈对 CodeEditor 的封装 source
主要是对 node vm 的封装 source
import vm, {Context, RunningScriptOptions, Script} from 'vm'
import {connectDBAsync, disconnectDBAsync} from '../db'
const globalConsole = global.console
// 沙盒日志
const sandboxConsole = (level: string, msg: any, ...args: any[]) => {
console.result += `${new Date().toLocaleString()} ${level}:\n${msg}`
args.forEach(item => {
if (typeof item !== 'object') {
console.result += ` ${item}`
} else {
console.result += ` ${JSON.stringify(item)}`
}
})
console.result += '\n\n'
}
const console: { result: string, log: Function, warn: Function, error: Function } = {
result: '',
log: (msg: any, ...args: any[]) => {
globalConsole.log('Sandbox console', msg, ...args)
sandboxConsole('debug', msg, ...args)
},
warn: (msg: any, ...args: any[]) => {
globalConsole.warn('Sandbox console', msg, ...args)
sandboxConsole('Warn', msg, ...args)
},
error: (msg: any, ...args: any[]) => {
globalConsole.error('Sandbox console', msg, ...args)
sandboxConsole('Erro', msg, ...args)
},
}
// vm 主要封装
// sandbox:any 是提供给前端允许调用的模块
// code 需要执行的代码
export default async function executeCodeInSandbox(sandbox: any, code?: string) {
if (!code || code.trim().length === 0) {
throw Error('Nothing executed.')
}
// 结构化代码
const slices = code.trim().split(/\r?\n/)
.map((l: string, i: number) => {
return l.trimEnd()
})
.filter((l: string, i: number) => {
return /^[A-Za-z0-9{}(&|]/.test(l.trimStart())
})
if (slices.length == 0) {
throw Error('Nothing executed.')
} else if (slices.length == 1) {
code = slices[0]
} else {
code = slices.reduce((p: string, c: string, i: number, a: string[]) => {
return `${p}\n ${c}`
})
}
// 对要调用的代码做一层安全包装,同时在执行前打开 DB,以便支持数据库查询操作
let wrapCode = `globalConsole.log('VM exec in process:', process.pid, process.title)
try {
console.result = ''
await connectDBAsync()
${code}
await disconnectDBAsync()
inject(console.result)
} catch (e) {
inject(e)
}`
console.log(wrapCode);
// 在 node:vm 中执行 code
await (async (code: string) => {
return new Promise((resolve) => {
const script: Script = new vm.Script(`(async()=>{${code}})()`)
const options: RunningScriptOptions = {
timeout: 1000,
displayErrors: true,
}
const context: Context = vm.createContext({
...sandbox,
console,
globalConsole,
process,
connectDBAsync,
disconnectDBAsync,
inject: (result: any) => {
sandbox.result = result
resolve(result)
}
})
script.runInContext(context, options)
})
})(wrapCode)
// 返回代码执行结果
return sandbox.result // util.inspect(sandbox.result)
}
Koa 路由接收 http 请求
import Router from 'koa-router'
import Koa from "koa"
import executeCodeInSandbox from "../utils/sandbox";
import { getUserByEmail } from '../db/userDao';
// modules which register in sandboxEnv, the code can reference only !!!
const sandboxEnv: any = {
require,
getUserByEmail
}
export const sandboxRouter: Router = new Router()
sandboxRouter.post('execute-code', '/execute-code', async (ctx: Koa.Context) => {
const { codeData } = ctx.request.body as { codeData?: string }
let result: any
console.log('sandbox exec start...\n', codeData)
try {
result = await executeCodeInSandbox({ ...sandboxEnv }, codeData)
if (typeof result !== 'object') {
result = `${result}`
}
} catch (e: any) {
result = e.stack ? e.stack : `${e}`
console.error('sandbox exec error', result)
}
console.log('sandbox exec end...\n', result)
ctx.body = {
result: result
}
})
这里需要注意, server 端可以将 sandboxEnv(utils/daos 等模块) 注册到了沙盒 vm context 中,只有注册到沙盒中的模块,前端代码才能够调用的到!
在前端代码编辑器中编写代码查看运行效果: