@jsreport/jsreport-core
Advanced tools
Comparing version 3.5.0 to 3.6.0
@@ -169,2 +169,5 @@ const path = require('path') | ||
for (const { TransportClass, options } of transportsToAdd) { | ||
if (options.silent) { | ||
continue | ||
} | ||
const transportInstance = new TransportClass(options) | ||
@@ -171,0 +174,0 @@ |
@@ -42,2 +42,3 @@ const path = require('path') | ||
const explicitOptions = loadConfigResult[0] | ||
const appliedConfigFile = loadConfigResult[1] | ||
@@ -83,5 +84,12 @@ | ||
options.sandbox = options.sandbox || {} | ||
if (options.allowLocalFilesAccess === true) { | ||
// NOTE: handling back-compatible introduction of "trustUserCode" option, "allowLocalFilesAccess" is deprecated | ||
if (explicitOptions.allowLocalFilesAccess === true && explicitOptions.trustUserCode == null) { | ||
options.trustUserCode = true | ||
} | ||
if (options.trustUserCode === true) { | ||
options.sandbox.allowedModules = '*' | ||
} | ||
options.sandbox.nativeModules = options.sandbox.nativeModules || [] | ||
@@ -103,3 +111,3 @@ options.sandbox.modules = options.sandbox.modules || [] | ||
return appliedConfigFile | ||
return [explicitOptions, appliedConfigFile] | ||
} | ||
@@ -106,0 +114,0 @@ |
@@ -62,2 +62,3 @@ const { getDefaultTempDirectory, getDefaultLoadConfig } = require('./defaults') | ||
enableRequestReportTimeout: { type: 'boolean', default: false, description: 'option that enables passing a custom report timeout per request using req.options.timeout. this enables that the caller of the report generation control the report timeout so enable it only when you trust the caller' }, | ||
trustUserCode: { type: 'boolean', default: false, description: 'option that control whether code sandboxing is enabled or not, code sandboxing has an impact on performance when rendering large reports. when true code sandboxing will be disabled meaning that users can potentially penetrate the local system if you allow code from external users to be part of your reports' }, | ||
allowLocalFilesAccess: { type: 'boolean', default: false }, | ||
@@ -81,2 +82,3 @@ encryption: { | ||
type: 'object', | ||
default: {}, | ||
properties: { | ||
@@ -94,5 +96,6 @@ allowedModules: { | ||
type: 'object', | ||
default: {}, | ||
properties: { | ||
max: { type: 'number' }, | ||
enabled: { type: 'boolean' } | ||
max: { type: 'number', default: 100 }, | ||
enabled: { type: 'boolean', default: true } | ||
} | ||
@@ -188,3 +191,3 @@ } | ||
}, | ||
maxResponseSize: { | ||
maxDiffSize: { | ||
type: ['string', 'number'], | ||
@@ -191,0 +194,0 @@ '$jsreport-acceptsSize': true, |
@@ -8,3 +8,3 @@ /*! | ||
const { Readable } = require('stream') | ||
const Reaper = require('reap2') | ||
const Reaper = require('@jsreport/reap') | ||
const optionsLoad = require('./optionsLoad') | ||
@@ -94,4 +94,4 @@ const { createLogger, configureLogger, silentLogs } = require('./logger') | ||
async extensionsLoad (opts) { | ||
const appliedConfigFile = await optionsLoad({ | ||
async extensionsLoad (_opts = {}) { | ||
const [explicitOptions, appliedConfigFile] = await optionsLoad({ | ||
defaults: this.defaults, | ||
@@ -111,2 +111,4 @@ options: this.options, | ||
const { onConfigDetails, ...opts } = _opts | ||
this.logger.info(`Initializing jsreport (version: ${this.version}, configuration file: ${appliedConfigFile || 'none'}, nodejs: ${process.versions.node})`) | ||
@@ -136,2 +138,6 @@ | ||
if (typeof onConfigDetails === 'function') { | ||
onConfigDetails(explicitOptions) | ||
} | ||
return this | ||
@@ -164,2 +170,3 @@ } | ||
this.closing = this.closed = false | ||
if (this._initialized || this._initializing) { | ||
@@ -183,4 +190,11 @@ throw new Error('jsreport already initialized or just initializing. Make sure init is called only once') | ||
this._registerLogMainAction() | ||
await this.extensionsLoad() | ||
let explicitOptions | ||
await this.extensionsLoad({ | ||
onConfigDetails: (_explicitOptions) => { | ||
explicitOptions = _explicitOptions | ||
} | ||
}) | ||
this.documentStore = DocumentStore(Object.assign({}, this.options, { logger: this.logger }), this.entityTypeValidator, this.encryption) | ||
@@ -209,2 +223,10 @@ documentStoreActions(this) | ||
if (this.options.trustUserCode) { | ||
this.logger.info('Code sandboxing is disabled, users can potentially penetrate the local system if you allow code from external users to be part of your reports') | ||
} | ||
if (explicitOptions.trustUserCode == null && explicitOptions.allowLocalFilesAccess != null) { | ||
this.logger.warn('options.allowLocalFilesAccess is deprecated, use options.trustUserCode instead') | ||
} | ||
this.logger.info(`Using general timeout for rendering (reportTimeout: ${this.options.reportTimeout})`) | ||
@@ -211,0 +233,0 @@ |
@@ -12,5 +12,6 @@ /*! | ||
module.exports = (reporter) => { | ||
const cache = LRU(reporter.options.sandbox.cache || { max: 100 }) | ||
const templatesCache = LRU(reporter.options.sandbox.cache) | ||
let systemHelpersCache | ||
reporter.templatingEngines = { cache } | ||
reporter.templatingEngines = { cache: templatesCache } | ||
@@ -64,2 +65,29 @@ const executionFnParsedParamsMap = new Map() | ||
}, | ||
waitForAsyncHelper: async (maybeAsyncContent) => { | ||
if ( | ||
context.__executionId == null || | ||
!executionAsyncResultsMap.has(context.__executionId) || | ||
typeof maybeAsyncContent !== 'string' | ||
) { | ||
return maybeAsyncContent | ||
} | ||
const asyncResultMap = executionAsyncResultsMap.get(context.__executionId) | ||
const asyncHelperResultRegExp = /{#asyncHelperResult ([^{}]+)}/ | ||
let content = maybeAsyncContent | ||
let matchResult | ||
do { | ||
if (matchResult != null) { | ||
const matchedPart = matchResult[0] | ||
const asyncResultId = matchResult[1] | ||
const result = await asyncResultMap.get(asyncResultId) | ||
content = `${content.slice(0, matchResult.index)}${result}${content.slice(matchResult.index + matchedPart.length)}` | ||
} | ||
matchResult = content.match(asyncHelperResultRegExp) | ||
} while (matchResult != null) | ||
return content | ||
}, | ||
waitForAsyncHelpers: async () => { | ||
@@ -107,19 +135,46 @@ if (context.__executionId != null && executionAsyncResultsMap.has(context.__executionId)) { | ||
const registerResults = await reporter.registerHelpersListeners.fire(req) | ||
const systemHelpers = [] | ||
const normalizedHelpers = `${helpers || ''}` | ||
const executionFnParsedParamsKey = `entity:${entity.shortid || 'anonymous'}:helpers:${normalizedHelpers}` | ||
for (const result of registerResults) { | ||
if (result == null) { | ||
continue | ||
const initFn = async (getTopLevelFunctions, compileScript) => { | ||
if (systemHelpersCache != null) { | ||
return systemHelpersCache | ||
} | ||
if (typeof result === 'string') { | ||
systemHelpers.push(result) | ||
const registerResults = await reporter.registerHelpersListeners.fire() | ||
const systemHelpers = [] | ||
for (const result of registerResults) { | ||
if (result == null) { | ||
continue | ||
} | ||
if (typeof result === 'string') { | ||
systemHelpers.push(result) | ||
} | ||
} | ||
const systemHelpersStr = systemHelpers.join('\n') | ||
const functionNames = getTopLevelFunctions(systemHelpersStr) | ||
const exposeSystemHelpersCode = `for (const fName of ${JSON.stringify(functionNames)}) { this[fName] = __topLevelFunctions[fName] }` | ||
// we sync the __topLevelFunctions with system helpers and expose it immediately to the global context | ||
const userCode = `(async () => { ${systemHelpersStr}; | ||
__topLevelFunctions = {...__topLevelFunctions, ${functionNames.map(h => `"${h}": ${h}`).join(',')}}; ${exposeSystemHelpersCode} | ||
})()` | ||
const filename = 'system-helpers.js' | ||
const script = compileScript(userCode, filename) | ||
systemHelpersCache = { | ||
filename, | ||
source: systemHelpersStr, | ||
script | ||
} | ||
return systemHelpersCache | ||
} | ||
const systemHelpersStr = systemHelpers.join('\n') | ||
const joinedHelpers = systemHelpersStr + '\n' + (helpers || '') | ||
const executionFnParsedParamsKey = `entity:${entity.shortid || 'anonymous'}:helpers:${joinedHelpers}` | ||
const executionFn = async ({ require, console, topLevelFunctions, context }) => { | ||
@@ -135,5 +190,5 @@ const asyncResultMap = new Map() | ||
if (!cache.has(key)) { | ||
if (!templatesCache.has(key)) { | ||
try { | ||
cache.set(key, engine.compile(content, { require })) | ||
templatesCache.set(key, engine.compile(content, { require })) | ||
} catch (e) { | ||
@@ -145,4 +200,3 @@ e.property = 'content' | ||
const compiledTemplate = cache.get(key) | ||
const compiledTemplate = templatesCache.get(key) | ||
const wrappedTopLevelFunctions = {} | ||
@@ -155,3 +209,5 @@ | ||
let contentResult = await engine.execute(compiledTemplate, wrappedTopLevelFunctions, data, { require }) | ||
const resolvedResultsMap = new Map() | ||
while (asyncResultMap.size > 0) { | ||
@@ -187,5 +243,7 @@ await Promise.all([...asyncResultMap.keys()].map(async (k) => { | ||
const awaiter = {} | ||
awaiter.promise = new Promise((resolve) => { | ||
awaiter.resolve = resolve | ||
}) | ||
executionFnParsedParamsMap.get(req.context.id).set(executionFnParsedParamsKey, awaiter) | ||
@@ -195,3 +253,3 @@ } | ||
if (reporter.options.sandbox.cache && reporter.options.sandbox.cache.enabled === false) { | ||
cache.reset() | ||
templatesCache.reset() | ||
} | ||
@@ -204,6 +262,6 @@ | ||
}, | ||
userCode: joinedHelpers, | ||
userCode: normalizedHelpers, | ||
initFn, | ||
executionFn, | ||
currentPath: entityPath, | ||
errorLineNumberOffset: systemHelpersStr.split('\n').length, | ||
onRequire: (moduleName, { context }) => { | ||
@@ -210,0 +268,0 @@ if (engine.onRequire) { |
@@ -7,3 +7,3 @@ const fs = require('fs').promises | ||
reporter.registerHelpersListeners.add('core-helpers', (req) => { | ||
reporter.registerHelpersListeners.add('core-helpers', () => { | ||
return helpersScript | ||
@@ -16,7 +16,7 @@ }) | ||
reporter.extendProxy((proxy, req, { safeRequire }) => { | ||
reporter.extendProxy((proxy, req, { sandboxRequire }) => { | ||
proxy.module = async (module) => { | ||
if (!reporter.options.allowLocalFilesAccess && reporter.options.sandbox.allowedModules !== '*') { | ||
if (!reporter.options.trustUserCode && reporter.options.sandbox.allowedModules !== '*') { | ||
if (reporter.options.sandbox.allowedModules.indexOf(module) === -1) { | ||
throw reporter.createError(`require of module ${module} was rejected. Either set allowLocalFilesAccess=true or sandbox.allowLocalModules='*' or sandbox.allowLocalModules=['${module}'] `, { status: 400 }) | ||
throw reporter.createError(`require of module ${module} was rejected. Either set trustUserCode=true or sandbox.allowLocalModules='*' or sandbox.allowLocalModules=['${module}'] `, { status: 400 }) | ||
} | ||
@@ -23,0 +23,0 @@ } |
@@ -75,3 +75,3 @@ const extend = require('node.extend.without.arrays') | ||
if (content != null) { | ||
if (content.length > this.reporter.options.profiler.maxResponseSize) { | ||
if (content.length > this.reporter.options.profiler.maxDiffSize) { | ||
content = { | ||
@@ -101,3 +101,8 @@ tooLarge: true | ||
m.req = { diff: createPatch('req', req.context.profiling.reqLastVal || '', stringifiedReq, 0) } | ||
m.req = { } | ||
if (stringifiedReq.length * 4 > this.reporter.options.profiler.maxDiffSize) { | ||
m.req.tooLarge = true | ||
} else { | ||
m.req.diff = createPatch('req', req.context.profiling.reqLastVal || '', stringifiedReq, 0) | ||
} | ||
@@ -104,0 +109,0 @@ req.context.profiling.resLastVal = (res.content == null || isbinaryfile(res.content) || content.tooLarge) ? null : res.content.toString() |
@@ -114,3 +114,3 @@ const ExtensionsManager = require('./extensionsManager') | ||
createProxy ({ req, runInSandbox, context, getTopLevelFunctions, safeRequire }) { | ||
createProxy ({ req, runInSandbox, context, getTopLevelFunctions, sandboxRequire }) { | ||
const proxyInstance = {} | ||
@@ -122,3 +122,3 @@ for (const fn of this._proxyRegistrationFns) { | ||
getTopLevelFunctions, | ||
safeRequire | ||
sandboxRequire | ||
}) | ||
@@ -142,6 +142,7 @@ } | ||
userCode, | ||
initFn, | ||
executionFn, | ||
currentPath, | ||
onRequire, | ||
propertiesConfig, | ||
currentPath, | ||
errorLineNumberOffset | ||
@@ -158,6 +159,7 @@ }, req) { | ||
userCode, | ||
initFn, | ||
executionFn, | ||
currentPath, | ||
onRequire, | ||
propertiesConfig, | ||
currentPath, | ||
errorLineNumberOffset | ||
@@ -164,0 +166,0 @@ }, req) |
const LRU = require('lru-cache') | ||
const stackTrace = require('stack-trace') | ||
const { customAlphabet } = require('nanoid') | ||
const safeSandbox = require('./safeSandbox') | ||
const createSandbox = require('./createSandbox') | ||
const nanoid = customAlphabet('abcdefghijklmnopqrstuvwxyz', 10) | ||
module.exports = (reporter) => { | ||
return ({ | ||
const functionsCache = LRU(reporter.options.sandbox.cache) | ||
return async ({ | ||
manager = {}, | ||
context, | ||
userCode, | ||
initFn, | ||
executionFn, | ||
@@ -22,3 +25,4 @@ currentPath, | ||
// it may turn out it is a bad approach in assets so we gonna delete it here | ||
const executionFnName = nanoid() + '_executionFn' | ||
const executionFnName = `${nanoid()}_executionFn` | ||
context[executionFnName] = executionFn | ||
@@ -32,3 +36,3 @@ context.__appDirectory = reporter.options.appDirectory | ||
const { sourceFilesInfo, run, restore, sandbox, safeRequire } = safeSandbox(context, { | ||
const { sourceFilesInfo, run, compileScript, restore, sandbox, sandboxRequire } = createSandbox(context, { | ||
onLog: (log) => { | ||
@@ -38,4 +42,5 @@ reporter.logger[log.level](log.message, { ...req, timestamp: log.timestamp }) | ||
formatError: (error, moduleName) => { | ||
error.message += ` To be able to require custom modules you need to add to configuration { "allowLocalFilesAccess": true } or enable just specific module using { sandbox: { allowedModules": ["${moduleName}"] }` | ||
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}"] }` | ||
}, | ||
safeExecution: reporter.options.trustUserCode === false, | ||
modulesCache: reporter.requestModulesCache.get(req.context.rootId), | ||
@@ -67,4 +72,14 @@ globalModules: reporter.options.sandbox.nativeModules || [], | ||
jsreportProxy = reporter.createProxy({ req, runInSandbox: run, context: sandbox, getTopLevelFunctions, safeRequire }) | ||
const _getTopLevelFunctions = (code) => { | ||
return getTopLevelFunctions(functionsCache, code) | ||
} | ||
jsreportProxy = reporter.createProxy({ | ||
req, | ||
runInSandbox: run, | ||
context: sandbox, | ||
getTopLevelFunctions: _getTopLevelFunctions, | ||
sandboxRequire | ||
}) | ||
jsreportProxy.currentPath = async () => { | ||
@@ -121,3 +136,14 @@ // we get the current path by throwing an error, which give us a stack trace | ||
const functionNames = getTopLevelFunctions(userCode) | ||
if (typeof initFn === 'function') { | ||
const initScriptInfo = await initFn(_getTopLevelFunctions, compileScript) | ||
if (initScriptInfo) { | ||
await run(initScriptInfo.script, { | ||
filename: initScriptInfo.filename || 'sandbox-init.js', | ||
source: initScriptInfo.source | ||
}) | ||
} | ||
} | ||
const functionNames = getTopLevelFunctions(functionsCache, userCode) | ||
const functionsCode = `return {${functionNames.map(h => `"${h}": ${h}`).join(',')}}` | ||
@@ -188,8 +214,7 @@ const executionCode = `;(async () => { ${userCode} \n\n;${functionsCode} })() | ||
const functionsCache = LRU({ max: 100 }) | ||
function getTopLevelFunctions (code) { | ||
function getTopLevelFunctions (cache, code) { | ||
const key = `functions:${code}` | ||
if (functionsCache.has(key)) { | ||
return functionsCache.get(key) | ||
if (cache.has(key)) { | ||
return cache.get(key) | ||
} | ||
@@ -232,4 +257,4 @@ | ||
functionsCache.set(key, names) | ||
cache.set(key, names) | ||
return names | ||
} |
{ | ||
"name": "@jsreport/jsreport-core", | ||
"version": "3.5.0", | ||
"version": "3.6.0", | ||
"description": "javascript based business reporting", | ||
@@ -35,6 +35,6 @@ "keywords": [ | ||
"@babel/traverse": "7.12.9", | ||
"@jsreport/advanced-workers": "1.2.1", | ||
"@jsreport/advanced-workers": "1.2.2", | ||
"@jsreport/mingo": "2.4.1", | ||
"ajv": "6.12.6", | ||
"app-root-path": "2.0.1", | ||
"app-root-path": "3.0.0", | ||
"bytes": "3.1.2", | ||
@@ -60,3 +60,3 @@ "camelcase": "5.0.0", | ||
"node.extend.without.arrays": "1.1.6", | ||
"reap2": "1.0.1", | ||
"@jsreport/reap": "0.1.0", | ||
"semver": "7.3.5", | ||
@@ -68,3 +68,3 @@ "serializator": "1.0.2", | ||
"uuid": "8.3.2", | ||
"vm2": "3.9.7", | ||
"vm2": "3.9.9", | ||
"winston": "3.3.3", | ||
@@ -71,0 +71,0 @@ "winston-transport": "4.4.0" |
346775
8559
+ Added@jsreport/reap@0.1.0
+ Added@jsreport/advanced-workers@1.2.2(transitive)
+ Added@jsreport/reap@0.1.0(transitive)
+ Addedapp-root-path@3.0.0(transitive)
+ Addedbatch@0.6.1(transitive)
+ Addedvm2@3.9.9(transitive)
- Removedreap2@1.0.1
- Removed@jsreport/advanced-workers@1.2.1(transitive)
- Removedapp-root-path@2.0.1(transitive)
- Removedbatch@0.4.0(transitive)
- Removedbytes@0.2.1(transitive)
- Removedcommander@1.2.0(transitive)
- Removedkeypress@0.1.0(transitive)
- Removedms@0.7.1(transitive)
- Removedreap2@1.0.1(transitive)
- Removedvm2@3.9.7(transitive)
Updatedapp-root-path@3.0.0
Updatedvm2@3.9.9