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

В первую очередь я работал над областью визуализации данных продукта, и вскоре мы поняли, что не можем удовлетворить все виды требований клиентов (все возможные типы диаграмм). Некоторые из этих требований не были предусмотрены, а некоторые из них не заслуживали включения в продукт. Чтобы решить эту проблему, мне пришлось разработать функцию, в которой будет редактор кода, в котором люди смогут писать JS-код для разработки любого вида визуализации. Код, который они пишут, будет запускаться в iframe с необходимыми данными вместе с jQuery и D3.

Точно так же в случае с чат-ботами нам пришлось предоставить редактор JS, в котором люди выполняют множество операций по управлению состоянием, вызовам внешних API и преобразованию данных. Этот ненадежный код будет запускаться в NodeJS VM, которая представляет собой управляемую песочницу, где только несколько модулей (синтаксический анализатор xml, синтаксический анализатор JSON, выборка узлов, создание токена JWT, cryptojs) доступны через контекст, даже не require , fs, http и т. д.. У виртуальной машины есть опция тайм-аута, с помощью которой мы можем указать, когда виртуальная машина должна завершить операцию и выйти.

Самая большая проблема, позволяющая людям писать код, заключается в том, что если какой-нибудь разработчик-любитель напишет условие, которое создаст бесконечные циклы внутри блока асинхронного кода (например, обратный вызов обещания)! Если код будет выполняться в браузерах, это повлияет только на пользователей, просматривающих результат. Но в случае с Node.js это будет иметь возможность полностью загружать цикл обработки событий, так что все приложение перестанет отвечать на любые новые запросы. Это так хорошо, как если бы система вышла из строя. Проверьте эту ветку, чтобы узнать больше. К сожалению, у него нет решения от реализации Node.js.

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

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

пока цикл

для цикла

цикл до .. пока

рекурсия

Рекурсия не может постоянно держать систему занятой, так как она вызовет исключение «Достигнут максимальный размер стека вызовов», которое может быть перехвачено и освободит цикл событий для обслуживания следующего запроса.

Итак, потенциально, while, for и do while - это три конструкции, которые могут создавать бесконечные циклы (поправьте меня, если я ошибаюсь). Возвращаясь к внедрению кода, мы не можем просто внедрить код в программу, используя сопоставление строк. Это очень опасно и легко может пойти не так. Лучшее решение для внедрения кода в нужное место - использование AST (абстрактное синтаксическое дерево). Посмотрите, как будет выглядеть AST в ASTExplorer. С AST нам не нужно делать никаких предположений, но, пройдя по узлам, мы сможем найти все циклы while, for или do while и точно ввести код.

Чувствуете ностальгию ?? Когда мы говорим об AST и внедрении кода, если вы используете ES6, первое, что вам приходит в голову, это Babel. Используя babel-core, мы можем создать AST из нашего JS-кода. Babel также предоставляет способ фильтрации определенного типа оператора (оператора while или for) с использованием шаблона посетителя. Посмотрите это руководство по разработке плагинов от Babel. С помощью этого шаблона посетителя мы можем найти все циклы while, for, do.. while во всем нашем коде и преобразовать их во все, что захотим. В этом случае мы введем простое условие для генерации исключения, если оно превышает лимит времени.

var fs = require("fs");
var babel = require("babel-core");
var loopcontrol = require("./loopcontrol");

// read the filename from the command line arguments
var fileName = "test.js";

// read the code from this file
fs.readFile(fileName, function(err, data) {
   if (err) throw err;

   // convert from a buffer to a string
   var src = data.toString();

   // use our plugin to transform the source
   var out = babel.transform(src, {
      plugins: [loopcontrol]
   });

   // print the generated code to screen
   console.log(out.code);
});

Приведенный выше код считывает код JS из test.js и преобразует его в AST с помощью метода babel.transform. Внедрение кода происходит через созданный мной плагин (loopcontrol). Давайте посмотрим на содержимое loopcontrol.js.

module.exports = function(babel) {
   var t = babel.types;
   return {
      visitor: {
         WhileStatement: function transformWhile(path) {
            let variableName = path.scope
.generateUidIdentifier("timer");
            let declaration = t.declareVariable(variableName);
            path.scope.parent.push(declaration);
            let definition = t.assignmentExpression(
               "=",
               variableName,
               t.callExpression(t.memberExpression(t.identifier("Date"), t.identifier("now")), [])
            );
            path.insertBefore(t.expressionStatement(definition));
            const lhs = t.parenthesizedExpression(t.binaryExpression("+", variableName, t.NumericLiteral(30000)));
            path
               .get("body")
               .pushContainer(
                  "body",
                  t.ifStatement(
                     t.binaryExpression(">", t.callExpression(t.memberExpression(t.identifier("Date"), t.identifier("now")), []), lhs),
                     t.throwStatement(t.stringLiteral("Execution Timedout")),
                     null
                  )
               );
         }
      }
   };
};

В этом фрагменте babel вызывает метод transformWhile каждый раз, когда встречает инструкцию while. Внутри метода transformWhile babel-types используется для создания объявления переменной (_timer ), чтобы сохранить текущую метку времени, а затем определите переменную с помощью Date.now () над циклом while. Затем в тело оператора while вводится оператор if, который проверяет, превышает ли текущая отметка времени более 30 секунд с момента инициализации, и выдает ошибку, если это правда.

Здесь вы можете увидеть до и после внедрения кода.

//Before code injection
Promise.resolve().then(() => {
    while(true) {
        let x = 1;
    }
});
//After code injection
Promise.resolve().then(() => {
    var _timer;
    _timer = Date.now();

    while (true) {
        let x = 1;
        if (Date.now() > (_timer + 30000)) throw "Execution TIMEOUT";
    }
});

Это дает возможность точно преобразовывать код в любой степени благодаря Babel. Теперь мы можем применить то же решение к любому циклу или любому потенциально вредоносному шаблону кода, с которым мы сталкиваемся. Существует отдельная версия Babel, которая позволяет нам делать это напрямую в браузерах.

Перед выполнением кода мы можем выполнить инъекцию кода, которая решит проблему бесконечных циклов из ненадежного кода в средах выполнения JS, таких как виртуальная машина.

AST потрясающие.

Надеюсь, это сэкономит чье-то время.