-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathindex.js
447 lines (383 loc) · 11.1 KB
/
index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
'use strict';
var EventEmitter = require('eventemitter3')
, BaseImage = require('alcatraz')
, slice = Array.prototype.slice
, iframe = require('frames');
/**
* Representation of a single container.
*
* Options:
*
* - retries; When an error occurs, how many times should we attempt to restart
* the code before we automatically stop() the container.
* - stop; Stop the container when an error occurs.
* - timeout; How long can a ping packet timeout before we assume that the
* container has died and should be restarted.
*
* @constructor
* @param {Element} mount The element we should attach to.
* @param {String} id A unique id for this container.
* @param {String} code The actual that needs to run within the sandbox.
* @param {Object} options Container configuration.
* @api private
*/
function Container(mount, id, code, options) {
if (!(this instanceof Container)) return new Container(mount, id, code, options);
if ('object' === typeof code) {
options = code;
code = null;
}
options = options || {};
this.i = iframe(mount, id); // The generated iframe.
this.mount = mount; // Mount point of the container.
this.console = []; // Historic console.* output.
this.setTimeout = {}; // Stores our setTimeout references.
this.id = id; // Unique id.
this.readyState = Container.CLOSED; // The readyState of the container.
this.created = +new Date(); // Creation EPOCH.
this.started = null; // Start EPOCH.
this.retries = 'retries' in options // How many times should we reload
? +options.retries || 3
: 3;
this.timeout = 'timeout' in options // Ping timeout before we reboot.
? +options.timeout || 1050
: 1050;
//
// Initialise as an EventEmitter before we start loading in the code.
//
EventEmitter.call(this);
//
// Optional code to load in the container and start it directly.
//
if (code) this.load(code).start();
}
//
// The container inherits from the EventEmitter3.
//
Container.prototype = new EventEmitter();
Container.prototype.constructor = Container;
/**
* Internal readyStates for the container.
*
* @type {Number}
* @private
*/
Container.CLOSING = 1;
Container.OPENING = 2;
Container.CLOSED = 3;
Container.OPEN = 4;
/**
* Start a new ping timeout.
*
* @api private
*/
Container.prototype.ping = function ping() {
if (this.setTimeout.pong) clearTimeout(this.setTimeout.pong);
var self = this;
this.setTimeout.pong = setTimeout(function pong() {
self.onmessage({
type: 'error',
scope: 'iframe.timeout',
args: [
'the iframe is no longer responding with ping packets'
]
});
}, this.timeout);
return this;
};
/**
* Retry loading the code in the iframe. The container will be restored to a new
* state or completely reset the iframe.
*
* @api private
*/
Container.prototype.retry = function retry() {
switch (this.retries) {
//
// This is our last attempt, we've tried to have the iframe restart the code
// it self, so for our last attempt we're going to completely create a new
// iframe and re-compile the code for it.
//
case 1:
this.stop(); // Clear old iframe and nuke it's references
this.i = iframe(this.mount, this.id);
this.load(this.image.source).start();
break;
//
// No more attempts left.
//
case 0:
this.stop();
this.emit('end');
return;
//
// By starting and stopping (and there for removing and adding it back to
// the DOM) the iframe will reload it's HTML and the added code.
//
default:
this.stop().start();
break;
}
this.emit('retry', this.retries);
this.retries--;
return this;
};
/**
* Inspect the container to get some useful statistics about it and it's health.
*
* @returns {Object}
* @api public
*/
Container.prototype.inspect = function inspect() {
if (!this.i.attached()) return {};
var date = new Date()
, memory;
//
// Try to read out the `performance` information from the iframe.
//
if (this.i.window() && this.i.window().performance) {
memory = this.i.window().performance.memory;
}
memory = memory || {};
return {
readyState: this.readyState,
retries: this.retries,
uptime: this.started ? (+date) - this.started : 0,
date: date,
memory: {
limit: memory.jsHeapSizeLimit || 0,
total: memory.totalJSHeapSize || 0,
used: memory.usedJSHeapSize || 0
}
};
};
/**
* Parse and process incoming messages from the iframe. The incoming messages
* should be objects that have a `type` property. The main reason why we have
* this as a separate method is to give us flexibility. We are leveraging iframes
* at the moment, but in the future we might want to leverage WebWorkers for the
* sand boxing of JavaScript.
*
* @param {Object} packet The incoming message.
* @returns {Boolean} Message was handled y/n.
* @api private
*/
Container.prototype.onmessage = function onmessage(packet) {
if ('object' !== typeof packet) return false;
if (!('type' in packet)) return false;
packet.args = packet.args || [];
switch (packet.type) {
//
// The code in the iframe used the `console` method.
//
case 'console':
this.console.push({
scope: packet.scope,
epoch: +new Date(),
args: packet.args
});
if (packet.attach) {
this.emit.apply(this, ['attach::'+ packet.scope].concat(packet.args));
this.emit.apply(this, ['attach', packet.scope].concat(packet.args));
}
break;
//
// An error happened in the iframe, process it.
//
case 'error':
var failure = packet.args[0].stack ? packet.args[0] : new Error(packet.args[0]);
failure.scope = packet.scope || 'generic';
this.emit('error', failure);
this.retry();
break;
//
// The iframe and it's code has been loaded.
//
case 'load':
if (this.readyState !== Container.OPEN) {
this.readyState = Container.OPEN;
this.emit('start');
}
break;
//
// The iframe is unloading, attaching
//
case 'unload':
if (this.readyState !== Container.CLOSED) {
this.readyState = Container.CLOSED;
this.emit('stop');
}
break;
//
// We've received a ping response from the iframe, so we know it's still
// running as intended.
//
case 'ping':
this.ping();
this.emit('ping');
break;
//
// Handle unknown package types by just returning false after we've emitted
// it as an `regular` message.
//
default:
this.emit.apply(this, ['message'].concat(packet.args));
return false;
}
return true;
};
/**
* Small wrapper around sandbox evaluation.
*
* @param {String} cmd The command to executed in the iframe.
* @param {Function} fn Callback
* @api public
*/
Container.prototype.eval = function evil(cmd, fn) {
var data;
try {
data = this.i.add().window().eval(cmd);
} catch (e) {
return fn(e);
}
return fn(undefined, data);
};
/**
* Start the container.
*
* @returns {Container}
* @api public
*/
Container.prototype.start = function start() {
this.readyState = Container.OPENING;
var self = this;
/**
* Simple argument proxy.
*
* @api private
*/
function onmessage() {
self.onmessage.apply(self, arguments);
}
//
// Code loading is an sync process, but this COULD cause huge stack traces
// and really odd feedback loops in the stack trace. So we deliberately want
// to destroy the stack trace here.
//
this.setTimeout.start = setTimeout(function async() {
var doc = self.i.document();
//
// No doc.open, the iframe has already been destroyed!
//
if (!doc.open || !self.i) return;
//
// We need to open and close the iframe in order for it to trigger an onload
// event. Certain scripts might require in order to execute properly.
//
doc.open();
doc.write([
'<!doctype html>',
'<html><head>',
//
// iFrames can generate pointless requests by searching for a favicon.
// This can add up to three extra requests for a simple iframe. To battle
// this, we need to supply an empty icon.
//
// @see http://stackoverflow.com/questions/1321878/how-to-prevent-favicon-ico-requests
//
'<link rel="icon" href="data:;base64,=">',
'</head><body>'
].join('\n'));
//
// Introduce our messaging variable, this needs to be done before we eval
// our code. If we set this value before the setTimeout, it doesn't work in
// Opera due to reasons.
//
self.i.window()[self.id] = onmessage;
self.eval(self.image.toString(), function evil(err) {
if (err) return self.onmessage({
type: 'error',
scope: 'iframe.eval',
args: [ err ]
});
});
//
// If executing the code results to an error we could actually be stopping
// and removing the iframe from the source before we're able to close it.
// This is because executing the code inside the iframe is actually an sync
// operation.
//
if (doc.close) doc.close();
}, 0);
//
// We can only write to the iframe if it's actually in the DOM. The `i.add()`
// method ensures that the iframe is added to the DOM.
//
this.i.add();
this.started = +new Date();
return this;
};
/**
* Stop running the code inside the container.
*
* @returns {Container}
* @api private
*/
Container.prototype.stop = function stop() {
if (this.readyState !== Container.CLOSED && this.readyState !== Container.CLOSING) {
this.readyState = Container.CLOSING;
}
this.i.remove();
//
// Opera doesn't support unload events. So adding an listener inside the
// iframe for `unload` doesn't work. This is the only way around it.
//
this.onmessage({ type: 'unload' });
//
// It's super important that this removed AFTER we've cleaned up all other
// references as we might need to communicate back to our container when we
// are unloading or when an `unload` event causes an error.
//
this.i.window()[this.id] = null;
//
// Clear the timeouts.
//
for (var timeout in this.setTimeout) {
clearTimeout(this.setTimeout[timeout]);
delete this.setTimeout[timeout];
}
return this;
};
/**
* Load the given code as image on to the container.
*
* @param {String} code The code that should run on the container.
* @returns {Container}
* @api public
*/
Container.prototype.load = function load(code) {
this.image = new BaseImage(this.id, code);
return this;
};
/**
* Completely destroy the given container and ensure that all references are
* nuked so we can clean up as much memory as possible.
*
* @returns {Container}
* @api private
*/
Container.prototype.destroy = function destroy() {
if (!this.i) return this;
this.stop();
//
// Remove all possible references to release as much memory as possible.
//
this.mount = this.image = this.id = this.i = this.created = null;
this.console.length = 0;
this.removeAllListeners();
return this;
};
//
// Expose the module.
//
module.exports = Container;