@jsreport/jsreport-core 3.11.4 → 3.12.0

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.
@@ -1,255 +1,257 @@
1
- const LRU = require('lru-cache')
2
- const stackTrace = require('stack-trace')
3
- const { customAlphabet } = require('nanoid')
4
- const nanoid = customAlphabet('abcdefghijklmnopqrstuvwxyz', 10)
5
- const createSandbox = require('./createSandbox')
6
- const normalizeError = require('../../shared/normalizeError')
7
-
8
- module.exports = (reporter) => {
9
- const functionsCache = LRU(reporter.options.sandbox.cache)
10
-
11
- return async ({
12
- manager = {},
13
- context,
14
- userCode,
15
- initFn,
16
- executionFn,
17
- currentPath,
18
- onRequire,
19
- propertiesConfig,
20
- errorLineNumberOffset = 0
21
- }, req) => {
22
- let jsreportProxy = null
23
-
24
- // we use dynamic name because of the potential nested vm2 execution in the jsreportProxy.assets.require
25
- // it may turn out it is a bad approach in assets so we gonna delete it here
26
- const executionFnName = `${nanoid()}_executionFn`
27
-
28
- context[executionFnName] = executionFn
29
- context.__appDirectory = reporter.options.appDirectory
30
- context.__rootDirectory = reporter.options.rootDirectory
31
- context.__parentModuleDirectory = reporter.options.parentModuleDirectory
32
- context.setTimeout = setTimeout
33
- context.__topLevelFunctions = {}
34
- context.__handleError = (err) => handleError(reporter, err)
35
-
36
- const { sourceFilesInfo, run, compileScript, restore, sandbox, sandboxRequire } = createSandbox(context, {
37
- onLog: (log) => {
38
- // we mark any log done in sandbox as userLevel: true, this allows us to detect which logs belongs to user
39
- // and can potentially contain sensitive information
40
-
41
- let consoleType = log.level
42
-
43
- if (consoleType === 'debug') {
44
- consoleType = 'log'
45
- } else if (consoleType === 'warn') {
46
- consoleType = 'warning'
47
- }
48
-
49
- reporter.logger.debug(`(console:${consoleType}) ${log.message}`, { ...req, timestamp: log.timestamp, userLevel: true })
50
- },
51
- formatError: (error, moduleName) => {
52
- error.message += ` To be able to require custom modules you need to add to configuration { "trustUserCode": true } or enable just specific module using { sandbox: { allowedModules": ["${moduleName}"] }`
53
- },
54
- safeExecution: reporter.options.trustUserCode === false,
55
- modulesCache: reporter.requestModulesCache.get(req.context.rootId),
56
- globalModules: reporter.options.sandbox.nativeModules || [],
57
- allowedModules: reporter.options.sandbox.allowedModules,
58
- propertiesConfig,
59
- requirePaths: [
60
- reporter.options.rootDirectory,
61
- reporter.options.appDirectory,
62
- reporter.options.parentModuleDirectory
63
- ],
64
- requireMap: (moduleName) => {
65
- const m = reporter.options.sandbox.modules.find((m) => m.alias === moduleName || m.path === moduleName)
66
-
67
- if (m) {
68
- return require(m.path)
69
- }
70
-
71
- if (moduleName === 'jsreport-proxy') {
72
- return jsreportProxy
73
- }
74
-
75
- if (onRequire) {
76
- return onRequire(moduleName, { context })
77
- }
78
- }
79
- })
80
-
81
- const _getTopLevelFunctions = (code) => {
82
- return getTopLevelFunctions(functionsCache, code)
83
- }
84
-
85
- jsreportProxy = reporter.createProxy({
86
- req,
87
- runInSandbox: run,
88
- context: sandbox,
89
- getTopLevelFunctions: _getTopLevelFunctions,
90
- sandboxRequire
91
- })
92
-
93
- jsreportProxy.currentPath = async () => {
94
- // we get the current path by throwing an error, which give us a stack trace
95
- // which we analyze and see if some source file is associated to an entity
96
- // if it is then we can properly get the path associated to it, if not we
97
- // fallback to the current path passed as options
98
- const filesCount = sourceFilesInfo.size
99
- let resolvedPath = currentPath
100
-
101
- if (filesCount > 0) {
102
- const err = new Error('get me stack trace please')
103
- const trace = stackTrace.parse(err)
104
-
105
- for (let i = 0; i < trace.length; i++) {
106
- const current = trace[i]
107
-
108
- if (sourceFilesInfo.has(current.getFileName())) {
109
- const { entity, entitySet } = sourceFilesInfo.get(current.getFileName())
110
-
111
- if (entity != null && entitySet != null) {
112
- resolvedPath = await reporter.folders.resolveEntityPath(entity, entitySet, req)
113
- break
114
- }
115
- }
116
- }
117
- }
118
-
119
- return resolvedPath
120
- }
121
-
122
- jsreportProxy.currentDirectoryPath = async () => {
123
- const currentPath = await jsreportProxy.currentPath()
124
-
125
- if (currentPath != null) {
126
- const localPath = currentPath.substring(0, currentPath.lastIndexOf('/'))
127
-
128
- if (localPath === '') {
129
- return '/'
130
- }
131
-
132
- return localPath
133
- }
134
-
135
- return currentPath
136
- }
137
-
138
- // NOTE: it is important that cleanup, restore methods are not called from a function attached to the
139
- // sandbox, because the arguments and return value of such function call will be sandboxed again, to solve this
140
- // we don't attach these methods to the sandbox, and instead share them through a "manager" object that should
141
- // be passed in options
142
- manager.restore = restore
143
-
144
- if (typeof initFn === 'function') {
145
- const initScriptInfo = await initFn(_getTopLevelFunctions, compileScript)
146
-
147
- if (initScriptInfo) {
148
- try {
149
- await run(initScriptInfo.script, {
150
- filename: initScriptInfo.filename || 'sandbox-init.js',
151
- source: initScriptInfo.source
152
- })
153
- } catch (e) {
154
- handleError(reporter, e)
155
- }
156
- }
157
- }
158
-
159
- const functionNames = getTopLevelFunctions(functionsCache, userCode)
160
- const functionsCode = `return {${functionNames.map(h => `"${h}": ${h}`).join(',')}}`
161
- const executionCode = `;(async () => { ${userCode} \n\n;${functionsCode} })()
162
- .then((topLevelFunctions) => {
163
- const mergedTopLevelFunctions = { ...topLevelFunctions, ...__topLevelFunctions }
164
-
165
- // expose top level functions to the sandbox context
166
- // so helpers can call other helpers (from shared asset helpers, or .registerHelpers call from proxy)
167
- for (const [topLevelFnName, topLevelFn] of Object.entries(mergedTopLevelFunctions)) {
168
- this[topLevelFnName] = topLevelFn
169
- }
170
-
171
- return ${executionFnName}({
172
- topLevelFunctions: mergedTopLevelFunctions,
173
- require,
174
- console,
175
- context: this
176
- })
177
- }).catch(__handleError);`
178
-
179
- try {
180
- return await run(executionCode, {
181
- filename: 'sandbox.js',
182
- source: userCode,
183
- errorLineNumberOffset
184
- })
185
- } catch (e) {
186
- handleError(reporter, e)
187
- }
188
- }
189
- }
190
-
191
- function handleError (reporter, errValue) {
192
- let newError = normalizeError(errValue)
193
-
194
- if (newError === errValue) {
195
- // here it means the original error was valid in the first place,
196
- // so it was not normalized
197
- newError = new Error(errValue.message)
198
- Object.assign(newError, errValue)
199
-
200
- if (errValue.stack) {
201
- newError.stack = errValue.stack
202
- }
203
- }
204
-
205
- throw reporter.createError(null, {
206
- original: newError,
207
- statusCode: 400
208
- })
209
- }
210
-
211
- function getTopLevelFunctions (cache, code) {
212
- const key = `functions:${code}`
213
-
214
- if (cache.has(key)) {
215
- return cache.get(key)
216
- }
217
-
218
- // lazy load to speed up boot
219
- const parser = require('@babel/parser')
220
- const traverse = require('@babel/traverse').default
221
-
222
- const names = []
223
- try {
224
- const ast = parser.parse(code, {
225
- sourceType: 'script',
226
- allowReturnOutsideFunction: false,
227
- allowAwaitOutsideFunction: true,
228
- plugins: [
229
- 'classProperties',
230
- 'classPrivateProperties',
231
- 'classPrivateMethods',
232
- 'doExpressions',
233
- 'functionBind',
234
- 'throwExpressions',
235
- 'topLevelAwait'
236
- ]
237
- })
238
-
239
- // traverse only function declaration that are defined
240
- // at the top level of program
241
- traverse(ast, {
242
- FunctionDeclaration: (path) => {
243
- if (path.parent.type === 'Program') {
244
- names.push(path.node.id.name)
245
- }
246
- }
247
- })
248
- } catch (e) {
249
- // we let the error handling for later eval
250
- return []
251
- }
252
-
253
- cache.set(key, names)
254
- return names
255
- }
1
+ const LRU = require('lru-cache')
2
+ const stackTrace = require('stack-trace')
3
+ const { customAlphabet } = require('nanoid')
4
+ const nanoid = customAlphabet('abcdefghijklmnopqrstuvwxyz', 10)
5
+ const createSandbox = require('./createSandbox')
6
+ const normalizeError = require('../../shared/normalizeError')
7
+
8
+ module.exports = function createRunInSandbox (reporter) {
9
+ const functionsCache = LRU(reporter.options.sandbox.cache)
10
+
11
+ return async function runInSandbox ({
12
+ manager = {},
13
+ context,
14
+ userCode,
15
+ initFn,
16
+ executionFn,
17
+ currentPath,
18
+ onRequire,
19
+ propertiesConfig,
20
+ errorLineNumberOffset = 0
21
+ }, req) {
22
+ let jsreportProxy = null
23
+
24
+ // we use dynamic name because of the potential nested vm2 execution in the jsreportProxy.assets.require
25
+ // it may turn out it is a bad approach in assets so we gonna delete it here
26
+ const executionFnName = `${nanoid()}_executionFn`
27
+
28
+ context[executionFnName] = executionFn
29
+ context.__appDirectory = reporter.options.appDirectory
30
+ context.__rootDirectory = reporter.options.rootDirectory
31
+ context.__parentModuleDirectory = reporter.options.parentModuleDirectory
32
+ context.setTimeout = setTimeout
33
+ context.__topLevelFunctions = {}
34
+ context.__handleError = (err) => handleError(reporter, err)
35
+
36
+ const { sourceFilesInfo, run, compileScript, restore, sandbox, sandboxRequire } = createSandbox(context, {
37
+ rootDirectory: reporter.options.rootDirectory,
38
+ onLog: (log) => {
39
+ // we mark any log done in sandbox as userLevel: true, this allows us to detect which logs belongs to user
40
+ // and can potentially contain sensitive information
41
+
42
+ let consoleType = log.level
43
+
44
+ if (consoleType === 'debug') {
45
+ consoleType = 'log'
46
+ } else if (consoleType === 'warn') {
47
+ consoleType = 'warning'
48
+ }
49
+
50
+ reporter.logger.debug(`(console:${consoleType}) ${log.message}`, { ...req, timestamp: log.timestamp, userLevel: true })
51
+ },
52
+ formatError: (error, moduleName) => {
53
+ error.message += ` To be able to require custom modules you need to add to configuration { "trustUserCode": true } or enable just specific module using { sandbox: { allowedModules": ["${moduleName}"] }`
54
+ },
55
+ safeExecution: reporter.options.trustUserCode === false,
56
+ isolateModules: reporter.options.sandbox.isolateModules !== false,
57
+ modulesCache: reporter.requestModulesCache.get(req.context.rootId),
58
+ globalModules: reporter.options.sandbox.nativeModules || [],
59
+ allowedModules: reporter.options.sandbox.allowedModules,
60
+ propertiesConfig,
61
+ requirePaths: [
62
+ reporter.options.rootDirectory,
63
+ reporter.options.appDirectory,
64
+ reporter.options.parentModuleDirectory
65
+ ],
66
+ requireMap: (moduleName) => {
67
+ const m = reporter.options.sandbox.modules.find((m) => m.alias === moduleName || m.path === moduleName)
68
+
69
+ if (m) {
70
+ return require(m.path)
71
+ }
72
+
73
+ if (moduleName === 'jsreport-proxy') {
74
+ return jsreportProxy
75
+ }
76
+
77
+ if (onRequire) {
78
+ return onRequire(moduleName, { context })
79
+ }
80
+ }
81
+ })
82
+
83
+ const _getTopLevelFunctions = function _getTopLevelFunctions (code) {
84
+ return getTopLevelFunctions(functionsCache, code)
85
+ }
86
+
87
+ jsreportProxy = reporter.createProxy({
88
+ req,
89
+ runInSandbox: run,
90
+ context: sandbox,
91
+ getTopLevelFunctions: _getTopLevelFunctions,
92
+ sandboxRequire
93
+ })
94
+
95
+ jsreportProxy.currentPath = async function getCurrentPath () {
96
+ // we get the current path by throwing an error, which give us a stack trace
97
+ // which we analyze and see if some source file is associated to an entity
98
+ // if it is then we can properly get the path associated to it, if not we
99
+ // fallback to the current path passed as options
100
+ const filesCount = sourceFilesInfo.size
101
+ let resolvedPath = currentPath
102
+
103
+ if (filesCount > 0) {
104
+ const err = new Error('get me stack trace please')
105
+ const trace = stackTrace.parse(err)
106
+
107
+ for (let i = 0; i < trace.length; i++) {
108
+ const current = trace[i]
109
+
110
+ if (sourceFilesInfo.has(current.getFileName())) {
111
+ const { entity, entitySet } = sourceFilesInfo.get(current.getFileName())
112
+
113
+ if (entity != null && entitySet != null) {
114
+ resolvedPath = await reporter.folders.resolveEntityPath(entity, entitySet, req)
115
+ break
116
+ }
117
+ }
118
+ }
119
+ }
120
+
121
+ return resolvedPath
122
+ }
123
+
124
+ jsreportProxy.currentDirectoryPath = async function getCurrentDirectoryPath () {
125
+ const currentPath = await jsreportProxy.currentPath()
126
+
127
+ if (currentPath != null) {
128
+ const localPath = currentPath.substring(0, currentPath.lastIndexOf('/'))
129
+
130
+ if (localPath === '') {
131
+ return '/'
132
+ }
133
+
134
+ return localPath
135
+ }
136
+
137
+ return currentPath
138
+ }
139
+
140
+ // NOTE: it is important that cleanup, restore methods are not called from a function attached to the
141
+ // sandbox, because the arguments and return value of such function call will be sandboxed again, to solve this
142
+ // we don't attach these methods to the sandbox, and instead share them through a "manager" object that should
143
+ // be passed in options
144
+ manager.restore = restore
145
+
146
+ if (typeof initFn === 'function') {
147
+ const initScriptInfo = await initFn(_getTopLevelFunctions, compileScript)
148
+
149
+ if (initScriptInfo) {
150
+ try {
151
+ await run(initScriptInfo.script, {
152
+ filename: initScriptInfo.filename || 'sandbox-init.js',
153
+ source: initScriptInfo.source
154
+ })
155
+ } catch (e) {
156
+ handleError(reporter, e)
157
+ }
158
+ }
159
+ }
160
+
161
+ const functionNames = getTopLevelFunctions(functionsCache, userCode)
162
+ const functionsCode = `return {${functionNames.map(h => `"${h}": ${h}`).join(',')}}`
163
+ const executionCode = `;(async () => { ${userCode} \n\n;${functionsCode} })()
164
+ .then((topLevelFunctions) => {
165
+ const mergedTopLevelFunctions = { ...topLevelFunctions, ...__topLevelFunctions }
166
+
167
+ // expose top level functions to the sandbox context
168
+ // so helpers can call other helpers (from shared asset helpers, or .registerHelpers call from proxy)
169
+ for (const [topLevelFnName, topLevelFn] of Object.entries(mergedTopLevelFunctions)) {
170
+ this[topLevelFnName] = topLevelFn
171
+ }
172
+
173
+ return ${executionFnName}({
174
+ topLevelFunctions: mergedTopLevelFunctions,
175
+ require,
176
+ console,
177
+ context: this
178
+ })
179
+ }).catch(__handleError);`
180
+
181
+ try {
182
+ return await run(executionCode, {
183
+ filename: 'sandbox.js',
184
+ source: userCode,
185
+ errorLineNumberOffset
186
+ })
187
+ } catch (e) {
188
+ handleError(reporter, e)
189
+ }
190
+ }
191
+ }
192
+
193
+ function handleError (reporter, errValue) {
194
+ let newError = normalizeError(errValue)
195
+
196
+ if (newError === errValue) {
197
+ // here it means the original error was valid in the first place,
198
+ // so it was not normalized
199
+ newError = new Error(errValue.message)
200
+ Object.assign(newError, errValue)
201
+
202
+ if (errValue.stack) {
203
+ newError.stack = errValue.stack
204
+ }
205
+ }
206
+
207
+ throw reporter.createError(null, {
208
+ original: newError,
209
+ statusCode: 400
210
+ })
211
+ }
212
+
213
+ function getTopLevelFunctions (cache, code) {
214
+ const key = `functions:${code}`
215
+
216
+ if (cache.has(key)) {
217
+ return cache.get(key)
218
+ }
219
+
220
+ // lazy load to speed up boot
221
+ const parser = require('@babel/parser')
222
+ const traverse = require('@babel/traverse').default
223
+
224
+ const names = []
225
+ try {
226
+ const ast = parser.parse(code, {
227
+ sourceType: 'script',
228
+ allowReturnOutsideFunction: false,
229
+ allowAwaitOutsideFunction: true,
230
+ plugins: [
231
+ 'classProperties',
232
+ 'classPrivateProperties',
233
+ 'classPrivateMethods',
234
+ 'doExpressions',
235
+ 'functionBind',
236
+ 'throwExpressions',
237
+ 'topLevelAwait'
238
+ ]
239
+ })
240
+
241
+ // traverse only function declaration that are defined
242
+ // at the top level of program
243
+ traverse(ast, {
244
+ FunctionDeclaration: (path) => {
245
+ if (path.parent.type === 'Program') {
246
+ names.push(path.node.id.name)
247
+ }
248
+ }
249
+ })
250
+ } catch (e) {
251
+ // we let the error handling for later eval
252
+ return []
253
+ }
254
+
255
+ cache.set(key, names)
256
+ return names
257
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jsreport/jsreport-core",
3
- "version": "3.11.4",
3
+ "version": "3.12.0",
4
4
  "description": "javascript based business reporting",
5
5
  "keywords": [
6
6
  "report",
@@ -40,7 +40,7 @@
40
40
  "@babel/parser": "7.14.4",
41
41
  "@babel/traverse": "7.12.9",
42
42
  "@colors/colors": "1.5.0",
43
- "@jsreport/advanced-workers": "1.2.4",
43
+ "@jsreport/advanced-workers": "1.3.0",
44
44
  "@jsreport/mingo": "2.4.1",
45
45
  "@jsreport/reap": "0.1.0",
46
46
  "ajv": "6.12.6",
@@ -71,12 +71,13 @@
71
71
  "triple-beam": "1.3.0",
72
72
  "unset-value": "2.0.1",
73
73
  "uuid": "8.3.2",
74
- "vm2": "3.9.17",
74
+ "vm2": "3.9.19",
75
75
  "winston": "3.8.1",
76
76
  "winston-transport": "4.5.0",
77
77
  "yieldable-json": "2.0.1"
78
78
  },
79
79
  "devDependencies": {
80
+ "@node-rs/jsonwebtoken": "0.2.0",
80
81
  "mocha": "9.2.2",
81
82
  "should": "13.2.3",
82
83
  "standard": "16.0.4",
@@ -1,5 +1,6 @@
1
1
  const extend = require('node.extend.without.arrays')
2
2
  const vm = require('vm')
3
+ const Module = require('module')
3
4
  const path = require('path')
4
5
  const process = require('process')
5
6
 
@@ -39,7 +40,12 @@ module.exports = (reporter, definition) => {
39
40
  return script.runInThisContext({
40
41
  displayErrors: true
41
42
  })(req, res, {
43
+ mainModuleFilename: require.main.filename,
42
44
  require: (m) => {
45
+ if (Module.builtinModules.includes(m)) {
46
+ return require(m)
47
+ }
48
+
43
49
  try {
44
50
  return require(path.join(process.cwd(), 'node_modules', m))
45
51
  } catch (e) {