speednode 0.8.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,412 @@
1
+ 'use strict';
2
+
3
+ const vm = require('vm');
4
+ const net = require('net');
5
+ const os = require('os');
6
+ const fs = require('fs');
7
+ let crypto_var = null;
8
+ try {
9
+ crypto_var = require('crypto');
10
+ } catch (err) {}
11
+ const crypto = crypto_var;
12
+ let contexts = {};
13
+ let scripts = {};
14
+ let process_exit = false;
15
+
16
+ /*** circular-json, originally taken from https://raw.githubusercontent.com/WebReflection/circular-json/
17
+ Copyright (C) 2013-2017 by Andrea Giammarchi - @WebReflection
18
+
19
+ Permission is hereby granted, free of charge, to any person obtaining a copy
20
+ of this software and associated documentation files (the "Software"), to deal
21
+ in the Software without restriction, including without limitation the rights
22
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
23
+ copies of the Software, and to permit persons to whom the Software is
24
+ furnished to do so, subject to the following conditions:
25
+
26
+ The above copyright notice and this permission notice shall be included in
27
+ all copies or substantial portions of the Software.
28
+
29
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
30
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
31
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
32
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
33
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
34
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
35
+ THE SOFTWARE.
36
+
37
+ ***
38
+
39
+ the original version has been restructured and modified to fit in here,
40
+ only stringify is used, unused parts removed.
41
+
42
+ */
43
+
44
+ const CircularJSON = {};
45
+ CircularJSON.specialChar = '~';
46
+ CircularJSON.safeSpecialChar = '\\x' + ('0' + CircularJSON.specialChar.charCodeAt(0).toString(16)).slice(-2);
47
+ CircularJSON.escapedSafeSpecialChar = '\\' + CircularJSON.safeSpecialChar;
48
+ CircularJSON.specialCharRG = new RegExp(CircularJSON.safeSpecialChar, 'g');
49
+ CircularJSON.indexOf = [].indexOf || function(v){
50
+ for(let i=this.length;i--&&this[i]!==v;);
51
+ return i;
52
+ };
53
+
54
+ CircularJSON.generateReplacer = function (value, replacer, resolve) {
55
+ let
56
+ doNotIgnore = false,
57
+ inspect = !!replacer,
58
+ path = [],
59
+ all = [value],
60
+ seen = [value],
61
+ mapp = [resolve ? CircularJSON.specialChar : '[Circular]'],
62
+ last = value,
63
+ lvl = 1,
64
+ i, fn
65
+ ;
66
+ if (inspect) {
67
+ fn = typeof replacer === 'object' ?
68
+ function (key, value) {
69
+ return key !== '' && CircularJSON.indexOf.call(replacer, key) < 0 ? void 0 : value;
70
+ } :
71
+ replacer;
72
+ }
73
+ return function(key, value) {
74
+ // the replacer has rights to decide
75
+ // if a new object should be returned
76
+ // or if there's some key to drop
77
+ // let's call it here rather than "too late"
78
+ if (inspect) value = fn.call(this, key, value);
79
+
80
+ // first pass should be ignored, since it's just the initial object
81
+ if (doNotIgnore) {
82
+ if (last !== this) {
83
+ i = lvl - CircularJSON.indexOf.call(all, this) - 1;
84
+ lvl -= i;
85
+ all.splice(lvl, all.length);
86
+ path.splice(lvl - 1, path.length);
87
+ last = this;
88
+ }
89
+ // console.log(lvl, key, path);
90
+ if (typeof value === 'object' && value) {
91
+ // if object isn't referring to parent object, add to the
92
+ // object path stack. Otherwise it is already there.
93
+ if (CircularJSON.indexOf.call(all, value) < 0) {
94
+ all.push(last = value);
95
+ }
96
+ lvl = all.length;
97
+ i = CircularJSON.indexOf.call(seen, value);
98
+ if (i < 0) {
99
+ i = seen.push(value) - 1;
100
+ if (resolve) {
101
+ // key cannot contain specialChar but could be not a string
102
+ path.push(('' + key).replace(CircularJSON.specialCharRG, CircularJSON.safeSpecialChar));
103
+ mapp[i] = CircularJSON.specialChar + path.join(CircularJSON.specialChar);
104
+ } else {
105
+ mapp[i] = mapp[0];
106
+ }
107
+ } else {
108
+ value = mapp[i];
109
+ }
110
+ } else {
111
+ if (typeof value === 'string' && resolve) {
112
+ // ensure no special char involved on deserialization
113
+ // in this case only first char is important
114
+ // no need to replace all value (better performance)
115
+ value = value
116
+ .replace(CircularJSON.safeSpecialChar, CircularJSON.escapedSafeSpecialChar)
117
+ .replace(CircularJSON.specialChar, CircularJSON.safeSpecialChar);
118
+ }
119
+ }
120
+ } else {
121
+ doNotIgnore = true;
122
+ }
123
+ return value;
124
+ };
125
+ };
126
+ CircularJSON.stringify = function stringify(value, replacer, space, doNotResolve) {
127
+ return JSON.stringify(
128
+ value,
129
+ CircularJSON.generateReplacer(value, replacer, !doNotResolve),
130
+ space
131
+ );
132
+ };
133
+ /*** end of circular-json ***/
134
+
135
+ // serialize Map as Object, also below in attach
136
+ function simple_map_replacer(key, value) {
137
+ if (value && typeof value === 'object' && value.constructor.name === "Map")
138
+ return Object.fromEntries(value);
139
+ return value;
140
+ }
141
+
142
+ function attachFunctionSource(responder_path, context, func) {
143
+ return `${func} = async function(...method_args) {
144
+ let context = '${context}';
145
+ let func = '${func}';
146
+ let request = [context, func, method_args];
147
+ let responder_path = '${responder_path}';
148
+ if (!global.__responder_socket) {
149
+ return new Promise(function(resolve, reject) {
150
+ setTimeout(function(){
151
+ if (os.platform() == 'win32') {
152
+ let socket = net.connect(responder_path);
153
+ socket.on('connect', function(){
154
+ global.__responder_socket = true;
155
+ socket.destroy();
156
+ resolve(${func}(...method_args));
157
+ })
158
+ socket.on('error', function (err) {
159
+ resolve(${func}(...method_args));
160
+ });
161
+ } else {
162
+ if (fs.existsSync(responder_path)) { global.__responder_socket = true; }
163
+ resolve(${func}(...method_args));
164
+ }
165
+ }, 10)
166
+ });
167
+ }
168
+ function simple_map_replacer(key, value) {
169
+ if (value && typeof value === 'object' && value.constructor.name === "Map")
170
+ return Object.fromEntries(value);
171
+ return value;
172
+ }
173
+ return new Promise(function(resolve, reject) {
174
+ let request_json = JSON.stringify(request, simple_map_replacer);
175
+ let buffer = Buffer.alloc(0);
176
+ let socket = net.connect(responder_path);
177
+ socket.setTimeout(2000);
178
+ socket.on('error', function (err) {
179
+ if (err.syscall === 'connect') {
180
+ // ignore, close will handle
181
+ } else if ((os.platform() == 'win32') && err.message.includes('read EPIPE')) {
182
+ // ignore, close will handle
183
+ } else if ((os.platform() == 'win32') && err.message.includes('write EPIPE')) {
184
+ // ignore, close will handle
185
+ } else { reject(err); }
186
+ });
187
+ socket.on('ready', function () {
188
+ socket.write(request_json + "\x04");
189
+ });
190
+ socket.on('data', function (data) {
191
+ buffer = Buffer.concat([buffer, data]);
192
+ });
193
+ socket.on('timeout', function() {
194
+ socket.destroy();
195
+ reject();
196
+ });
197
+ socket.on('close', function() {
198
+ if (buffer.length > 0) {
199
+ let method_result = JSON.parse(buffer.toString('utf8'));
200
+ if (method_result[0] == 'err') {
201
+ reject(method_result);
202
+ } else {
203
+ resolve(method_result[1]);
204
+ }
205
+ } else {
206
+ resolve(null);
207
+ }
208
+ });
209
+ });
210
+ }`;
211
+ }
212
+
213
+ function createCompatibleContext(uuid, options) {
214
+ let c = vm.createContext();
215
+ vm.runInContext('delete this.console', c, "(execjs)");
216
+ vm.runInContext('delete this.gc', c, "(execjs)");
217
+ contexts[uuid] = { context: c, options: options };
218
+ return c;
219
+ }
220
+
221
+ function createPermissiveContext(uuid, options) {
222
+ let c = vm.createContext({ __responder_socket: false, process: { release: { name: "node" }, env: process.env }, Buffer, clearTimeout, crypto, fs, net, os, require, setTimeout });
223
+ vm.runInContext('global = globalThis;', c);
224
+ contexts[uuid] = { context: c, options: options };
225
+ return c;
226
+ }
227
+
228
+ function formatResult(result) {
229
+ if (typeof result === 'undefined' && result !== null) { return ['ok']; }
230
+ else {
231
+ try { return ['ok', result]; }
232
+ catch (err) { return ['err', ['', err].join(''), err.stack]; }
233
+ }
234
+ }
235
+
236
+ function getContext(uuid) {
237
+ if (contexts[uuid]) { return contexts[uuid].context; }
238
+ else { return null; }
239
+ }
240
+
241
+ function getContextOptions(uuid) {
242
+ let options = { filename: "(execjs)", displayErrors: true };
243
+ if (contexts[uuid].options.timeout)
244
+ options.timeout = contexts[uuid].options.timeout;
245
+ return options;
246
+ }
247
+
248
+ function massageStackTrace(stack) {
249
+ if (stack && stack.indexOf("SyntaxError") == 0)
250
+ return "(execjs):1\n" + stack;
251
+ return stack;
252
+ }
253
+
254
+ let socket_path = process.env.SOCKET_PATH;
255
+ if (!socket_path) { throw 'No SOCKET_PATH given!'; };
256
+ let debug_contexts = [];
257
+ let commands_swapped = false;
258
+
259
+ function swap_commands(c) {
260
+ c.oexec = c.exec;
261
+ c.exec = c.execd;
262
+ c.oeval = c.eval;
263
+ c.eval = c.evald;
264
+ c.oevsc = c.evsc;
265
+ c.evsc = c.evscd;
266
+ commands_swapped = true;
267
+ }
268
+
269
+ let commands = {
270
+ attach: function(input) {
271
+ let context = getContext(input.context);
272
+ let responder_path;
273
+ if (process.platform == 'win32') { responder_path = '\\\\\\\\.\\\\pipe\\\\' + socket_path + '_responder'; }
274
+ else { responder_path = socket_path + '_responder' }
275
+ let result = vm.runInContext(attachFunctionSource(responder_path, input.context, input.func), context, { filename: "(execjs)", displayErrors: true });
276
+ return formatResult(result);
277
+ },
278
+ bench: function (input) {
279
+ if (typeof global.gc === "function") { global.gc(); }
280
+ let context = getContext(input.context);
281
+ let options = getContextOptions(input.context);
282
+ performance.mark('start_bench');
283
+ let result = vm.runInContext(input.source, context, options);
284
+ performance.mark('stop_bench');
285
+ let duration = performance.measure('bench_time', 'start_bench', 'stop_bench').duration;
286
+ performance.clearMarks();
287
+ performance.clearMeasures();
288
+ if (typeof global.gc === "function") { global.gc(); }
289
+ return formatResult({ result: result, duration: duration });
290
+ },
291
+ create: function (input) {
292
+ let context = createCompatibleContext(input.context, input.options);
293
+ let result = vm.runInContext(input.source, context, getContextOptions(input.context));
294
+ return formatResult(result);
295
+ },
296
+ created: function (input) {
297
+ debug_contexts.push(input.context);
298
+ if (!commands_swapped) {
299
+ swap_commands(commands);
300
+ let result = eval(input.source);
301
+ return formatResult(result);
302
+ } else { return formatResult(true) }
303
+ },
304
+ createp: function (input) {
305
+ let context = createPermissiveContext(input.context, input.options);
306
+ let result = vm.runInContext(input.source, context, getContextOptions(input.context));
307
+ return formatResult(result);
308
+ },
309
+ deleteContext: function(uuid) {
310
+ delete contexts[uuid];
311
+ delete scripts[uuid]
312
+ return ['ok', Object.keys(contexts).length];
313
+ },
314
+ exit: function(code) {
315
+ process_exit = code;
316
+ return ['ok'];
317
+ },
318
+ exec: function (input) {
319
+ let result = vm.runInContext(input.source, getContext(input.context), getContextOptions(input.context));
320
+ return formatResult(result);
321
+ },
322
+ execd: function(input) {
323
+ if (debug_contexts.includes(input.context)) {
324
+ let result = eval(input.source);
325
+ return formatResult(result);
326
+ } else {
327
+ return commands.oexec(input);
328
+ }
329
+ },
330
+ eval: function (input) {
331
+ if (input.source.match(/^\s*{/)) { input.source = `(${input.source})`; }
332
+ else if (input.source.match(/^\s*function\s*\(/)) { input.source = `(${input.source})`; }
333
+ let result = vm.runInContext(input.source, getContext(input.context), getContextOptions(input.context));
334
+ return formatResult(result);
335
+ },
336
+ evald: function(input) {
337
+ if (debug_contexts.includes(input.context)) {
338
+ if (input.source.match(/^\s*{/)) { input.source = `(${input.source})`; }
339
+ else if (input.source.match(/^\s*function\s*\(/)) { input.source = `(${input.source})`; }
340
+ let result = eval(input.source);
341
+ return formatResult(result);
342
+ } else {
343
+ return commands.oeval(input);
344
+ }
345
+ },
346
+ scsc: function (input) {
347
+ if (!scripts[input.context]) { scripts[input.context] = {}; }
348
+ scripts[input.context][input.key] = new vm.Script(input.source);
349
+ return formatResult(true);
350
+ },
351
+ evsc: function(input) {
352
+ let result = scripts[input.context][input.key].runInContext(getContext(input.context));
353
+ return formatResult(result);
354
+ },
355
+ evscd: function(input) {
356
+ if (debug_contexts.includes(input.context)) {
357
+ let result = scripts[input.context][input.key].runInThisContext();
358
+ return formatResult(result);
359
+ } else {
360
+ return commands.oevsc(input);
361
+ }
362
+ }
363
+ // ctxo: function (input) {
364
+ // return formatResult(getContextOptions(input.context));
365
+ // },
366
+ };
367
+
368
+ let server = net.createServer(function(s) {
369
+ let received_data = [];
370
+
371
+ s.on('data', function (data) {
372
+ received_data.push(data);
373
+ if (data[data.length - 1] !== 4) { return; }
374
+
375
+ let request = received_data.join('').toString('utf8');
376
+ request = request.substring(0, request.length - 1);
377
+ received_data = [];
378
+
379
+ let input, result;
380
+ let outputJSON = '';
381
+
382
+ try { input = JSON.parse(request); }
383
+ catch(err) {
384
+ outputJSON = JSON.stringify(['err', ['', err].join(''), err.stack]);
385
+ s.write([outputJSON, "\x04"].join(''));
386
+ return;
387
+ }
388
+
389
+ try { result = commands[input.cmd](input.args); }
390
+ catch (err) {
391
+ outputJSON = JSON.stringify(['err', ['', err].join(''), massageStackTrace(err.stack)]);
392
+ s.write([outputJSON, "\x04"].join(''));
393
+ return;
394
+ }
395
+
396
+ try { outputJSON = JSON.stringify(result, simple_map_replacer); }
397
+ catch(err) {
398
+ if (err.message.includes('circular')) { outputJSON = CircularJSON.stringify(result, simple_map_replacer); }
399
+ else { outputJSON = JSON.stringify([['', err].join(''), err.stack]); }
400
+ s.write([outputJSON, "\x04"].join(''));
401
+ if (process_exit !== false) { process.exit(process_exit); }
402
+ return;
403
+ }
404
+
405
+ try { s.write([outputJSON, "\x04"].join('')); }
406
+ catch (err) {}
407
+ if (process_exit !== false) { process.exit(process_exit); }
408
+ });
409
+ });
410
+
411
+ if (process.platform == 'win32') { server.listen('\\\\.\\pipe\\' + socket_path); }
412
+ else { server.listen(socket_path); }
@@ -0,0 +1,202 @@
1
+ module Speednode
2
+ class Runtime < ExecJS::Runtime
3
+ class Context < ::ExecJS::Runtime::Context
4
+ def self.finalize(runtime, uuid)
5
+ proc do
6
+ runtime.unregister_context(uuid)
7
+ end
8
+ end
9
+
10
+ def initialize(runtime, source = "", options = {})
11
+ @runtime = runtime
12
+ @uuid = SecureRandom.uuid
13
+ @runtime.register_context(@uuid, self)
14
+ @permissive = !!options.delete(:permissive)
15
+ @debug = @permissive ? !!options.delete(:debug) { false } : false
16
+ @debug = false unless ENV['NODE_OPTIONS']&.include?('--inspect')
17
+ @vm = @runtime.vm
18
+ @timeout = options[:timeout] ? options[:timeout]/1000 : 600
19
+
20
+ filename = options.delete(:filename)
21
+ source = File.read(filename) if filename
22
+
23
+ begin
24
+ source = source.encode(Encoding::UTF_8)
25
+ rescue
26
+ source = source.force_encoding('UTF-8')
27
+ end
28
+
29
+ ObjectSpace.define_finalizer(self, self.class.finalize(@runtime, @uuid))
30
+
31
+ if @debug && @permissive
32
+ raw_created(source, options)
33
+ elsif @permissive
34
+ raw_createp(source, options)
35
+ else
36
+ raw_create(source, options)
37
+ end
38
+
39
+ add_script(key: :_internal_exec_fin, source: '!global.__LastExecutionFinished')
40
+ end
41
+
42
+ # def options
43
+ # @vm.context_options(@uuid)
44
+ # end
45
+
46
+ def attach(func, procedure = nil, &block)
47
+ raise "#attach requires a permissive context." unless @permissive
48
+ run_block = block_given? ? block : procedure
49
+ ::Speednode::Runtime.attach_proc(@uuid, func, run_block)
50
+ @vm.attach(@uuid, func)
51
+ end
52
+
53
+ def await(source)
54
+ raw_eval <<~JAVASCRIPT
55
+ (async () => {
56
+ global.__LastExecutionFinished = false;
57
+ global.__LastResult = null;
58
+ global.__LastErr = null;
59
+ global.__LastResult = await #{source};
60
+ global.__LastExecutionFinished = true;
61
+ })().catch(function(err) {
62
+ global.__LastResult = null;
63
+ global.__LastErr = err;
64
+ global.__LastExecutionFinished = true;
65
+ })
66
+ JAVASCRIPT
67
+ await_result
68
+ end
69
+
70
+ def bench(source, _options = nil)
71
+ raw_bench(source) if /\S/ =~ source
72
+ end
73
+
74
+ def call(identifier, *args)
75
+ raw_eval("#{identifier}.apply(this, #{::Oj.dump(args, mode: :strict)})")
76
+ end
77
+
78
+ def eval(source, _options = nil)
79
+ raw_eval(source) if /\S/ =~ source
80
+ end
81
+
82
+ def exec(source, _options = nil)
83
+ raw_exec("(function(){#{source}})()")
84
+ end
85
+
86
+ def eval_script(key:)
87
+ extract_result(@vm.evsc(@uuid, key))
88
+ end
89
+
90
+ def add_script(key:, source:)
91
+ extract_result(@vm.scsc(@uuid, key, source.encode(Encoding::UTF_8)))
92
+ end
93
+
94
+ def permissive?
95
+ @permissive
96
+ end
97
+
98
+ def permissive_eval(source, _options = nil)
99
+ raise "Context not permissive!" unless @permissive
100
+ raw_eval(source) if /\S/ =~ source
101
+ end
102
+
103
+ def permissive_exec(source, _options = nil)
104
+ raise "Context not permissive!" unless @permissive
105
+ raw_exec("(function(){#{source}})()")
106
+ end
107
+
108
+ def stop
109
+ @runtime.unregister_context(@uuid)
110
+ end
111
+
112
+ protected
113
+
114
+ def raw_bench(source)
115
+ extract_result(@vm.bench(@uuid, source.encode(Encoding::UTF_8)))
116
+ end
117
+
118
+ def raw_eval(source)
119
+ extract_result(@vm.eval(@uuid, source.encode(Encoding::UTF_8)))
120
+ end
121
+
122
+ def raw_exec(source)
123
+ extract_result(@vm.exec(@uuid, source.encode(Encoding::UTF_8)))
124
+ end
125
+
126
+ def raw_create(source, options)
127
+ source = source.encode(Encoding::UTF_8)
128
+ result = @vm.create(@uuid, source, options)
129
+ extract_result(result)
130
+ end
131
+
132
+ def raw_created(source, options)
133
+ source = source.encode(Encoding::UTF_8)
134
+ result = @vm.created(@uuid, source, options)
135
+ extract_result(result)
136
+ end
137
+
138
+ def raw_createp(source, options)
139
+ source = source.encode(Encoding::UTF_8)
140
+ result = @vm.createp(@uuid, source, options)
141
+ extract_result(result)
142
+ end
143
+
144
+ def extract_result(output)
145
+ if output[0] == 'ok'
146
+ output[1]
147
+ else
148
+ _status, value, stack = output
149
+ stack ||= ""
150
+ stack = stack.split("\n").map do |line|
151
+ line.sub(" at ", "").strip
152
+ end
153
+ stack.reject! do |line|
154
+ line.include?('(node:') ||
155
+ line.include?('lib\speednode\runner.js') ||
156
+ line.include?('lib/speednode/runner.js')
157
+ end
158
+ stack.shift unless stack[0].to_s.include?("(execjs)")
159
+ error_class = value =~ /SyntaxError:/ ? ExecJS::RuntimeError : ExecJS::ProgramError
160
+ error = error_class.new(value)
161
+ error.set_backtrace(stack + caller)
162
+ raise error
163
+ end
164
+ end
165
+
166
+ def await_result
167
+ start_time = ::Time.now
168
+ while eval_script(key: :_internal_exec_fin) && !timed_out?(start_time)
169
+ sleep 0.005
170
+ end
171
+ result = exec <<~JAVASCRIPT
172
+ if (global.__LastExecutionFinished === true) {
173
+ var err = global.__LastErr;
174
+ var result = global.__LastResult;
175
+
176
+ global.__LastErr = null;
177
+ global.__LastResult = null;
178
+ global.__LastExecutionFinished = false;
179
+
180
+ if (err) { return ['err', ['', err].join(''), err.stack]; }
181
+ else if (typeof result === 'undefined' && result !== null) { return ['ok']; }
182
+ else {
183
+ try { return ['ok', result]; }
184
+ catch (err) { return ['err', ['', err].join(''), err.stack]; }
185
+ }
186
+ } else {
187
+ var new_err = new Error('Last command did not yet finish execution!');
188
+ return ['err', ['', new_err].join(''), new_err.stack];
189
+ }
190
+ JAVASCRIPT
191
+ extract_result(result)
192
+ end
193
+
194
+ def timed_out?(start_time)
195
+ if (::Time.now - start_time) > @timeout
196
+ raise "Speednode: Command Execution timed out!"
197
+ end
198
+ false
199
+ end
200
+ end
201
+ end
202
+ end