Мне тут по долгу службы пришлось столкнуться с 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);
};
Понятно или еще комментариев дописать?
Нужно только осторожно следить за тем, какие функции и классы открывать для пользователей. Возможно, я что-то не предусмотрел, поправьте меня в таком случае.
Вы, наверное, слышали об этом 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
. Чтобы это понять, официального сайта было недостаточно, и пришлось провести пару-тройку часов в обнимку с дебаггером и гуглом.
В общем, впечатление от фреймворка очень положительное, надо будет попробовать написать что-нибудь более-менее серьезное.
Еще примеры можете оценить на главной странице фреймворка, они там гораздо лучше, чем я вам привел.
Model-View-Whatever
Делюсь с вами кусочком из своей практики, который может кому-нибудь пригодиться. Лично я был бы рад найти подобный пост на просторах интернета, когда столкнулся с этой проблемой.
Итак, задача: с помощью 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’ы весь год могут курить бамбук и писать большие рекламные посты в моихкругах и линкединах, и только в этом месяце для них появляется работа.
Свершилось чудо! JetBrains услышали мои молитвы и добавили поддержку аннотаций Google Closure Compiler в свои новые продукты. Теперь редактор знает, чем отличается тип параметра {string}
от {!string}
и от {?string}
. Он больше не ругается за то, что я пытаюсь сравнивать {String}
cо {string}
, ну не красота ли? Ну и куча всяких других полезных плюшек, вроде полного понимания структуры наследования объектов. Ради всего этого можно поставить EAP-версию и потерпеть другие мелкие баги.