remoteCode.js 2.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112
  1. import http from 'http';
  2. import https from 'https';
  3. import vm2 from 'vm2';
  4. import NodeCache from 'node-cache';
  5. const { NodeVM } = vm2;
  6. const allowedModules = ['http', 'https', 'stream', 'url', 'zlib', 'encoding'];
  7. class RemoteCode {
  8. constructor({ disableCache = false, preProcess = (script) => script }) {
  9. const cacheConfig = {
  10. useClones: false,
  11. stdTTL: 200,
  12. checkperiod: 250,
  13. };
  14. Object.assign(this, {
  15. disableCache,
  16. scriptCache: new NodeCache(cacheConfig),
  17. cacheConfig,
  18. preProcess,
  19. });
  20. this.vm = new NodeVM({
  21. console: 'inherit',
  22. require: {
  23. builtin: allowedModules,
  24. root: './',
  25. },
  26. });
  27. }
  28. /**
  29. * Get and cache the script designed by name from remote
  30. * @param {string} scriptPath script name.
  31. * @param {string} extraCommands to be concatened at the end of script.
  32. */
  33. async cacheOrFetch(req, remote, scriptPath, extraCommands = '') {
  34. if (!this.scriptCache.has(remote)) {
  35. this.scriptCache.set(remote, new NodeCache(this.cacheConfig));
  36. }
  37. const cache = this.scriptCache.get(remote);
  38. if (cache.has(scriptPath) && !this.disableCache) {
  39. return cache.get(scriptPath);
  40. } else {
  41. const httpClient = remote.startsWith('https') ? https : http;
  42. return new Promise((resolve, reject) => {
  43. const scriptUrl = `${remote}/${scriptPath}`.replace('//', '/');
  44. httpClient
  45. .get(scriptUrl, (resp) => {
  46. if (resp.statusCode === 404) {
  47. reject({ status: 'not-found' });
  48. return;
  49. }
  50. let script = '';
  51. resp.on('data', (chunk) => {
  52. script += chunk;
  53. });
  54. resp.on('end', () => {
  55. try {
  56. script = this.preProcess.bind(this)(script, req);
  57. } catch (e) {
  58. reject({ status: 'error', error: e });
  59. }
  60. script += extraCommands;
  61. try {
  62. const scriptFunction = this.vm.run(script).default;
  63. cache.set(scriptPath, scriptFunction);
  64. this.scriptCache.set(remote, cache);
  65. resolve(scriptFunction);
  66. } catch (e) {
  67. reject({ status: 'error', error: e });
  68. }
  69. });
  70. })
  71. .on('error', (err) => {
  72. /* istanbul ignore next */
  73. reject({ status: 'error', error: err });
  74. });
  75. });
  76. }
  77. }
  78. async exec(req, remote, scriptPath, context) {
  79. try {
  80. const toRun = await this.cacheOrFetch(req, remote, scriptPath);
  81. return toRun({ ...context });
  82. } catch (e) {
  83. if (e.status === 'not-found') {
  84. throw `Script ${scriptPath} not found on remote ${remote}`;
  85. } else {
  86. if (e.error) {
  87. throw e.error;
  88. } else {
  89. throw e;
  90. }
  91. }
  92. }
  93. }
  94. clearCache(remote) {
  95. this.scriptCache.del(remote);
  96. }
  97. }
  98. export default RemoteCode;