├── .coveralls.yml ├── .dawn ├── pipe.yml └── rc.yml ├── .eslintrc.json ├── .eslintrc.yml ├── .gitignore ├── .npmignore ├── .travis.yml ├── .vscode ├── launch.json └── tasks.json ├── DOC.md ├── LICENSE ├── NOTICE.md ├── README.md ├── package-lock.json ├── package.json ├── src ├── CGroups.ts ├── Call.ts ├── IAlias.ts ├── ICallOptions.ts ├── IMessage.ts ├── ISafeifyOptions.ts ├── IScriptOptions.ts ├── IUnsafe.ts ├── MessageType.ts ├── Proxy.ts ├── Safeify.ts ├── Script.ts ├── Worker.ts ├── WorkerState.ts ├── debug.ts ├── index.ts └── runner.ts ├── test ├── cgroups.test.ts ├── proxy.test.ts └── run.test.ts ├── tsconfig.json └── tslint.json /.coveralls.yml: -------------------------------------------------------------------------------- 1 | service_name: travis-pro 2 | repo_token: DxYnLhzm2xRP3qYa3fNM8H0zRHXgDXnld -------------------------------------------------------------------------------- /.dawn/pipe.yml: -------------------------------------------------------------------------------- 1 | init: 2 | - name: pkginfo 3 | 4 | dev: 5 | - name: shell 6 | script: 7 | - DEBUG=none ts-node ./src/debug.ts 8 | 9 | build: 10 | - name: clean 11 | target: ./lib/ 12 | - name: shell 13 | script: 14 | - tsc --locale zh-cn -d --esModuleInterop 15 | 16 | test: 17 | - name: clean 18 | target: ./lib/ 19 | - name: lint 20 | target: ./src 21 | - name: tslint 22 | - name: unit 23 | env: typescript 24 | files: ./test/**/*.ts 25 | 26 | publish: 27 | - name: shell 28 | script: 29 | - dn test 30 | - dn build 31 | - npm pu --registry=http://registry.npmjs.org -------------------------------------------------------------------------------- /.dawn/rc.yml: -------------------------------------------------------------------------------- 1 | server: https://alibaba.github.io/dawn 2 | registry: https://registry.npm.taobao.org -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "dawn" 4 | ] 5 | } -------------------------------------------------------------------------------- /.eslintrc.yml: -------------------------------------------------------------------------------- 1 | extends: 2 | - dawn 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | .DS_Store 3 | node_modules/ 4 | .nyc_output/ 5 | coverage/ 6 | lib/ -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | *.log 2 | .DS_Store 3 | node_modules/ 4 | .nyc_output/ 5 | coverage/ 6 | test/ 7 | .dawn/ 8 | .vscode/ 9 | src/ -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "10" -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // 使用 IntelliSense 了解相关属性。 3 | // 悬停以查看现有属性的描述。 4 | // 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "node", 9 | "request": "launch", 10 | "name": "Launch Program", 11 | "program": "${workspaceFolder}/lib/debug.js", 12 | "env": { 13 | "DEBUG": "app,static" 14 | }, 15 | "preLaunchTask": "tsc", 16 | "console": "integratedTerminal", 17 | "outFiles": [ 18 | "${workspaceFolder}/lib/**/*.js" 19 | ] 20 | } 21 | ] 22 | } -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=733558 3 | // for the documentation about the tasks.json format 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "identifier": "tsc", 8 | "type": "typescript", 9 | "tsconfig": "tsconfig.json", 10 | "option": "watch", 11 | "problemMatcher": [ 12 | "$tsc-watch" 13 | ] 14 | } 15 | ] 16 | } -------------------------------------------------------------------------------- /DOC.md: -------------------------------------------------------------------------------- 1 | # Safeify 2 | 3 | > 让你的 Node 应用安全的隔离的执行非信任的用户自定义代码。 4 | 5 | 6 | ![1](https://dn-cnode.qbox.me/FhCV7Ayx7uZy6mh6EHBt3IzdBI2Z) 7 | 8 | ## 有哪些动态执行脚本的场景? 9 | 10 | 在一些应用中,我们希望给用户提供插入自定义逻辑的能力,比如 Microsoft 的 Office 中的 `VBA`,比如一些游戏中的 `lua` 脚本,FireFox 的「油猴脚本」,能够让用户发在可控的范围和权限内发挥想象做一些好玩、有用的事情,扩展了能力,满足用户的个性化需求。 11 | 12 | 大多数都是一些客户端程序,在一些在线的系统和产品中也常常也有类似的需求,事实上,在线的应用中也有不少提供了自定义脚本的能力,比如 Google Docs 中的 `Apps Script`,它可以让你使用 `JavaScript` 做一些非常有用的事情,比如运行代码来响应文档打开事件或单元格更改事件,为公式制作自定义电子表格函数等等。 13 | 14 | 与运行在「用户电脑中」的客户端应用不同,用户的自定义脚本通常只能影响用户自已,而对于在线的应用或服务来讲,有一些情况就变得更为重要,比如「安全」,用户的「自定义脚本」必须严格受到限制和隔离,即不能影响到宿主程序,也不能影响到其它用户。 15 | 16 | 而 Safeify 就是一个针对 Nodejs 应用,用于安全执行用户自定义的非信任脚本的模块。 17 | 18 | 19 | ## 怎样安全的执行动态脚本? 20 | 21 | 我们先看看通常都能如何在 JavaScript 程序中动态执行一段代码?比如大名顶顶的 `eval` 22 | 23 | ```js 24 | eval('1+2') 25 | ``` 26 | 27 | 上述代码没有问题顺利执行了,`eval` 是全局对象的一个函数属性,执行的代码拥有着和应用中其它正常代码一样的的权限,它能访问「执行上下文」中的局部变量,也能访问所有「全局变量」,在这个场景下,它是一个非常危险的函数。 28 | 29 | 再来看看 `Functon`,通过 `Function` 构造器,我们可以动态的创建一个函数,然后执行它 30 | 31 | ```js 32 | const sum = new Function('m', 'n', 'return m + n'); 33 | console.log(sum(1, 2)); 34 | ``` 35 | 36 | 它也一样的顺利执行了,使用 Function 构造器生成的函数,并不会在创建它的上下文中创建闭包,一般在全局作用域中被创建。当运行函数的时候,只能访问自己的本地变量和全局变量,不能访问 Function 构造器被调用生成的上下文的作用域。如同一个站在地上、一个站在一张薄薄的纸上一样,在这个场景下,几乎没有高下之分。 37 | 38 | **结合 ES6 的新特性 `Proxy` 便能更安全一些** 39 | 40 | ```js 41 | function evalute(code,sandbox) { 42 | sandbox = sandbox || Object.create(null); 43 | const fn = new Function('sandbox', `with(sandbox){return (${code})}`); 44 | const proxy = new Proxy(sandbox, { 45 | has(target, key) { 46 | // 让动态执行的代码认为属性已存在 47 | return true; 48 | } 49 | }); 50 | return fn(proxy); 51 | } 52 | evalute('1+2') // 3 53 | evalute('console.log(1)') // Cannot read property 'log' of undefined 54 | ``` 55 | 56 | 我们知道无论 `eval` 还是 `function`,执行时都会把作用域一层一层向上查找,如果找不到会一直到 `global`,那么利用 `Proxy` 的原理就是,让执行了代码在 `sandobx` 中找的到,以达到「防逃逸」的目的。 57 | 58 | > 在浏览器中,还可以利用 iframe,创建一个再多安全一些的隔离环境,本文着眼于 Node.js,在这里不做过多讨论。 59 | 60 | **在 Node.js 中呢,有没有其它选择** 61 | 62 | 或许没看到这儿之前你就已经想到了 `VM`,它是 Node.js 默认就提供的一个内建模块,`VM` 模块提供了一系列 API 用于在 V8 虚拟机环境中编译和运行代码。JavaScript 代码可以被编译并立即运行,或编译、保存然后再运行。 63 | 64 | ```js 65 | const vm = require('vm'); 66 | const script = new vm.Script('m + n'); 67 | const sandbox = { m: 1, n: 2 }; 68 | const context = new vm.createContext(sandbox); 69 | script.runInContext(context); 70 | ``` 71 | 执行上这的代码就能拿到结果 `3`,同时,通过 `vm.Script` 还能指定代码执行了「最大毫秒数」,超过指定的时长将终止执行并抛出一个异常 72 | 73 | ```js 74 | try { 75 | const script = new vm.Script('while(true){}',{ timeout: 50 }); 76 | .... 77 | } catch (err){ 78 | //打印超时的 log 79 | console.log(err.message); 80 | } 81 | ``` 82 | 83 | 上面的脚本执行将会失败,被检测到超时并抛出异常,然后被 `Try Cache` 捕获到并打出 log,但同时需要注意的是 `vm.Script` 的 `timeout` 选项「只针对同步代有效」,而不包括是异步调用的时间,比如 84 | 85 | ```js 86 | const script = new vm.Script('setTimeout(()=>{},2000)',{ timeout: 50 }); 87 | .... 88 | ``` 89 | 90 | 上述代码,并不是会在 50ms 后抛出异常,因为 50ms 上边的代码同步执行肯定完了,而 `setTimeout` 所用的时间并不算在内,也就是说 `vm` 模块没有办法对异步代码直接限制执行时间。我们也不能额外通过一个 `timer` 去检查超时,因为检查了执行中的 vm 也没有方法去中止掉。 91 | 92 | 另外,在 Node.js 通过 `vm.runInContext` 看起来似乎隔离了代码执行环境,但实际上却很容易「逃逸」出去。 93 | 94 | ```js 95 | const vm = require('vm'); 96 | const sandbox = {}; 97 | const script = new vm.Script('this.constructor.constructor("return process")().exit()'); 98 | const context = vm.createContext(sandbox); 99 | script.runInContext(context); 100 | ``` 101 | 102 | 执行上边的代码,宿主程序立即就会「退出」,`sandbox` 是在 `VM` 之外的环境创建的,需 `VM` 中的代码的 `this` 指向的也是 `sandbox`,那么 103 | 104 | ```js 105 | //this.constructor 就是外所的 Object 构建函数 106 | const ObjConstructor = this.constructor; 107 | //ObjConstructor 的 constructor 就是外包的 Function 108 | const Function = ObjConstructor.constructor; 109 | //创建一个函数,并执行它,返回全局 process 全局对象 110 | const process = (new Function('return process'))(); 111 | //退出当前进程 112 | process.exit(); 113 | ``` 114 | 没有人愿意用户一段脚本就能让应用挂掉吧。除了退出进程序之外,实际上还能干更多的事情。 115 | 116 | 有个简单的方法就能避免通过 `this.constructor` 拿到 `process`,如下: 117 | 118 | ```js 119 | const vm = require('vm'); 120 | //创建一外无 proto 的空白对象作为 sandbox 121 | const sandbox = Object.create(null); 122 | const script = new vm.Script('...'); 123 | const context = vm.createContext(sandbox); 124 | script.runInContext(context); 125 | ``` 126 | 127 | 但还是有风险的,由于 JavaScript 本身的动态的特点,各种黑魔法防不胜防。事实 Node.js 的官方文档中也提到「 不要把 `VM` 当做一个安全的沙箱,去执行任意非信任的代码」。 128 | 129 | **有哪些做了进一步工作的社区模块?** 130 | 131 | 在社区中有一些开源的模块用于运行不信任代码,例如 `sandbox`、`vm2`、`jailed` 等。相比较而言 `vm2` 对各方面做了更多的安全工作,相对安全些。 132 | 133 | 从 `vm2` 的官方 `README` 中可以看到,它基于 Node.js 内建的 VM 模块,来建立基础的沙箱环境,然后同时使用上了文介绍过的 ES6 的 `Proxy` 技术来防止沙箱脚本逃逸。 134 | 135 | 用同样的测试代码来试试 `vm2` 136 | 137 | ```js 138 | const { VM } = require('vm2'); 139 | new VM().run('this.constructor.constructor("return process")().exit()'); 140 | ``` 141 | 142 | 如上代码,并没有成功结束掉宿主程序,vm2 官方 REAME 中说「vm2 是一个沙盒,可以在 Node.js 中按全的执行不受信任的代码」。 143 | 144 | 然而,事实上我们还是可以干一些「坏」事情,比如: 145 | 146 | ```js 147 | const { VM } = require('vm2'); 148 | const vm = new VM({ timeout: 1000, sandbox: {}}); 149 | vm.run('new Promise(()=>{})'); 150 | ``` 151 | 152 | 上边的代码将永远不会执行结束,如同 Node.js 内建模块一样 vm2 的 `timeout` 对异步操作是无效的。同时,`vm2` 也不能额外通过一个 `timer` 去检查超时,因为它也没有办法将执行中的 vm 终止掉。这会一点点耗费完服务器的资源,让你的应用挂掉。 153 | 154 | 那么或许你会想,我们能不能在上边的 `sandbox` 中放一个假的 `Promise` 从而禁掉 Promise 呢?答案是能提供一个「假」的 `Promise`,但却没有办法完成禁掉 `Promise`,比如 155 | 156 | ```js 157 | const { VM } = require('vm2'); 158 | const vm = new VM({ 159 | timeout: 1000, sandbox: { Promise: function(){}} 160 | }); 161 | vm.run('Promise = (async function(){})().constructor;new Promise(()=>{});'); 162 | ``` 163 | 164 | 可以看到通过一行 `Promise = (async function(){})().constructor` 就可以轻松再次拿到 `Promise` 了。从另一个层面来看,况且或许有时我们还想让自定义脚本支持异步处理呢。 165 | 166 | 167 | ## 如何建立一个更安全一些的沙箱? 168 | 169 | 通过上文的探究,我们并没有找到一个完美的方案在 Node.js 建立安全的隔离的沙箱。其中 vm2 做了不少处理,相对来讲算是较安全的方案了,但问题也很明显,比如异步不能检查超时的问题、和宿主程序在相同进程的问题。 170 | 171 | 没有进程隔离时,通过 VM 创建的 sanbox 大体是这样的 172 | 173 | ![2](https://segmentfault.com/img/bV88VW?w=1232&h=626) 174 | 175 | 那么,我们是不是可以尝试,将非受信代码,通过 vm2 这个模块隔离在一个独立的进程中执行呢?然后,执行超时时,直接将隔离的进程干掉,但这里我们需要考虑如下几个问题 176 | 177 | **通过进程池统一调度管理沙箱进程** 178 | 179 | 如果来一个执行任务,创建一个进程,用完销毁,仅处理进程的开销就已经稍大了,并且也不能不设限的开新进程和宿主应用抢资源,那么,需要建一个进程池,所有任务到来会创建一个 `Script` 实例,先进入一个 `pending` 队列,然后直接将 `script` 实例的 `defer` 对象返回,调用处就能 `await` 执行结果了,然后由 `sandbox master` 根据工程进程的空闲程序来调度执行,master 会将 `script` 的执行信息,包括重要的 `ScriptId`,发送给空闲的 worker,worker 执行完成后会将「结果 + script 信息」回传给 master,master 通过 ScriptId 识别是哪个脚本执行完毕了,就是结果进行 `resolve` 或 reject 处理。 180 | 181 | 这样,通过「进程池」即能降低「进程来回创建和销毁的开销」,也能确保不过度抢占宿主资源,同时,在异步操作超时,还能将工程进程直接杀掉,同时,master 将发现一个工程进程挂掉,会立即创建替补进程。 182 | 183 | **处理的数据和结果,还有公开给沙箱的方法** 184 | 185 | 进程间如何通讯,需要「动态代码」处理数据可以直接序列化后通过 IPC 发送给隔离 Sandbox 进程,执行结果一样经过序列化通过 IPC 传输。 186 | 187 | 其中,如果想法公开一个方法给 sandbox,因为不在一个进程,并不能方便的将一个方案的引用传递给 sandbox。我们可以将宿主的方法,在传递给 sandbox worker 之类做一下处理,转换为一个「描述对象」,包括了允许 sandbox 调用的方法信息,然后将信息,如同其它数据一样发送给 worker 进程,worker 收到数据后,识出来所「方法描述对象」,然后在 worker 进程中的 sandbox 对象上建立代理方法,代理方法同样通过 IPC 和 master 通讯。 188 | 189 | **针对沙箱进程进行 CPU 和内存配额限制** 190 | 191 | 在 Linux 平台,通过 CGoups 对沙箱进程进行整体的 CPU 和内存等资源的配额限制,Cgroups 是 Control Groups 的缩写,是 Linux 内核提供的一种可以限制、记录、隔离进程组(Process Groups)所使用的物理资源(如:CPU、Memory,IO 等等)的机制。最初由 Google 的工程师提出,后来被整合进 Linux 内核。Cgroups 也是 LXC 为实现虚拟化所使用的资源管理手段,可以说没有 CGroups 就没有 LXC。 192 | 193 | 194 | **最终,我们建立了一个大约这样的「沙箱环境** 195 | 196 | ![3](https://segmentfault.com/img/bV88VU?w=1642&h=820) 197 | 198 | 如此这般处理起来是不是感觉很麻烦?但我们就有了一个更加安全一些的沙箱环境了,这些处理。笔者已经基于 TypeScript 编写,并封装为一个独立的模块 `Safeify`。 199 | 200 | 201 | 相较于内建的 VM 及常见的几个沙箱模块, Safeify 具有如下特点: 202 | 203 | - 为将要执行的动态代码建立专门的进程池,与宿主应用程序分离在不同的进程中执行 204 | - 支持配置沙箱进程池的最大进程数量 205 | - 支持限定同步代码的最大执行时间,同时也支持限定包括异步代码在内的执行时间 206 | - 支持限定沙箱进程池的整体的 CPU 资源配额(小数) 207 | - 支持限定沙箱进程池的整体的最大的内存限制(单位 m) 208 | 209 | 210 | GitHub: https://github.com/Houfeng/safeify ,欢迎 Star & Issues 211 | 212 | 最后,简单介绍一下 Safeify 如何使用,通过如下命令安装 213 | 214 | ```sh 215 | npm i safeify --save 216 | ``` 217 | 218 | 在应用中使用,还是比较简单的,如下代码(TypeScript 中类似) 219 | 220 | ```js 221 | import { Safeify } from './Safeify'; 222 | 223 | const safeVm = new Safeify({ 224 | timeout: 50, //超时时间,默认 50ms 225 | asyncTimeout: 500, //包含异步操作的超时时间,默认 500ms 226 | quantity: 4, //沙箱进程数量,默认同 CPU 核数 227 | memoryQuota: 500, //沙箱最大能使用的内存(单位 m),默认 500m 228 | cpuQuota: 0.5, //沙箱的 cpu 资源配额(百分比),默认 50% 229 | }); 230 | 231 | const context = { 232 | a: 1, 233 | b: 2, 234 | add(a, b) { 235 | return a + b; 236 | } 237 | }; 238 | 239 | const rs = await safeVm.run(`return add(a,b)`, context); 240 | console.log('result',rs); 241 | ``` 242 | 243 | 关于安全的问题,没有最安全,只有更安全,Safeify 已在一个项目中使用,但自定义脚本的功能是仅针对内网用户,有不少动态执行代码的场景其实是可以避免的,绕不开或实在需要提供这个功能时,希望本文或 Safeify 能对大家有所帮助就行了。 244 | 245 | -- end -- -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | 203 | ---------------------------------------------------------------------------- 204 | 205 | 中文补充说明 206 | 207 | 1. 使用了 Safeify 全部或部分代码的库或应用,必须在所有公开的文档中「声明使用了 Safeify」, 208 | 同时,保留对 Safeify 的链接 https://github.com/Houfeng/safeify(在链接有变更时 209 | 需及时更新链接),保留 LICENSE 和 NOTICE 文件的全部内容。 210 | 211 | 2. 使用了 Safeify 全部或部分代码的库或应用,在所有公开的文章、博客及演讲中,应提及使用了 212 | Safeify,并放置 Safeify 相关链接。 213 | 214 | 3. 使用了 Safeify 全部或部分代码的库或应用,无论使用何种新的协议,所产生的包括但不限于 215 | 「代码、改进、创意、专利」,Safeify 都能无条件在后续版本中使用。 -------------------------------------------------------------------------------- /NOTICE.md: -------------------------------------------------------------------------------- 1 | 中文补充说明 2 | 3 | 1. 使用了 Safeify 全部或部分代码的库或应用,必须在所有公开的文档中「声明使用了 Safeify」, 4 | 同时,保留对 Safeify 的链接 https://github.com/Houfeng/safeify(在链接有变更时 5 | 需及时更新链接),保留 LICENSE 和 NOTICE 文件的全部内容。 6 | 7 | 2. 使用了 Safeify 全部或部分代码的库或应用,在所有公开的文章、博客及演讲中,应提及使用了 8 | Safeify,并放置 Safeify 相关链接。 9 | 10 | 3. 使用了 Safeify 全部或部分代码的库或应用,无论使用何种新的协议,所产生的包括但不限于 11 | 「代码、改进、创意、专利」,Safeify 都能无条件在后续版本中使用。 -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Safeify 2 | 3 |
4 | 5 | [![npm](https://img.shields.io/npm/l/safeify.svg)](LICENSE.md) 6 | [![NPM Version](https://img.shields.io/npm/v/safeify.svg)](https://www.npmjs.com/package/safeify) 7 | [![Build Status](https://www.travis-ci.org/Houfeng/safeify.svg?branch=master)](https://www.travis-ci.org/Houfeng/safeify) 8 | [![Coverage Status](https://coveralls.io/repos/github/Houfeng/safeify/badge.svg?branch=master)](https://coveralls.io/github/Houfeng/safeify?branch=master) 9 | [![npm](https://img.shields.io/npm/dt/safeify.svg)](https://www.npmjs.com/package/safeify) 10 | 11 |
12 | 13 | # 说明 14 | 15 | Safeify 可让 Node 应用安全的隔离执行非信任的用户自定义代码,[了解详细](//github.com/Houfeng/safeify/blob/master/DOC.md) 16 | 17 | # 安装 18 | 19 | ```sh 20 | npm install safeify -S 21 | ``` 22 | 23 | # 使用 24 | 25 | ```ts 26 | import { Safeify } from "safeify"; 27 | 28 | (async ()=>{ 29 | 30 | // 创建 safeify 实例 31 | const safeVm = new Safeify({ 32 | timeout: 3000, 33 | asyncTimeout: 60000 34 | }); 35 | 36 | // 定义 context 37 | const context = { 38 | a: 1, 39 | b: 2, 40 | system: { 41 | add(a: number, b: number) { 42 | return (a + b) * 2; 43 | } 44 | } 45 | }; 46 | 47 | // 执行动态代码 48 | const result= await safeVm.run(`return system.add(1,2)`, context); 49 | console.log('result', result); 50 | 51 | // 释放资源 52 | safeVm.destroy(); 53 | 54 | })(); 55 | ``` -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "safeify", 3 | "version": "5.0.6", 4 | "description": "", 5 | "main": "./lib/index.js", 6 | "typings": "./lib/index.d.ts", 7 | "scripts": { 8 | "test": "dn test", 9 | "cover": "nyc report --reporter=text-lcov | coveralls" 10 | }, 11 | "author": "", 12 | "license": "Apache License 2.0", 13 | "dependencies": { 14 | "debug": "^3.1.0", 15 | "mkdirp": "^0.5.1", 16 | "mz": "^2.7.0", 17 | "ntils": "^5.2.1", 18 | "vm2": "^3.9.3" 19 | }, 20 | "devDependencies": { 21 | "@types/debug": "^0.0.30", 22 | "@types/js-yaml": "^3.10.1", 23 | "@types/mkdirp": "^0.5.2", 24 | "@types/mz": "^0.0.32", 25 | "@types/node": "^9.4.5", 26 | "coveralls": "^3.0.0", 27 | "dawn": "^1.8.0", 28 | "dn-middleware-clean": "^1.0.2", 29 | "dn-middleware-copy": "^0.2.7", 30 | "dn-middleware-lint": "^2.1.3", 31 | "dn-middleware-shell": "^1.1.0", 32 | "dn-middleware-tslint": "^1.1.2", 33 | "dn-middleware-unit": "^0.2.0", 34 | "eslint-config-dawn": "^1.0.7", 35 | "ts-node": "^4.1.0", 36 | "typescript": "^3.2.2" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/CGroups.ts: -------------------------------------------------------------------------------- 1 | import * as os from "os"; 2 | import * as fs from "mz/fs"; 3 | 4 | const mkdirp = require("mkdirp"); 5 | 6 | const mkdir = async (dir: string) => { 7 | return new Promise((resolve, reject) => { 8 | mkdirp(dir, (err: any) => { 9 | if (err) return reject(err); 10 | resolve(dir); 11 | }); 12 | }); 13 | }; 14 | 15 | export interface IResources { 16 | [name: string]: any; 17 | } 18 | 19 | export class CGroups { 20 | public root: string; 21 | public name = ""; 22 | public resources: string[] = []; 23 | public platform: string; 24 | 25 | constructor(name: string, root = "/sys/fs/cgroup", platform = os.platform()) { 26 | this.root = root; 27 | this.name = name; 28 | this.platform = platform; 29 | } 30 | 31 | public set(resources: IResources) { 32 | if (this.platform !== "linux") return; 33 | const resList = Object.keys(resources); 34 | return Promise.all( 35 | resList.map(async resName => { 36 | if (!this.resources.some(res => res === resName)) { 37 | this.resources.push(resName); 38 | } 39 | const groupPath = `${this.root}/${resName}/${this.name}`; 40 | await mkdir(groupPath); 41 | const setting = resources[resName]; 42 | const settingNames = Object.keys(setting); 43 | return Promise.all( 44 | settingNames.map(name => { 45 | const value = setting[name]; 46 | const filename = `${groupPath}/${resName}.${name}`; 47 | return fs.writeFile(filename, String(value)); 48 | }) 49 | ); 50 | }) 51 | ); 52 | } 53 | 54 | public addProcess(pid: number) { 55 | if (this.platform !== "linux") return; 56 | return Promise.all( 57 | this.resources.map(resName => { 58 | const filename = `${this.root}/${resName}/${this.name}/tasks`; 59 | return fs.writeFile(filename, String(pid)); 60 | }) 61 | ); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/Call.ts: -------------------------------------------------------------------------------- 1 | import { ICallOptions } from "./ICallOptions"; 2 | 3 | const { newGuid } = require("ntils"); 4 | 5 | export class Call { 6 | public id: string; 7 | public name: string; 8 | public args: string; 9 | public result: any; 10 | public error: any; 11 | public defer: Promise; 12 | public resolve: Function; 13 | public reject: Function; 14 | 15 | constructor(options: ICallOptions) { 16 | Object.assign(this, options); 17 | this.id = newGuid(); 18 | this.defer = new Promise((resolve, reject) => { 19 | this.resolve = resolve; 20 | this.reject = reject; 21 | }); 22 | } 23 | 24 | toJSON() { 25 | // tslint:disable-next-line 26 | const { id, name, args, result, error } = this; 27 | return { id, name, args, result, error }; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/IAlias.ts: -------------------------------------------------------------------------------- 1 | export interface IAlias { 2 | [name: string]: string; 3 | } 4 | -------------------------------------------------------------------------------- /src/ICallOptions.ts: -------------------------------------------------------------------------------- 1 | export interface ICallOptions { 2 | name: string; 3 | args: any; 4 | } 5 | -------------------------------------------------------------------------------- /src/IMessage.ts: -------------------------------------------------------------------------------- 1 | import { MessageType } from "./MessageType"; 2 | 3 | export interface IMessage { 4 | type: MessageType; 5 | [key: string]: any; 6 | } 7 | -------------------------------------------------------------------------------- /src/ISafeifyOptions.ts: -------------------------------------------------------------------------------- 1 | import { IUnsafe } from "./IUnsafe"; 2 | 3 | export interface ISafeifyOptions { 4 | timeout?: number; 5 | asyncTimeout?: number; 6 | /** @deprecated use workerQuota */ 7 | quantity?: number; 8 | workers?: number; 9 | unrestricted?: boolean; 10 | memoryQuota?: number; 11 | cpuQuota?: number; 12 | greedy?: boolean; 13 | unsafe?: IUnsafe; 14 | } 15 | -------------------------------------------------------------------------------- /src/IScriptOptions.ts: -------------------------------------------------------------------------------- 1 | import { IUnsafe } from "./IUnsafe"; 2 | 3 | export interface IScriptOptions { 4 | code: string; 5 | timeout?: number; 6 | asyncTimeout?: number; 7 | sandbox?: any; 8 | unsafe?: IUnsafe; 9 | } 10 | -------------------------------------------------------------------------------- /src/IUnsafe.ts: -------------------------------------------------------------------------------- 1 | import { IAlias } from "./IAlias"; 2 | 3 | export interface IUnsafe { 4 | require?: boolean | string; 5 | modules?: string[] | IAlias | boolean; 6 | } 7 | -------------------------------------------------------------------------------- /src/MessageType.ts: -------------------------------------------------------------------------------- 1 | export enum MessageType { 2 | run = "run", 3 | done = "done", 4 | call = "call", 5 | ret = "ret", 6 | ready = "ready" 7 | } 8 | -------------------------------------------------------------------------------- /src/Proxy.ts: -------------------------------------------------------------------------------- 1 | const { isString } = require("ntils"); 2 | const callSymbol = "func://942ccb3b-a367-a650-9981-02e44a98a5e6/"; 3 | 4 | export function createCallProxy(name: string | string[]) { 5 | if (isString(name)) { 6 | return callSymbol + name; 7 | } else { 8 | return callSymbol + (name as string[]).join("."); 9 | } 10 | } 11 | 12 | export function isCallProxy(value: any) { 13 | if (!isString(value)) return false; 14 | return (value as string).startsWith(callSymbol); 15 | } 16 | 17 | export function getCallName(value: string) { 18 | return value.replace(callSymbol, ""); 19 | } 20 | -------------------------------------------------------------------------------- /src/Safeify.ts: -------------------------------------------------------------------------------- 1 | import * as os from "os"; 2 | import * as childProcess from "child_process"; 3 | import { ISafeifyOptions } from "./ISafeifyOptions"; 4 | import { CGroups } from "./CGroups"; 5 | import { Worker } from "./Worker"; 6 | import { MessageType } from "./MessageType"; 7 | import { IMessage } from "./IMessage"; 8 | import { Script } from "./Script"; 9 | import { WorkerState } from "./WorkerState"; 10 | 11 | const { isFunction, isNumber, getByPath } = require("ntils"); 12 | const log = require("debug")("safeify"); 13 | 14 | const defaultSandbox = Object.create(null); 15 | const defaultOptions: ISafeifyOptions = { 16 | timeout: 1000, 17 | asyncTimeout: 30000, 18 | workers: os.cpus().length, 19 | cpuQuota: 0.5, 20 | memoryQuota: 500 21 | }; 22 | const runnerFile = require.resolve("./runner"); 23 | /* istanbul ignore next */ 24 | const childExecArgv = (process.execArgv || []).map(flag => 25 | flag.includes("--inspect") ? "--inspect=0" : flag 26 | ); 27 | const instances: Safeify[] = []; 28 | 29 | process.once("exit", () => { 30 | instances.forEach(instance => instance.destroy()); 31 | }); 32 | 33 | export class Safeify { 34 | private options: ISafeifyOptions = {}; 35 | private workers: Worker[] = []; 36 | private pendingScripts: Script[] = []; 37 | private cgroups: CGroups = null; 38 | private inited = false; 39 | private presets: string[] = []; 40 | 41 | constructor(opts: ISafeifyOptions = {}) { 42 | instances.push(this); 43 | opts = this.normalize(opts); 44 | this.options = { ...defaultOptions, ...opts }; 45 | } 46 | 47 | private normalize(opts: ISafeifyOptions) { 48 | opts = { ...opts }; 49 | // 兼容旧的 quantity 参数 50 | if (!isNumber(opts.workers)) opts.workers = opts.quantity; 51 | // 校验参数 52 | if (!isNumber(opts.workers)) opts.workers = defaultOptions.workers; 53 | if (opts.workers < 2) opts.workers = 2; 54 | if (!isNumber(opts.timeout)) opts.timeout = 1000; 55 | if (!isNumber(opts.asyncTimeout) || opts.asyncTimeout < opts.timeout) { 56 | opts.asyncTimeout = opts.timeout * 3; 57 | } 58 | return opts; 59 | } 60 | 61 | public async init() { 62 | /* istanbul ignore if */ 63 | if (this.inited) return; 64 | this.inited = true; 65 | const { unrestricted } = this.options; 66 | /* istanbul ignore if */ 67 | if (!unrestricted) await this.createControlGroup(); 68 | await this.createWorkers(); 69 | } 70 | 71 | public destroy = () => { 72 | const index = instances.indexOf(this); 73 | if (index > -1) instances.splice(index, 1); 74 | this.workers.forEach(worker => this.destroyWorker(worker)); 75 | this.workers = []; 76 | }; 77 | 78 | /** 79 | * @deprecated Please use destroy' instead of 'distory' 80 | */ 81 | public distory = () => { 82 | console.warn("deprecated:", `Please use 'destroy' instead of 'distory'`); 83 | return this.destroy(); 84 | }; 85 | 86 | private destroyWorker(worker: Worker) { 87 | worker.state = WorkerState.unhealthy; 88 | worker.runningScripts.forEach(script => script.stop()); 89 | worker.process.removeAllListeners("message"); 90 | worker.process.removeAllListeners("disconnect"); 91 | if (worker.process.connected) worker.process.disconnect(); 92 | if (!worker.process.killed) worker.process.kill("SIGKILL"); 93 | } 94 | 95 | get workerTotal() { 96 | return this.workers.length; 97 | } 98 | 99 | get pendingTotal() { 100 | return this.pendingScripts.length; 101 | } 102 | 103 | get runningTotal() { 104 | return this.workers.reduce((count, worker: Worker) => { 105 | return count + worker.runningScripts.length; 106 | }, 0); 107 | } 108 | 109 | private async onWokerCall(message: IMessage) { 110 | const { call, pid, scriptId } = message; 111 | /* istanbul ignore if */ 112 | if (!call) return; 113 | const worker = this.workers.find(item => item.process.pid === pid); 114 | /* istanbul ignore if */ 115 | if (!worker) return; 116 | const script = worker.runningScripts.find(item => item.id === scriptId); 117 | /* istanbul ignore if */ 118 | if (!script) return; 119 | try { 120 | const breadcrumb = call.name.split("."); 121 | const name = breadcrumb.pop(); 122 | const context = getByPath(script.sandbox, breadcrumb) || defaultSandbox; 123 | call.result = await context[name](...call.args); 124 | } catch (err) { 125 | call.error = err.message; 126 | } 127 | const type = MessageType.ret; 128 | worker.process.send({ type, call }); 129 | } 130 | 131 | private onWorkerMessage = (message: IMessage) => { 132 | switch (message.type) { 133 | case MessageType.done: 134 | return this.onWorkerDone(message); 135 | case MessageType.call: 136 | return this.onWokerCall(message); 137 | } 138 | }; 139 | 140 | private onWorkerDone(message: IMessage) { 141 | const { pid, script } = message; 142 | const worker = this.workers.find(item => item.process.pid === pid); 143 | this.handleScriptDone(worker, script, false); 144 | } 145 | 146 | private async handleScriptDone(worker: Worker, script: any, kill: boolean) { 147 | if (!worker || !script) return; 148 | if (kill) { 149 | worker.state = WorkerState.unhealthy; 150 | await this.alignmentWorker(); 151 | } else { 152 | worker.stats--; 153 | } 154 | if (this.pendingScripts.length > 0) this.execute(); 155 | this.handleScriptResult(worker, script); 156 | } 157 | 158 | private handleScriptResult(worker: Worker, script: Script) { 159 | const runningIndex = worker.runningScripts.findIndex( 160 | item => item.id === script.id 161 | ); 162 | /* istanbul ignore if */ 163 | if (runningIndex < 0) return; 164 | const runningScript = worker.runningScripts.splice(runningIndex, 1)[0]; 165 | /* istanbul ignore if */ 166 | if (!runningScript) return; 167 | runningScript.stop(); 168 | if (script.error) { 169 | runningScript.reject(new Error(script.error)); 170 | log("onWorkerDone error", script.id, script.error); 171 | } else { 172 | runningScript.resolve(script.result); 173 | log("onWorkerDone result", script.id, script.result); 174 | } 175 | } 176 | 177 | private alignmentWorker = async () => { 178 | const healthyWorkers: Worker[] = []; 179 | const unhealthyWorkers: Worker[] = []; 180 | this.workers.forEach(item => { 181 | if (item.state === WorkerState.healthy && item.process.connected) { 182 | healthyWorkers.push(item); 183 | } else { 184 | unhealthyWorkers.push(item); 185 | } 186 | }); 187 | this.workers = healthyWorkers; 188 | await this.createWorkers(); 189 | if (this.pendingScripts.length > 0) this.execute(); 190 | unhealthyWorkers.forEach(item => this.destroyWorker(item)); 191 | }; 192 | 193 | /* istanbul ignore next */ 194 | private onWorkerDisconnect = async () => { 195 | log("onWorkerDisconnect", "pendingScripts", this.pendingScripts.length); 196 | await this.alignmentWorker(); 197 | }; 198 | 199 | private createControlGroup() { 200 | this.cgroups = new CGroups("safeify"); 201 | const { cpuQuota, memoryQuota } = this.options; 202 | return this.cgroups.set({ 203 | cpu: { cfs_quota_us: 100000 * cpuQuota }, 204 | memory: { limit_in_bytes: 1048576 * memoryQuota } 205 | }); 206 | } 207 | 208 | private async createWorker(): Promise { 209 | const { unrestricted } = this.options; 210 | const workerProcess = childProcess.fork(runnerFile, [], { 211 | execArgv: childExecArgv 212 | }); 213 | if (!unrestricted) await this.cgroups.addProcess(workerProcess.pid); 214 | return new Promise(resolve => { 215 | workerProcess.once("message", (message: IMessage) => { 216 | /* istanbul ignore if */ 217 | if (!message || message.type !== MessageType.ready) return; 218 | workerProcess.on("message", this.onWorkerMessage); 219 | workerProcess.on("disconnect", this.onWorkerDisconnect); 220 | resolve(new Worker(workerProcess)); 221 | }); 222 | }); 223 | } 224 | 225 | private get healthyWorkers() { 226 | return this.workers.filter( 227 | worker => worker.process.connected && worker.state === WorkerState.healthy 228 | ); 229 | } 230 | 231 | private async createWorkers() { 232 | const num = this.options.workers - this.healthyWorkers.length; 233 | const workers = []; 234 | for (let i = 0; i < num; i++) { 235 | workers.push( 236 | (async () => { 237 | const worker = await this.createWorker(); 238 | this.workers.push(worker); 239 | return worker; 240 | })() 241 | ); 242 | } 243 | return Promise.all(workers); 244 | } 245 | 246 | private execute() { 247 | const worker = 248 | this.options.greedy === false 249 | ? this.healthyWorkers.find(w => w.stats < 1) 250 | : this.healthyWorkers.sort((a, b) => a.stats - b.stats)[0]; 251 | /* istanbul ignore if */ 252 | if (!worker) return; 253 | log("execute pid", worker.process.pid); 254 | const script = this.pendingScripts.shift(); 255 | /* istanbul ignore if */ 256 | if (!script) return; 257 | worker.stats++; 258 | worker.runningScripts.push(script); 259 | log("execute code", script.code); 260 | script.start(() => this.onScriptAsyncTimeout(worker, script)); 261 | worker.process.send({ type: MessageType.run, script }); 262 | } 263 | 264 | private onScriptAsyncTimeout = (worker: Worker, script: Script) => { 265 | worker.runningScripts.forEach(item => { 266 | if (item.id === script.id) return; 267 | this.pendingScripts.unshift(item.stop()); 268 | }); 269 | script.error = "Script execution timed out."; 270 | this.handleScriptDone(worker, script, true); 271 | }; 272 | 273 | private toCode(code: string | Function): string { 274 | if (!code) return ";"; 275 | if (isFunction(code)) { 276 | const result = /\{([\s\S]*)\}/.exec(code.toString()); 277 | return result[1] || ""; 278 | } else { 279 | return code.toString(); 280 | } 281 | } 282 | 283 | public preset(code: string | Function) { 284 | this.presets.push(this.toCode(code)); 285 | } 286 | 287 | public async run(code: string | Function, sandbox?: any) { 288 | await this.init(); 289 | code = [...this.presets, this.toCode(code), os.EOL].join(";"); 290 | log("run", code); 291 | const { timeout, asyncTimeout, unsafe } = this.options; 292 | const script = new Script({ code, timeout, asyncTimeout, sandbox, unsafe }); 293 | this.pendingScripts.push(script); 294 | this.execute(); 295 | return script.defer; 296 | } 297 | } 298 | -------------------------------------------------------------------------------- /src/Script.ts: -------------------------------------------------------------------------------- 1 | import { IScriptOptions } from "./IScriptOptions"; 2 | import { createCallProxy } from "./Proxy"; 3 | import { IUnsafe } from "./IUnsafe"; 4 | 5 | const IGNORE_PARAM = /^\_\_/; 6 | const { 7 | newGuid, 8 | each, 9 | isFunction, 10 | isObject, 11 | isArray, 12 | isDate 13 | } = require("ntils"); 14 | 15 | function convertParams(sandbox: any, breadcrumb: string[] = []) { 16 | const result = Object.create(null); 17 | each(sandbox, (name: string, value: any) => { 18 | if (IGNORE_PARAM.test(name)) return; 19 | const currentBreadcrumb = [...breadcrumb, name]; 20 | if (isFunction(value)) { 21 | result[name] = createCallProxy(currentBreadcrumb); 22 | } else if (isObject(value) && !isArray(value) && !isDate(value)) { 23 | result[name] = convertParams(value, currentBreadcrumb); 24 | } else { 25 | result[name] = value; 26 | } 27 | }); 28 | return result; 29 | } 30 | 31 | export class Script { 32 | public id: string; 33 | public code: string; 34 | public sandbox: any; 35 | public result: any; 36 | public error: any; 37 | public timeout: number; 38 | public asyncTimeout: number; 39 | private timer: number; 40 | public defer: Promise; 41 | public resolve: (value?: any) => void; 42 | public reject: (value?: any) => void; 43 | public params: any; 44 | public unsafe: IUnsafe; 45 | 46 | constructor(options: IScriptOptions) { 47 | Object.assign(this, options); 48 | this.id = newGuid(); 49 | if (!this.sandbox) this.sandbox = {}; 50 | this.params = convertParams(this.sandbox); 51 | this.defer = new Promise((resolve, reject) => { 52 | this.resolve = resolve; 53 | this.reject = reject; 54 | }); 55 | } 56 | 57 | start(fn: Function) { 58 | this.timer = setTimeout(fn, this.asyncTimeout); 59 | return this; 60 | } 61 | 62 | stop() { 63 | clearTimeout(this.timer); 64 | return this; 65 | } 66 | 67 | toJSON() { 68 | // tslint:disable-next-line 69 | const { 70 | id, 71 | code, 72 | result, 73 | error, 74 | timeout, 75 | asyncTimeout, 76 | params, 77 | unsafe 78 | } = this; 79 | return { id, code, result, error, timeout, asyncTimeout, params, unsafe }; 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/Worker.ts: -------------------------------------------------------------------------------- 1 | import { ChildProcess } from "child_process"; 2 | import { WorkerState } from "./WorkerState"; 3 | import { Script } from "./Script"; 4 | 5 | export class Worker { 6 | process: ChildProcess; 7 | stats: number; 8 | state: WorkerState; 9 | runningScripts: Script[]; 10 | constructor(process: ChildProcess) { 11 | this.process = process; 12 | this.state = WorkerState.healthy; 13 | this.stats = 0; 14 | this.runningScripts = []; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/WorkerState.ts: -------------------------------------------------------------------------------- 1 | export enum WorkerState { 2 | healthy = "healthy", 3 | unhealthy = "unhealthy" 4 | } 5 | -------------------------------------------------------------------------------- /src/debug.ts: -------------------------------------------------------------------------------- 1 | import { Safeify } from "./Safeify"; 2 | 3 | console.time("debug"); 4 | 5 | (async () => { 6 | console.log("初始化"); 7 | const safeVm = new Safeify({ 8 | timeout: 1000, 9 | asyncTimeout: 3000 10 | }); 11 | 12 | await safeVm.init(); 13 | 14 | console.log("workers", safeVm.workerTotal); 15 | 16 | const context = { 17 | a: 1, 18 | b: 2, 19 | system: { 20 | add(a: number, b: number) { 21 | return (a + b) * 2; 22 | } 23 | } 24 | }; 25 | 26 | console.time("测试"); 27 | try { 28 | await Promise.all( 29 | new Array(1) 30 | .fill(1) 31 | .map(() => safeVm.run(`return system.add(1,2)`, context)) 32 | ); 33 | console.log("成功"); 34 | } catch (err) { 35 | console.log("失败", err.stack); 36 | } 37 | console.timeEnd("测试"); 38 | await safeVm.destroy(); 39 | console.log("结束"); 40 | })(); 41 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { Safeify } from "./Safeify"; 2 | export * from "./Safeify"; 3 | export default Safeify; 4 | -------------------------------------------------------------------------------- /src/runner.ts: -------------------------------------------------------------------------------- 1 | import { VM } from "vm2"; 2 | import { MessageType } from "./MessageType"; 3 | import { IMessage } from "./IMessage"; 4 | import { Script } from "./Script"; 5 | import { isCallProxy, getCallName } from "./Proxy"; 6 | import { Call } from "./Call"; 7 | import { IUnsafe } from "./IUnsafe"; 8 | import { IAlias } from "./IAlias"; 9 | 10 | const { each, isObject, isString, isArray, isDate } = require("ntils"); 11 | 12 | const pendingCalls: Call[] = []; 13 | 14 | function wrapCode(code: string) { 15 | return `(async function(Buffer){${code}})(undefined)`; 16 | } 17 | 18 | function sendResult(message: any) { 19 | const pid = process.pid; 20 | const type = MessageType.done; 21 | const script = message.script; 22 | script.code = script.sandbox = null; 23 | process.send({ pid, type, ...message }); 24 | } 25 | 26 | function createProxyFunc(scriptId: string, value: string) { 27 | const type = MessageType.call; 28 | const pid = process.pid; 29 | return (...args: any[]) => { 30 | const name = getCallName(value); 31 | const call = new Call({ name, args }); 32 | pendingCalls.push(call); 33 | process.send({ pid, scriptId, type, call }); 34 | return call.defer; 35 | }; 36 | } 37 | 38 | function receiveCallRet(call: Call) { 39 | const pendingIndex = pendingCalls.findIndex(item => item.id === call.id); 40 | if (pendingIndex > -1) { 41 | const pendingCall = pendingCalls.splice(pendingIndex, 1)[0]; 42 | if (pendingCall) { 43 | if (call.error) { 44 | pendingCall.reject(new Error(call.error)); 45 | } else { 46 | pendingCall.resolve(call.result); 47 | } 48 | } 49 | } 50 | } 51 | 52 | function convertParams(scriptId: string, params: any) { 53 | const result = Object.create(null); 54 | each(params, (name: string, value: any) => { 55 | if (isCallProxy(value)) { 56 | result[name] = createProxyFunc(scriptId, value); 57 | } else if (isObject(value) && !isArray(value) && !isDate(value)) { 58 | result[name] = convertParams(scriptId, value); 59 | } else { 60 | result[name] = value; 61 | } 62 | }); 63 | return result; 64 | } 65 | 66 | function attchUnsafe(sandbox: any, unsafe: IUnsafe) { 67 | const { require: req, modules = [] } = unsafe; 68 | if (req === false) return sandbox; 69 | const method = isString(req) ? req : "require"; 70 | sandbox[method] = (name: string) => { 71 | if (modules === true) return require(name); 72 | if (isArray(modules) && (modules).includes(name)) { 73 | return require(name); 74 | } 75 | if ((modules)[name]) return require((modules)[name]); 76 | return null; 77 | }; 78 | } 79 | 80 | async function run(script: Script) { 81 | const { timeout, code, params, unsafe } = script; 82 | const sandbox = convertParams(script.id, params); 83 | if (unsafe) attchUnsafe(sandbox, unsafe); 84 | const vm = new VM({ sandbox, timeout }); 85 | try { 86 | script.result = await vm.run(wrapCode(code)); 87 | } catch (err) { 88 | script.error = err.message; 89 | } 90 | sendResult({ script }); 91 | } 92 | 93 | process.on("message", (message: IMessage) => { 94 | switch (message.type) { 95 | case MessageType.run: 96 | return run(message.script); 97 | case MessageType.ret: 98 | return receiveCallRet(message.call); 99 | } 100 | }); 101 | 102 | // 发送 ready 消息 103 | process.send({ type: MessageType.ready }); 104 | -------------------------------------------------------------------------------- /test/cgroups.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert'; 2 | import * as os from 'os'; 3 | import * as fs from 'fs'; 4 | import { CGroups } from "../src/CGroups"; 5 | 6 | const tmpDir = os.tmpdir(), platform = 'linux'; 7 | 8 | describe('CGroups', function () { 9 | 10 | it('set', async function () { 11 | const cgroups = new CGroups('test', tmpDir, platform); 12 | await cgroups.set({ 13 | cpu: { cfs_quota_us: 100000 }, 14 | memory: { limit_in_bytes: 1048576 } 15 | }); 16 | assert.deepEqual(['cpu', 'memory'], cgroups.resources); 17 | }); 18 | 19 | it('addProcess', async function () { 20 | const cgroups = new CGroups('test', tmpDir, platform); 21 | await cgroups.set({ 22 | cpu: { cfs_quota_us: 100000 } 23 | }); 24 | await cgroups.addProcess(123); 25 | const filename = `${cgroups.root}/cpu/${cgroups.name}/tasks`; 26 | const text = fs.readFileSync(filename).toString(); 27 | assert.equal('123', text); 28 | }); 29 | 30 | }); -------------------------------------------------------------------------------- /test/proxy.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert'; 2 | import { createCallProxy, isCallProxy } from "../src/Proxy"; 3 | 4 | describe('Proxy', function () { 5 | 6 | it('createCallProxy: string', async function () { 7 | const proxySymbol = createCallProxy('test') 8 | assert.equal(proxySymbol, 9 | 'func://942ccb3b-a367-a650-9981-02e44a98a5e6/test'); 10 | }); 11 | 12 | it('createCallProxy: array', async function () { 13 | const proxySymbol = createCallProxy(['test', 'test']) 14 | assert.equal(proxySymbol, 15 | 'func://942ccb3b-a367-a650-9981-02e44a98a5e6/test.test'); 16 | }); 17 | 18 | it('isCallProxy: true', async function () { 19 | const result = isCallProxy( 20 | 'func://942ccb3b-a367-a650-9981-02e44a98a5e6/test.test' 21 | ) 22 | assert.equal(result, true); 23 | }); 24 | 25 | it('isCallProxy: false1', async function () { 26 | const result = isCallProxy('test') 27 | assert.equal(result, false); 28 | }); 29 | 30 | it('isCallProxy: false2', async function () { 31 | const result = isCallProxy({}) 32 | assert.equal(result, false); 33 | }); 34 | 35 | }); -------------------------------------------------------------------------------- /test/run.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert'; 2 | 3 | import { Safeify } from "../src"; 4 | 5 | const context = { 6 | a: 1, 7 | b: 2, 8 | __ignore: 1, 9 | getTime: function () { 10 | return 2; 11 | }, 12 | error: function () { 13 | throw new Error('this is a error') 14 | }, 15 | system: { 16 | calc(a: number, b: number, time: number) { 17 | return (a + b) * (time || 1); 18 | }, 19 | get(a: number) { 20 | return new Promise(resolve => setTimeout(() => resolve(a), 100)); 21 | } 22 | } 23 | }; 24 | 25 | describe('Safeify', function () { 26 | 27 | it('run: success', async function () { 28 | const safeVm = new Safeify({ 29 | timeout: 500, 30 | asyncTimeout: 5000, 31 | unrestricted: true 32 | }); 33 | await safeVm.init(); 34 | const result = await safeVm.run( 35 | `return system.calc(a,b, await getTime())`, context 36 | ); 37 | await safeVm.destroy(); 38 | assert.equal(6, result); 39 | }); 40 | 41 | it('run: error', async function () { 42 | const safeVm = new Safeify({ 43 | timeout: 500, 44 | asyncTimeout: 5000, 45 | unrestricted: true 46 | }); 47 | await safeVm.init(); 48 | let error; 49 | try { 50 | await safeVm.run(`return system.calc(a,d)`, context); 51 | } catch (err) { 52 | error = err.message; 53 | } 54 | await safeVm.destroy(); 55 | assert.equal('d is not defined', error); 56 | }); 57 | 58 | 59 | it('run: call error', async function () { 60 | const safeVm = new Safeify({ 61 | timeout: 500, 62 | asyncTimeout: 5000, 63 | unrestricted: true 64 | }); 65 | await safeVm.init(); 66 | let error; 67 | try { 68 | await safeVm.run(`return error()`, context); 69 | } catch (err) { 70 | error = err.message; 71 | } 72 | await safeVm.destroy(); 73 | assert.equal('this is a error', error); 74 | }); 75 | 76 | it('run: sync timeout', async function () { 77 | const safeVm = new Safeify({ 78 | timeout: 500, 79 | asyncTimeout: 5000, 80 | unrestricted: true 81 | }); 82 | await safeVm.init(); 83 | let error: string; 84 | try { 85 | await safeVm.run(`while(true);`, context); 86 | } catch (err) { 87 | error = err.message; 88 | } 89 | await safeVm.destroy(); 90 | assert.equal(error.includes('Script execution timed out'), true); 91 | }); 92 | 93 | it('run: async timeout', async function () { 94 | const safeVm = new Safeify({ 95 | timeout: 500, 96 | asyncTimeout: 500, 97 | unrestricted: true 98 | }); 99 | await safeVm.init(); 100 | let error; 101 | try { 102 | await safeVm.run(`return new Promise(()=>{})`, context); 103 | } catch (err) { 104 | error = err.message; 105 | } 106 | await safeVm.destroy(); 107 | assert.equal('Script execution timed out.', error); 108 | }); 109 | 110 | 111 | it('run: ignore', async function () { 112 | const safeVm = new Safeify({ 113 | timeout: 500, 114 | asyncTimeout: 5000, 115 | unrestricted: true 116 | }); 117 | await safeVm.init(); 118 | let error; 119 | try { 120 | await safeVm.run(`return __ignore`, context); 121 | } catch (err) { 122 | error = err.message; 123 | } 124 | await safeVm.destroy(); 125 | assert.equal('__ignore is not defined', error); 126 | }); 127 | 128 | it('run: blank', async function () { 129 | const safeVm = new Safeify({ 130 | timeout: 500, 131 | asyncTimeout: 5000, 132 | unrestricted: true 133 | }); 134 | await safeVm.init(); 135 | const result = await safeVm.run(''); 136 | await safeVm.destroy(); 137 | assert.equal(undefined, result); 138 | }); 139 | 140 | it('run: function', async function () { 141 | const safeVm = new Safeify({ 142 | timeout: 500, 143 | asyncTimeout: 5000, 144 | unrestricted: true 145 | }); 146 | await safeVm.init(); 147 | const result = await safeVm.run(function (system: any) { 148 | return system.calc(1, 2); 149 | }, context); 150 | await safeVm.destroy(); 151 | assert.equal(3, result); 152 | }); 153 | 154 | it('run: preset', async function () { 155 | const safeVm = new Safeify({ 156 | timeout: 500, 157 | asyncTimeout: 5000, 158 | unrestricted: true 159 | }); 160 | await safeVm.init(); 161 | safeVm.preset(function (system: any) { 162 | const calc = system.calc; 163 | }); 164 | const result = await safeVm.run(`return calc(1,2)`, context); 165 | await safeVm.destroy(); 166 | assert.equal(3, result); 167 | }); 168 | 169 | it('run: worker', async function () { 170 | const safeVm = new Safeify({ 171 | timeout: 500, 172 | asyncTimeout: 5000, 173 | unrestricted: true, 174 | quantity: 1, 175 | }); 176 | await safeVm.init(); 177 | assert.equal(2, safeVm.workerTotal); 178 | assert.equal(0, safeVm.pendingTotal); 179 | assert.equal(0, safeVm.runningTotal); 180 | const result = await safeVm.run(function (system: any) { 181 | return system.get(1); 182 | }, context); 183 | await safeVm.destroy(); 184 | assert.equal(1, result); 185 | assert.equal(0, safeVm.runningTotal); 186 | }); 187 | 188 | it('run: loop in micro-task', async function () { 189 | const safeVm = new Safeify({ 190 | timeout: 500, 191 | asyncTimeout: 500, 192 | unrestricted: true 193 | }); 194 | await safeVm.init(); 195 | let error; 196 | try { 197 | await safeVm.run( 198 | `return Promise.resolve().then(()=>{while(true);})`, context 199 | ); 200 | } catch (err) { 201 | error = err.message; 202 | } 203 | await safeVm.destroy(); 204 | assert.equal('Script execution timed out.', error); 205 | }); 206 | 207 | it('run: unsafe named', async function () { 208 | const safeVm = new Safeify({ 209 | timeout: 500, 210 | asyncTimeout: 500, 211 | unrestricted: true, 212 | unsafe: { 213 | require: 'xxx', 214 | modules: ['ntils'] 215 | } 216 | }); 217 | await safeVm.init(); 218 | let result 219 | try { 220 | result = await safeVm.run(`return xxx('ntils').isNumber(1)`, context); 221 | } catch (err) { 222 | console.log(err.message); 223 | } 224 | await safeVm.destroy(); 225 | assert.equal(1, result); 226 | }); 227 | 228 | it('run: unsafe default', async function () { 229 | const safeVm = new Safeify({ 230 | timeout: 500, 231 | asyncTimeout: 500, 232 | unrestricted: true, 233 | unsafe: { 234 | modules: ['ntils'] 235 | } 236 | }); 237 | await safeVm.init(); 238 | let result 239 | try { 240 | result = await safeVm.run(`return require('ntils').isNumber(1)`, context); 241 | } catch (err) { 242 | console.log(err.message); 243 | } 244 | await safeVm.destroy(); 245 | assert.equal(1, result); 246 | }); 247 | 248 | it('run: unsafe error', async function () { 249 | const safeVm = new Safeify({ 250 | timeout: 500, 251 | asyncTimeout: 500, 252 | unrestricted: true, 253 | unsafe: { 254 | modules: [] 255 | } 256 | }); 257 | await safeVm.init(); 258 | let error 259 | try { 260 | await safeVm.run(`return require('ntils').isNumber(1)`, context); 261 | } catch (err) { 262 | error = err.message; 263 | } 264 | await safeVm.destroy(); 265 | assert.equal('Cannot read property \'isNumber\' of null', error); 266 | }); 267 | 268 | it('run: unsafe any modules', async function () { 269 | const safeVm = new Safeify({ 270 | timeout: 500, 271 | asyncTimeout: 500, 272 | unrestricted: true, 273 | unsafe: { 274 | modules: true 275 | } 276 | }); 277 | await safeVm.init(); 278 | let result 279 | try { 280 | result = await safeVm.run(`return require('ntils').isNumber(1)`, context); 281 | } catch (err) { 282 | console.log(err.message); 283 | } 284 | await safeVm.destroy(); 285 | assert.equal(1, result); 286 | }); 287 | 288 | it('run: unsafe by alias', async function () { 289 | const safeVm = new Safeify({ 290 | timeout: 500, 291 | asyncTimeout: 500, 292 | unrestricted: true, 293 | unsafe: { 294 | modules: { 295 | 'n': 'ntils' 296 | } 297 | } 298 | }); 299 | await safeVm.init(); 300 | let result 301 | try { 302 | result = await safeVm.run(`return require('n').isNumber(1)`, context); 303 | } catch (err) { 304 | console.log(err.message); 305 | } 306 | await safeVm.destroy(); 307 | assert.equal(1, result); 308 | }); 309 | 310 | it('run: evoke new workers', async function () { 311 | const safeVm = new Safeify({ 312 | timeout: 500, 313 | asyncTimeout: 500, 314 | unrestricted: true, 315 | workers: 2, 316 | }); 317 | await safeVm.init(); 318 | let result 319 | for (let i = 0; i < 4; i++) { 320 | try { 321 | await safeVm.run(`return new Promise(()=>{})`, context); 322 | } catch (err) { 323 | console.log(err.message); 324 | } 325 | } 326 | assert.equal(2, safeVm.workerTotal); 327 | try { 328 | result = await safeVm.run(`return true`, context); 329 | } catch (err) { 330 | console.log(err.message); 331 | } 332 | assert.equal(2, safeVm.workerTotal); 333 | await safeVm.destroy(); 334 | assert.equal(true, result); 335 | }); 336 | 337 | }); -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "./lib/", 4 | "sourceMap": true, 5 | "noImplicitAny": true, 6 | "module": "commonjs", 7 | "target": "es2017", 8 | "experimentalDecorators": true, 9 | "removeComments": true, 10 | "esModuleInterop": true, 11 | "moduleResolution": "node", 12 | "lib": [ 13 | "esnext", 14 | "dom" 15 | ] 16 | }, 17 | "include": [ 18 | "./src/**/*.ts" 19 | ] 20 | } -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "tslint-config-dawn" 4 | ] 5 | } --------------------------------------------------------------------------------