sandbox.js

/**
 * This file is part of Domotz Agent.
 *
 * @license
 * Domotz Agent is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * Domotz Agent is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with Domotz Agent.  If not, see <http://www.gnu.org/licenses/>.
 *
 * @requires sandbox/library/device
 * @requires sandbox/library/logger
 * @requires sandbox/library/crypto
 * @requires sandbox/library/buffer
 * @copyright Copyright (C) Domotz Inc
 */

const deviceLibrary = require('./library/device');
const domotzContext = require('./context');
const logger = require('./library/logger').logger;
const util = require('util');
const vm = require('vm');

const driverTypes = require('./constants').driverTypes;
const errorTypes = require('./constants').errorTypes;
const rttOffset = 5000;

var newConsole = null;
var startTime = null;
var agentDriverSettings = null;

function failure(errorType) {
    if (!errorType || !(errorType in errorTypes)) {
        newConsole.warning('Error type %s is not recognized, converting to generic error', errorType);
        errorType = errorTypes.GENERIC_ERROR;
    }
    process.send({
        outcome: 'failure',
        errorType: errorType,
        log: newConsole.get(),
        elapsed: new Date() - startTime,
    });
}

function successConfigurationManagement(configurationData) {
    newConsole.debug('Success called');
    process.send({
        outcome: 'success',
        configurationData: configurationData,
        log: newConsole.get(),
        elapsed: new Date() - startTime,
    });
}

function successGeneric(variables, table) {
    newConsole.debug('Success called');
    var tables;
    // In case the user passes the table object as first argument
    if (!table && variables && variables.isTable) {
        tables = [variables.getResult()];
        variables = null;
    }
    if (table && table.isTable) {
        tables = [table.getResult()];
    }
    if (variables !== null && variables !== undefined) {
        validateVariables(variables);
    }
    process.send({
        outcome: 'success',
        variables: variables,
        tables: tables,
        log: newConsole.get(),
        elapsed: new Date() - startTime,
    });
}

function validateVariables(variables) {
    var uids = [];
    for (var i = 0; i < variables.length; i++) {
        var uid = variables[i].uid;
        if (uids.indexOf(uid) > -1) {
            newConsole.error('Duplicate variable uid received: ' + uid);
            failure('PARSING_ERROR');
            return;
        }
        uids.push(uid);
    }
}

function timeoutReached() {
    newConsole.warning('Timeout expired');
    failure(errorTypes.TIMEOUT_ERROR);
}

function buildDomotzContext(message, newConsole) {
    var context = domotzContext.createDomotzContext(message, newConsole);

    context.failure = failure;
    if (message.driverType === driverTypes.CONFIGURATION_MANAGEMENT) {
        context.success = successConfigurationManagement;
    } else if (message.driverType === driverTypes.GENERIC) {
        context.success = successGeneric;
    } else {
        context.success = successGeneric;
    }

    return context;
}

process.on('message', function (message) {
    agentDriverSettings = message.agentDriverSettings;
    newConsole = logger(message.logLevel || 'info', agentDriverSettings.max_log_entries);
    var context = vm.createContext({
        D: buildDomotzContext(message, newConsole),
        import: function () {
            newConsole.error('Error: Import not possible in sandbox');
            failure(errorTypes.IMPORT_NOT_ALLOWED);
        },
        require: function () {
            newConsole.error('Error: Require not possible in sandbox');
            failure(errorTypes.REQUIRE_NOT_ALLOWED);
        },
        console: newConsole,
    });

    // ToDo: check if this should not be included in the D context directly
    if (message.device) {
        context.D.device = deviceLibrary.device(message.device, agentDriverSettings, newConsole);
        newConsole.debug('Created new device for script execution: %s', util.inspect(message.device));
    }

    const script = new vm.Script(message.script);
    var timeout = message.timeout || 5000;
    setTimeout(timeoutReached, timeout + rttOffset);

    startTime = new Date();
    script.runInContext(context, {
        displayErrors: true,
        fileName: 'custom script',
        lineOffset: 0,
        timeout: timeout,
    });
});

process.on('uncaughtException', function (err) {
    var errorText = err.toString();
    if (errorText.indexOf('timed out') !== -1) {
        newConsole.error('Script Execution timed out after: ' + (new Date() - startTime) + ' ms');
        failure(errorTypes.TIMEOUT_ERROR);
    } else {
        newConsole.error('Caught exception: ' + errorText + '\n' + err.stack);
        failure(errorTypes.GENERIC_ERROR);
    }
});