Выполнение пользовательского кода в nodejs

Мне тут по долгу службы пришлось столкнуться с node.js. Это такой серверный JavaScript, если что. И в его основе лежит V8 — JS-движок от Google, который стоит в Google Chrome.

Весьма вероятно, что в ближайшее время вы увидите еще много постов про node.js в этом блоге, а пока вот вам практическая задача.

У нас есть сервис с некоторым набором данных, неважно каких. Этот сервис позволяет выполнять пользовательские операции над этими данными (например, валидацию). Т.е. любой пользователь может зайти, написать что-то вроде setValid(data.a == 1) — и код будет выполнен. Понятное дело, что сразу возникает вопрос безопасности. Ведь никто не мешает пользователю подключить библиотеку работы с файловой системой и сделать что-нибудь нехорошее:

var fs = require('fs');
fs.readdir('~', function(err, files) {
  files.forEach(function (file) {
    fs.unlink('~/' + file);
  });
});

Или может вообще не возвращать результат, а загрузить процессор на 100%. Мы ведь ничего не можем гарантировать, когда дело доходит по пользовательского кода. Да ведь там могут быть и просто синтаксические ошибки.

Итак, мое решение.

Во-первых, весь пользовательский код выполняется в отдельном процессе. Этот процесс принудительно завершается по истечении некоторого времени (секунд 10 или около того).

// parent.js
var child = child_process.fork('./child.js');
child.on('message', function(message) {
    // message будет содержать ответ от потомка
});
setTimeout(function () {
    child.kill();
    console.log('Task was killed by timeout');
}, 10000);
child.send(data); // Оправляем параметры для выполнения
// child.js
process.on('message', function (message) {
    // message будет содержать параметры выполнения
    // TODO ????
    process.send(result); // Отправляем результат
});

Во-вторых, я оборачиваю вызов eval в try..catch, хотя да, это очевидно.

В-третьих, eval вызывается в специальной функции с кучей неиспользуемых параметров. Имена этих параметров соответствуют всем доступным глобальным именам и они будут равны undefined в момент работы eval.

var libs = { // Разрешенные библиотеки
    "libxmljs": require("libxmljs") 
};
var l = "";
for (var i in libs) {
    if (libs.hasOwnProperty(i)) {
        if (l) {
            l += ',';
        }
        l += i + '=this.libs.' + i;
    }
}
l = "var " + l + ';';

// Класс ответственный за выполнение кода
var RunnerClass = function (code) {
    this.code = code;
};
RunnerClass.prototype.__proto__ = EventEmitter.prototype;

RunnerClass.prototype.run = function (data) {
    var code = this.code;

    // Объект, который представляет собой контекст this для выполняемого кода. всё, что описано тут, доступно во вложенном коде.
    var context = new EventEmitter();
    context["data"] = data;
    context["libs"] = libs;
    context["finish"] = function () {
        this.emit("finish");
    };
    context.on("finish", function () {
        this.emit("finish", context.result);
        console.log('Result', context.result);        
    }.bind(this));

    // Вот эти параметры и прячут глобальные переменные
    var sandbox = function(Buffer, CodeRunner, EventsEmitter, context, global, exports, i, libs, process, module, require,  __dirname, __filename) {
        try {
            eval(l + '!function(code,l){' + code + '}.call(this)'); // Дополнительно прячем в именах параметров код и подключение библиотек.
        } catch (e) {
            console.log("Code execution error: " + e.message);
        }
    };
    // Вызываем sandbox без параметров, так что все имена становятся равными undefined
    sandbox.call(context); 
};

Понятно или еще комментариев дописать?

Нужно только осторожно следить за тем, какие функции и классы открывать для пользователей. Возможно, я что-то не предусмотрел, поправьте меня в таком случае.

AngularJS

Вы, наверное, слышали об этом JavaScript-фреймворке. Модный тренд, если позволите так выразиться. Superheroic JavaScript MVW1 Framework, как утвержают сами разработчики. Не удержался и посмотрел на него, тем более что выдалась возможность сделать это в рабочее время и за счет заказчика. Мы выбираем основу для будущего проекта, вот и решили глянуть современные js фреймворки, и мне достался AngularJS. Поделюсь своими впечатлениями.

Не знаю как с производительностью, но с функциональностью у этого фреймворка полный порядок. Покажу на небольшом примере. Итак, у нас есть кусочек разметки:

<table class="table table-striped table-hover" ng-controller="TestController">
    <thead>
        <tr>
            <th>Action</th>
        </tr>
    </thead>
    <tbody>
        <tr ng-repeat="action in actions">
            <td>{{action.action}}</td>
        </tr>
    </tbody>
</table>

Дальше мы подключаем JavaScript:

function TestController($scope) {
    $scope.actions = [
        {action: 'Action1'},
        {action: 'Action2'}
    ];
}

Ну и всё. У нас готова страница с таблицей, в которой 2 строки: «Action1» и «Action2». Но это еще не все. Если мы потом добавим в поле actions еще чего-нибудь, то оно тоже автоматически отобразится на странице.

Единственное, мне потребовалось некоторое время, чтобы разобраться с некоторыми более сложными вещами. Например, создание независимых компонентов. В моей задаче требовалось, чтобы родительский компонент передавал данные для дочернего. В результате оказалось, что на момент вызова родительского контроллера дочерний еще не инициализирован и нужно немного подождать. И лучше всего использовать для этого событие $viewContentLoaded. Чтобы это понять, официального сайта было недостаточно, и пришлось провести пару-тройку часов в обнимку с дебаггером и гуглом.

В общем, впечатление от фреймворка очень положительное, надо будет попробовать написать что-нибудь более-менее серьезное.

Еще примеры можете оценить на главной странице фреймворка, они там гораздо лучше, чем я вам привел.


  1. Model-View-Whatever

Получение бинарных данных в Google Closure Library

Делюсь с вами кусочком из своей практики, который может кому-нибудь пригодиться. Лично я был бы рад найти подобный пост на просторах интернета, когда столкнулся с этой проблемой.

Итак, задача: с помощью XmlHttpRequest получать бинарные данные с сервера. Ну как-то так получилось, что нужно скачивать сжатый с помощью gzip xml-файл с сервера и выбирать из него некоторый набор данных.

Первая важная вещь, которая понадобится, — это метод overrideMimeType у объекта XMLHttpRequest. Этот метод позволяет переопределить тип получаемых с сервера данных. И даже существует специальная кодировка, которая специально была задумана для приема бинарных данных — x-user-defined (мы помним, что XMLHttpRequest автоматически преобразует все принятые данные в юникод). Поэтому простое добавление следующей строчки должно сделать большую часть работы.

xhr.overrideMimeType('text/plain; charset=x-user-defined');

Мозг, натренированный jQuery, выдает кусок кода практически сразу:

$.ajax({
    url: url,
    beforeSend: function(xhr) {
        xhr.overrideMimeType(
            'text/plain; charset=x-user-defined'
        );
    },
    success: successHandler,
    error: errorHandler
});

А вот с Google Closure Library не всё так просто. Там нет никаких beforeSend, и если посмотреть в документацию, то нужная функция выглядит следующим образом:

goog.net.XhrIo.send(url, opt_callback, opt_method, opt_content, opt_headers, opt_timeoutInterval)

Только один callback, который срабатывает на complete, данные, заголовки, таймаут. Не густо. Но если посмотреть в код этой функции, то можно обнаружить, что на самом деле это прокси для объекта goog.net.XhrIo. Но goog.net.XhrIo тоже не конструирует XMLHttpRequest. Вместо этого использует фабрику для создания запросов, что, кстати, логично, но больше Java-way чем JS-way. Ну да ладно. Получается, чтобы вклинить вызов overrideMimeType, нужно сделать свою фабрику с блекджеком, например, вот так:

my.net.BinaryXmlHttpFactory = function () {
    goog.base(this);
};
goog.inherits(my.net.BinaryXmlHttpFactory, goog.net.DefaultXmlHttpFactory);

my.net.BinaryXmlHttpFactory.prototype.createInstance = function () {
    var xhr = goog.base(this, "createInstance");
    xhr.overrideMimeType('text/plain; charset=x-user-defined');
    return xhr;
};

Теперь дело осталось за малым: инициализировать goog.net.XhrIo c новой фабрикой и выполнить запрос:

var xhrIo = new goog.net.XhrIo(new my.net.BinaryXmlHttpFactory());
goog.events.listen(xhrIo, goog.net.EventType.SUCCESS, successHandler);
goog.events.listen(xhrIo, [goog.net.EventType.ERROR, goog.net.EventType.ABORT], errorHandler);
xhrIo.send(url);

Вот и всё. Сразу оговорюсь, что всё это не будет работать в Internet Explorer, т.к. там нужна своя особая магия для получения бинарных данных, в отличие от остальных, где остаток действий выглядит довольно просто:

var res = '';
for (var i = 0; i < data.length; ++i) {
    var code = data.charCodeAt(i);
    res += String.fromCharCode(code & 0xff);
}

Надеюсь, что кому-то этот пост поможет и направит мысли в правильную сторону.

Август — время собеседований

Раньше мне как-то не приходилось с этим сталкиваться, но, оказывается, август/начало сентября — это время, когда программисты ходят по собеседованиям и всем кагалом меняют работу. За весь предыдущий год к нам заглянуло от силы 5-10 человек, а сейчас идут толпами, практически каждый день приходится кого-нибудь собеседовать, иногда даже несколько человек. И ведь хорошие кадры приходят! Складывается впечатление, что HR’ы весь год могут курить бамбук и писать большие рекламные посты в моихкругах и линкединах, и только в этом месяце для них появляется работа.

Google Closure Compiler JsDoc в продуктах JetBrains

Свершилось чудо! JetBrains услышали мои молитвы и добавили поддержку аннотаций Google Closure Compiler в свои новые продукты. Теперь редактор знает, чем отличается тип параметра {string} от {!string} и от {?string}. Он больше не ругается за то, что я пытаюсь сравнивать {String}{string}, ну не красота ли? Ну и куча всяких других полезных плюшек, вроде полного понимания структуры наследования объектов. Ради всего этого можно поставить EAP-версию и потерпеть другие мелкие баги.

← СтаршеМоложе →