Выполнение пользовательского кода в 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); 
};

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

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

Good day
Good day!

Good day!

getElementsByClassName в IE

А вы знаете, что в Internet Explorer вплоть до 8-го у document нет метода getElementsByClassName? А следовательно, все ваши верстки, основанные только на классах, будут сильно проигрывать в производительности. Все фреймворки эмулируют метод вызовом getElementsByTagName('*') с дальнейшей ручной фильтрацией.

var els = document.getElementsByTagName("*"), filtered = [], len = els.length, i;
for (i = 0; i < len; ++i) {
    el = els[i];
    el.className.search(/^(.* |)some-class( .*|)$/) !== -1 && filtered.push(el);
}

Ну это код на случай, если вам придется использовать vanilla.js. Обратите внимание, что в цикле нельзя изменять DOM-дерево. Нужно сначала скопировать все подходящие элементы в отдельный массив и только потом их обрабатывать. Все потому, что getElementsByTagName (как и все остальные похожие функции) возвращает NodeList, который динамически изменяется вместе с деревом.

May the Force be with you.

jQuery.Deferred

А помните все те велосипеды, которые приходилось писать для обработки множества асинхронных вызовов? Скажем, у нас есть 10 функций или ajax-запросов. Нужно вызвать их все, подождать пока они отработают, а потом выполнить какой-то общий код. Знакомо, не правда ли? И вот, я случайно обнаружил, что в jQuery подобные вещи уже давно реализованы и их можно использовать. Называется всё это jQuery.Deferred.

Итак, попробуем написать тестовое приложение. Оно запускает 10 таймеров с различным временем. И когда все таймеры сработают, приложение должно вывести какое-нибудь сообщение.

var doAction = function (i) {
    var d = new $.Deferred();
    setTimeout(function () {
        console.log(i + ' done.');
        d.resolve();
    }, Math.random() * 9000 + 1000);
    return d.promise();
};
$(function () {
    var actions = [];
    for (var i = 0; i < 10; ++i) {
        actions.push(doAction(i));
    }
    $.when.apply(this, actions).then(function () {
        console.log('Everything is ok.');
    });
});

У объекта Deferred довольно много всяческих методов, но нам интересно несколько. Метод promise возвращает специальный объект, с помощью которого можно отслеживать состояние выполняемой задачи, методы resolve и reject завершают задачу успешно и не успешно соответственно.

Получив набор объектов promise, мы можем их комбинировать. В нашем случае с помощью $.when создаем новый deferred-объект, который является объединением всех остальных. Ну и с помощью then вешаем общую функцию, которая сработает самой последней.

Ну и напоследок, $.ajax тоже является deferred объектом, поэтому можно написать:

$.when($.ajax(a), $.ajax(b)).then(function () {});

И получить обработчик, который выполнится после завершения всех запросов. Хотя что я вам тут рассказываю, вы ведь всё это знаете и без меня.

Ошибки bingbot/msnbot

Как же я люблю Microsoft. Вот, например, есть у них поисковик Bing (им, кстати, кто-нибудь пользуется?). Авторы crawler’ов для этого поисковика совершенно не умеют работать с UTF-8. Почему-то только они присылают запросы с неверно закодированными кириллическими буквами.

Пример. Есть страница «работа» :: [dikmax’s blog]. Везде в ссылках на эту страницу у меня написано <a href=”/tag/работа”>.

Какую страницу запрашивают все браузеры и поисковики: /tag/%D1%80%D0%B0%D0%B1%D0%BE%D1%82%D0%B0. А что же пытается получить msnbot: /tag/%D1%E2%82%AC%D0%B0%D0%B1%D0%BE%D1%E2%80%9A%D0%B0.

            D1                (incomplete sequence)
U-000020AC  E2 82 AC         
U-00000430  D0 B0            
U-00000431  D0 B1            
U-0000043E  D0 BE            
            D1                (incomplete sequence)
U-0000201A  E2 80 9A         
U-00000430  D0 B0            

Я не представляю, какое преобразование нужно было сделать, чтобы получить такую последовательность байт.

Решение понятно, нужно кодировать ссылки на стороне сервера. Но зачем, если и так всё работает? А Bing — не тот поисковик, под который стоит подстраиваться. Хотя когда-нибудь я поправлю и это.

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