isolated-function 0.0.3 → 0.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -5
- package/package.json +3 -4
- package/src/compile.js +21 -15
- package/src/index.js +69 -23
- package/src/template/index.js +19 -32
package/README.md
CHANGED
|
@@ -6,8 +6,9 @@
|
|
|
6
6
|
|
|
7
7
|
**Highlights**
|
|
8
8
|
|
|
9
|
-
- Based
|
|
9
|
+
- Based in [Node.js Permission Model](https://nodejs.org/api/permissions.html#permission-model)
|
|
10
10
|
- Auto install npm dependencies.
|
|
11
|
+
- Memory limit support.
|
|
11
12
|
- Timeout support.
|
|
12
13
|
|
|
13
14
|
## Install
|
|
@@ -19,10 +20,10 @@ npm install isolated-function --save
|
|
|
19
20
|
## Usage
|
|
20
21
|
|
|
21
22
|
```js
|
|
22
|
-
const
|
|
23
|
+
const isolatedFunction = require('isolated-function')
|
|
23
24
|
|
|
24
25
|
/* This function will run in a sandbox, in a separate process */
|
|
25
|
-
const sum =
|
|
26
|
+
const sum = isolatedFunction((y, z) => y + z)
|
|
26
27
|
|
|
27
28
|
/* Interact with it as usual from your main code */
|
|
28
29
|
const result = await sum(3, 2)
|
|
@@ -33,7 +34,7 @@ console.log(result)
|
|
|
33
34
|
You can also use `require' for external dependencies:
|
|
34
35
|
|
|
35
36
|
```js
|
|
36
|
-
const isEmoji =
|
|
37
|
+
const isEmoji = isolatedFunction(emoji => {
|
|
37
38
|
const isEmoji = require('is-standard-emoji')
|
|
38
39
|
return isEmoji(emoji)
|
|
39
40
|
})
|
|
@@ -48,7 +49,7 @@ It's intentionally not possible to expose any Node.js objects or functions direc
|
|
|
48
49
|
|
|
49
50
|
## API
|
|
50
51
|
|
|
51
|
-
###
|
|
52
|
+
### isolatedFunction(snippet, [options])
|
|
52
53
|
|
|
53
54
|
#### snippet
|
|
54
55
|
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "isolated-function",
|
|
3
3
|
"description": "Runs untrusted code in a Node.js v8 sandbox.",
|
|
4
4
|
"homepage": "https://github.com/Kikobeats/isolated-function",
|
|
5
|
-
"version": "0.0.
|
|
5
|
+
"version": "0.0.5",
|
|
6
6
|
"main": "src/index.js",
|
|
7
7
|
"exports": {
|
|
8
8
|
".": "./src/index.js"
|
|
@@ -33,14 +33,13 @@
|
|
|
33
33
|
"v8"
|
|
34
34
|
],
|
|
35
35
|
"dependencies": {
|
|
36
|
+
"@kikobeats/time-span": "~1.0.5",
|
|
36
37
|
"acorn": "~8.12.1",
|
|
37
38
|
"acorn-walk": "~8.3.3",
|
|
38
39
|
"ensure-error": "~3.0.1",
|
|
39
40
|
"esbuild": "~0.23.1",
|
|
40
|
-
"process-stats": "~3.7.10",
|
|
41
41
|
"serialize-error": "8",
|
|
42
|
-
"tinyspawn": "~1.3.2"
|
|
43
|
-
"v8-sandbox": "~3.2.12"
|
|
42
|
+
"tinyspawn": "~1.3.2"
|
|
44
43
|
},
|
|
45
44
|
"devDependencies": {
|
|
46
45
|
"@commitlint/cli": "latest",
|
package/src/compile.js
CHANGED
|
@@ -11,6 +11,8 @@ const path = require('path')
|
|
|
11
11
|
|
|
12
12
|
const generateTemplate = require('./template')
|
|
13
13
|
|
|
14
|
+
const MINIFY = process.env.ISOLATED_FUNCTIONS_MINIFY !== 'false'
|
|
15
|
+
|
|
14
16
|
const packageManager = (() => {
|
|
15
17
|
try {
|
|
16
18
|
execSync('which pnpm').toString().trim()
|
|
@@ -46,31 +48,35 @@ const detectDependencies = code => {
|
|
|
46
48
|
return Array.from(dependencies)
|
|
47
49
|
}
|
|
48
50
|
|
|
49
|
-
|
|
50
|
-
const
|
|
51
|
-
await fs.mkdir(
|
|
51
|
+
const getTmp = async content => {
|
|
52
|
+
const cwd = await fs.mkdtemp(path.join(tmpdir(), 'compile-'))
|
|
53
|
+
await fs.mkdir(cwd, { recursive: true })
|
|
52
54
|
|
|
53
|
-
const
|
|
55
|
+
const filepath = path.join(cwd, 'index.js')
|
|
56
|
+
await fs.writeFile(filepath, content)
|
|
54
57
|
|
|
55
|
-
const
|
|
56
|
-
|
|
58
|
+
const cleanup = () => fs.rm(cwd, { recursive: true, force: true })
|
|
59
|
+
return { filepath, cwd, content, cleanup }
|
|
60
|
+
}
|
|
57
61
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
62
|
+
module.exports = async snippet => {
|
|
63
|
+
const tmp = await getTmp(generateTemplate(snippet))
|
|
64
|
+
const dependencies = detectDependencies(tmp.content)
|
|
65
|
+
await $(packageManager.init, { cwd: tmp.cwd })
|
|
66
|
+
await $(`${packageManager.install} ${dependencies.join(' ')}`, {
|
|
67
|
+
cwd: tmp.cwd
|
|
68
|
+
})
|
|
61
69
|
|
|
62
70
|
const result = await esbuild.build({
|
|
63
|
-
entryPoints: [
|
|
71
|
+
entryPoints: [tmp.filepath],
|
|
64
72
|
bundle: true,
|
|
73
|
+
minify: MINIFY,
|
|
65
74
|
write: false,
|
|
66
75
|
platform: 'node'
|
|
67
76
|
})
|
|
68
77
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
await fs.rm(tmp, { recursive: true })
|
|
72
|
-
|
|
73
|
-
return bundledCode
|
|
78
|
+
await tmp.cleanup()
|
|
79
|
+
return getTmp(result.outputFiles[0].text)
|
|
74
80
|
}
|
|
75
81
|
|
|
76
82
|
module.exports.detectDependencies = detectDependencies
|
package/src/index.js
CHANGED
|
@@ -1,32 +1,78 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
|
-
const { Sandbox } = require('v8-sandbox')
|
|
4
3
|
const { deserializeError } = require('serialize-error')
|
|
4
|
+
const timeSpan = require('@kikobeats/time-span')()
|
|
5
|
+
const $ = require('tinyspawn')
|
|
6
|
+
const path = require('path')
|
|
5
7
|
|
|
6
8
|
const compile = require('./compile')
|
|
7
9
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
const createError = ({ name, message, ...props }) => {
|
|
11
|
+
const error = new Error(message)
|
|
12
|
+
error.name = name
|
|
13
|
+
Object.assign(error, props)
|
|
14
|
+
return error
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const flags = ({ filename, memory }) => {
|
|
18
|
+
const flags = ['--experimental-permission', `--allow-fs-read=${filename}`]
|
|
19
|
+
if (memory) flags.push(`--max-old-space-size=${memory}`)
|
|
20
|
+
return flags.join(' ')
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
module.exports = (snippet, { timeout = 0, memory } = {}) => {
|
|
24
|
+
if (typeof snippet !== 'function') throw new TypeError('Expected a function')
|
|
25
|
+
const compilePromise = compile(snippet)
|
|
26
|
+
|
|
27
|
+
const fn = async (...args) => {
|
|
28
|
+
let duration
|
|
29
|
+
try {
|
|
30
|
+
const { filepath } = await compilePromise
|
|
12
31
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
})
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
32
|
+
const cwd = path.dirname(filepath)
|
|
33
|
+
const filename = path.basename(filepath)
|
|
34
|
+
const cmd = `node ${flags({ filename, memory })} ${filename} ${JSON.stringify(args)}`
|
|
35
|
+
|
|
36
|
+
duration = timeSpan()
|
|
37
|
+
const { stdout } = await $(cmd, {
|
|
38
|
+
cwd,
|
|
39
|
+
timeout,
|
|
40
|
+
killSignal: 'SIGKILL'
|
|
41
|
+
})
|
|
42
|
+
const { isFulfilled, value, profiling } = JSON.parse(stdout)
|
|
43
|
+
profiling.duration = duration()
|
|
44
|
+
if (isFulfilled) return [value, profiling]
|
|
45
|
+
throw deserializeError(value)
|
|
46
|
+
} catch (error) {
|
|
47
|
+
if (error.signalCode === 'SIGTRAP') {
|
|
48
|
+
throw createError({
|
|
49
|
+
name: 'MemoryError',
|
|
50
|
+
message: 'Out of memory',
|
|
51
|
+
profiling: { duration: duration() }
|
|
52
|
+
})
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (error.signalCode === 'SIGKILL') {
|
|
56
|
+
throw createError({
|
|
57
|
+
name: 'TimeoutError',
|
|
58
|
+
message: 'Execution timed out',
|
|
59
|
+
profiling: { duration: duration() }
|
|
60
|
+
})
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (error.code === 'ERR_ACCESS_DENIED') {
|
|
64
|
+
throw createError({
|
|
65
|
+
name: 'PermissionError',
|
|
66
|
+
message: `Access to '${error.permission}' has been restricted`,
|
|
67
|
+
profiling: { duration: duration() }
|
|
68
|
+
})
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
throw error
|
|
72
|
+
}
|
|
31
73
|
}
|
|
74
|
+
|
|
75
|
+
const cleanup = async () => (await compilePromise).cleanup
|
|
76
|
+
|
|
77
|
+
return [fn, cleanup]
|
|
32
78
|
}
|
package/src/template/index.js
CHANGED
|
@@ -2,39 +2,26 @@
|
|
|
2
2
|
|
|
3
3
|
const SERIALIZE_ERROR = require('./serialize-error')
|
|
4
4
|
|
|
5
|
-
const
|
|
6
|
-
|
|
7
|
-
'base64ToBuffer',
|
|
8
|
-
'bufferToBase64',
|
|
9
|
-
'define',
|
|
10
|
-
'defineAsync',
|
|
11
|
-
'dispatch',
|
|
12
|
-
'httpRequest',
|
|
13
|
-
'info',
|
|
14
|
-
'setResult'
|
|
15
|
-
]
|
|
5
|
+
const generateTemplate = snippet => `
|
|
6
|
+
const args = JSON.parse(process.argv[2])
|
|
16
7
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
let ${DISALLOW_INTERNALS.join(',')};
|
|
21
|
-
globalThis = { clearTimeout, setTimeout }
|
|
22
|
-
globalThis.global = globalThis
|
|
23
|
-
return (${snippet.toString()})(...arguments)
|
|
24
|
-
})()`
|
|
25
|
-
})()
|
|
8
|
+
;(async () => {
|
|
9
|
+
let value
|
|
10
|
+
let isFulfilled
|
|
26
11
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
}
|
|
12
|
+
try {
|
|
13
|
+
value = await (${snippet.toString()})(...args)
|
|
14
|
+
isFulfilled = true
|
|
15
|
+
} catch (error) {
|
|
16
|
+
value = ${SERIALIZE_ERROR}(error)
|
|
17
|
+
isFulfilled = false
|
|
18
|
+
} finally {
|
|
19
|
+
console.log(JSON.stringify({
|
|
20
|
+
isFulfilled,
|
|
21
|
+
value,
|
|
22
|
+
profiling: { memory: process.memoryUsage().rss }
|
|
23
|
+
}))
|
|
24
|
+
}
|
|
25
|
+
})()`
|
|
39
26
|
|
|
40
27
|
module.exports = generateTemplate
|