lychees's picture
Upload 569 files
87b3b3a
Game.prototype.verbotenWords = [
'.call', 'call(', 'apply', 'bind', // prevents arbitrary code execution
'prototype', // prevents messing with prototypes
'debugger', // prevents pausing execution
'delete', // prevents removing items
'constructor', // prevents retrieval of Function using an instance of it
'window', // prevents setting "window.[...] = map", etc.
'top', // prevents user code from escaping the iframe
'validate', 'onExit', 'objective', // don't let players rewrite these methods
'\\u' // prevents usage of arbitrary code through unicode escape characters, see issue #378
];
Game.prototype.allowedTime = 2000; // for infinite loop prevention
var DummyDisplay = function () {
this.clear = function () {};
this.drawAll = function () {};
this.drawObject = function () {};
this.drawText = function () {};
this.writeStatus = function () {};
};
Game.prototype.validate = function(allCode, playerCode, restartingLevelFromScript) {
var game = this;
try {
for (var i = 0; i < this.verbotenWords.length; i++) {
var badWord = this.verbotenWords[i];
if (playerCode.indexOf(badWord) > -1) {
throw "You are not allowed to use '" + badWord + "'!";
}
}
var dummyMap = new Map(new DummyDisplay(), this);
dummyMap._dummy = true;
dummyMap._setProperties(this.editor.getProperties().mapProperties);
// modify the code to always check time to prevent infinite loops
allCode = allCode.replace(/\)\s*{/g, ") {"); // converts Allman indentation -> K&R
allCode = allCode.replace(/\n\s*while\s*\((.*)\)/g, "\nfor (dummy=0;$1;)"); // while -> for
allCode = $.map(allCode.split('\n'), function (line, i) {
return line.replace(/for\s*\((.*);(.*);(.*)\)\s*{/g,
"for ($1, startTime = Date.now();$2;$3){" +
"if (Date.now() - startTime > " + game.allowedTime + ") {" +
"throw '[Line " + (i+1) + "] TimeOutException: Maximum loop execution time of " + game.allowedTime + " ms exceeded.';" +
"}");
}).join('\n');
allCode = "'use strict';var validateLevel,onExit,objective\n"+allCode;
allCode = allCode+"\n({startLevel:startLevel,validateLevel:validateLevel,onExit:onExit,objective:objective})";
if (this._debugMode) {
console.log(allCode);
}
var allowjQuery = dummyMap._properties.showDummyDom;
// setup iframe in which code is run. As a side effect, this sets `this._eval`
// and `this.SyntaxError` correctly.
var userEnv = this.initIframe(allowjQuery);
// evaluate the code to get startLevel() and (opt) validateLevel() methods
var userOutput = this._eval(allCode);
// start the level on a dummy map to validate
this._setPlayerCodeRunning(true);
userOutput.startLevel(dummyMap);
this._setPlayerCodeRunning(false);
// re-run to check if the player messed with startLevel
this._startOfStartLevelReached = false;
this._endOfStartLevelReached = false;
dummyMap._reset();
this._setPlayerCodeRunning(true);
userOutput.startLevel(dummyMap);
this._setPlayerCodeRunning(false);
// does startLevel() execute fully?
// (if we're restarting a level after editing a script, we can't test for this
// - nor do we care)
if (!this._startOfStartLevelReached && !restartingLevelFromScript) {
throw 'startLevel() has been tampered with!';
}
if (!this._endOfStartLevelReached && !restartingLevelFromScript) {
throw 'startLevel() returned prematurely!';
}
this.validateLevel = function () { return true; };
// does validateLevel() succeed?
if (typeof(userOutput.validateLevel) === "function") {
this.validateLevel = userOutput.validateLevel;
this._setPlayerCodeRunning(true);
userOutput.validateLevel(dummyMap);
this._setPlayerCodeRunning(false);
}
dummyMap._clearIntervals();
this.onExit = function () { return true; };
if (typeof userOutput.onExit === "function") {
this.onExit = userOutput.onExit;
}
this.objective = function () { return false; };
if (typeof userOutput.objective === "function") {
this.objective = userOutput.objective;
}
return userOutput.startLevel;
} catch (e) {
// cleanup
this._setPlayerCodeRunning(false);
if (dummyMap) {
dummyMap._clearIntervals();
}
var exceptionText = e.toString();
if (e instanceof this.SyntaxError) {
var lineNum = this.findSyntaxError(allCode, e.message);
if (lineNum) {
exceptionText = "[Line " + lineNum + "] " + exceptionText;
}
}
this.display.appendError(exceptionText);
// throw e; // for debugging
return null;
}
};
// makes sure nothing un-kosher happens during a callback within the game
// e.g. item collison; function phone
Game.prototype.validateCallback = function(callback, throwExceptions) {
var savedException = null;
var exceptionFound = false;
try {
// run the callback and check for forbidden method calls
try {
this._setPlayerCodeRunning(true);
var result = callback();
this._setPlayerCodeRunning(false);
} catch (e) {
// cleanup
this._setPlayerCodeRunning(false);
if (e.toString().indexOf("Forbidden method call") > -1 ||
e.toString().indexOf("Attempt to modify private property") > -1 ||
e.toString().indexOf("Attempt to read private property") > -1) {
// display error, disable player movement
this.display.appendError(e.toString(), "%c{red}Please reload the level.");
this.sound.playSound('static');
this.map.getPlayer()._canMove = false;
this.map._callbackValidationFailed = true;
this.map._clearIntervals();
// throw e; // for debugging
return;
} else {
// other exceptions are fine here, but be sure to run validation before passing them up
savedException = e;
exceptionFound = true;
}
}
// check if validator still passes
try {
if (typeof(this.validateLevel) === 'function') {
this._setPlayerCodeRunning(true);
this.validateLevel(this.map);
this._setPlayerCodeRunning(false);
}
} catch (e) {
this._setPlayerCodeRunning(false);
// validation failed - not much to do here but restart the level, unfortunately
this.display.appendError(e.toString(), "%c{red}Validation failed! Please reload the level.");
// play error sound
this.sound.playSound('static');
// disable player movement
this.map.getPlayer()._canMove = false;
this.map._callbackValidationFailed = true;
this.map._clearIntervals();
return;
}
// refresh the map (unless it refreshes automatically), just in case
if(!this.map._properties.refreshRate) {
this.map.refresh();
}
if(exceptionFound) {
throw savedException;
}
return result;
} catch (e) {
this.map.writeStatus(e.toString());
// throw e; // for debugging
if (throwExceptions) {
throw e;
}
}
};
Game.prototype.validateAndRunScript = function (code) {
try {
// Game.prototype.blah => game.blah
code = code.replace(/Game.prototype/, 'this');
// Blah => game._blahPrototype
code = code.replace(/function Map/, 'this._mapPrototype = function');
code = code.replace(/function Player/, 'this._playerPrototype = function');
new Function(code).bind(this).call(); // bind the function to current instance of game!
if (this._mapPrototype) {
// re-initialize map if necessary
this.map._reset(); // for cleanup
this.map = new this._mapPrototype(this.display, this);
}
// re-initialize objects if necessary
this.objects = this.getListOfObjects();
// and restart current level from saved state
var savedState = this.editor.getGoodState(this._currentLevel);
this._evalLevelCode(savedState['code'], savedState['playerCode'], false, true);
} catch (e) {
this.display.writeStatus(e.toString());
//throw e; // for debugging
}
}
var allowedGlobals = {
// These four are allowed primarily to avoid confusing the programmer
'Object':true,
'Array':true,
'String':true,
'Number':true,
// Math.Floor and Math.random are used in many levels
'Math':true,
// parseInt is used in a few bonus levels
'parseInt':true,
// Date is used by the infinite loop prevention code
'Date':true
}
Game.prototype.initIframe = function(allowjQuery){
var iframe = $("#user_code")[0];
// reset any state in the iframe
iframe.src = "about:blank";
var iframewindow = iframe.contentWindow;
if (iframewindow.eval) {
this._eval = iframewindow.eval;
this.SyntaxError = iframewindow.SyntaxError;
}
// delete any unwated global variables in the iframe
function purgeObject(object) {
var globals = Object.getOwnPropertyNames(object);
for (var i = 0;i < globals.length;i++) {
var variable = globals[i];
if (!allowedGlobals.hasOwnProperty(variable)) {
delete object[variable];
}
}
var prototype = Object.getPrototypeOf(object);
if (prototype && prototype != iframewindow.Object.prototype) {
purgeObject(prototype);
}
}
purgeObject(iframewindow);
// document can't be deleted, so purge it instead
purgeObject(iframewindow.document);
// add in any necessary global variables
iframewindow.ROT = {Map: {DividedMaze: ROT.Map.DividedMaze }}
if (allowjQuery) {
// this is not secure, however it doesn't matter since the only level
// with showDummyDom set has no editable code
iframewindow.$ = iframewindow.jQuery = jQuery;
}
return iframewindow;
}
// Object security
// takes an object and modifies it so that all properties starting with `_`
// throw an error when accessed in level code,
// and that all functions are unwritable
Game.prototype.secureObject = function(object, objecttype) {
for (var prop in object) {
if(prop == "_startOfStartLevelReached" || prop == "_endOfStartLevelReached"){
// despite starting with an _, these two properties are intended to be called from map code
continue;
}
if(prop[0] == "_"){
this.secureProperty(object, prop, objecttype);
} else if (typeof object[prop] == "function") {
Object.defineProperty(object, prop, {
configurable:false,
writable:false
});
}
}
}
Game.prototype.secureProperty = function(object, prop, objecttype){
var val = object[prop];
var game = this;
Object.defineProperty(object, prop, {
configurable:false,
enumerable:false,
get:function(){
if (game._isPlayerCodeRunning()) {
throw "Attempt to read private property " + objecttype + "." + prop;
}
return val;
},
set:function(newValue){
if(game._isPlayerCodeRunning()) {
throw "Attempt to modify private property " + objecttype + "." + prop;
}
val = newValue
}
});
}
// awful awful awful method that tries to find the line
// of code where a given error occurs
Game.prototype.findSyntaxError = function(code, errorMsg) {
var lines = code.split('\n');
// One line at the top is the added declarations and doesn't
// correspond to any real editor code
var phantomLines = 1;
for (var i = 1; i <= lines.length; i++) {
var line = lines[i - 1];
var startStartLevel = "map._startOfStartLevelReached()";
var endStartLevel = "map._endOfStartLevelReached()";
if (line == startStartLevel || line == endStartLevel ) {
// This line was added by the editor and doesn't show up to the user
// so shouldn't be counted.
phantomLines += 1;
}
var testCode = lines.slice(0, i).join('\n');
try {
this._eval("'use strict';" + testCode);
} catch (e) {
if (e.message === errorMsg) {
return i - phantomLines;
}
}
}
return null;
};