Генерация конфигов для nginx, история одного pull request

Приветствую, товарищи. На моих боевых серверах прекрасный nginx крутится с 2006 года и за годы его администрирования я накопил много конфигов и шаблонов. Я много нахваливал nginx и как-то так вышло, что даже хаб nginx на Хабре тоже завёл я, понты \m/

Друзья попросили поднять им девелоперскую ферму и вместо того, чтобы тащить им свои специфические шаблоны, я вспомнил про интересный проект nginxconfig.io, который и конфиги раскидывает по полкам и для lets encrypt всё готовит итп. Я и подумал, почему бы и нет? Однако, мне бесил тот факт, что nginxconfig предлагает мне скачать zip архив в браузер, не позволяя слить его сразу на сервак по средствам wget/fetch/curl. Что за бред, зачем он мне в браузере, мне он нужен на сервере из консоли. Разозлившись, я залез на github посмотреть кишки проекта, что привело к его форку и, как следствие, pull request`у. О котором я бы не стал писать, если бы он не был интересным ;)

image

Конечно, перед тем, как ковырять исходники, я посмотрел откуда хром тянет сгенеренный zip архив с конфигами, а там меня ждал адрес, начинающийся с «blob:», оппа. Уже стало понятно, что по ходу сервис ничего не генерирует, по факту, что это всё делает js. Действительно, zip архив генерирует сам клиент, браузер, javascript. Т.е. прелесть в том, что проект nginxconfig.io может быть просто сохранён как html страница, залит на какой-нибудь narod.ru и он будет работат) Это очень забавное и интересное решение, однако, оно жутко неудобное для настройки серверов, собственно, именно для того, для чего этот проект и создавался. Качать сгенеренный архив браузером, а потом передавать его на сервер с помощью nc… в 2019 году? Я поставил перед собой задачу найти способ качать полученный конфиг сразу на сервер.

Сделав форк проекта, я начал думать, какие у меня есть варианты. Задача усложнялось тем, что я не хотел отходить от условия, что проект должен оставаться чистым front-end, без какого-либо back-end. Конечно, самое простое решение было бы подтянуть nodejs и заставить его генерировать архив с конфигами по прямым ссылкам.

Вариантов, собственно, было не много. Точнее, в голову пришёл только один. Нам надо настроить конфиги и получить ссылку, которую сможем скопировать в консоль сервера, чтобы получить zip архив.

Несколько текстовых файлов в получаемом zip архиве весили совсем немного, буквально несколько килобайт. Очевидным решением было получить base64 строку из сгенерированного zip архива и кинуть её в буфер, тогда как на сервере командой в консоли

echo 'base64string' | base64 --decode > config.zip

мы могли бы создать этот самый zip файл.

nginxconfig.io был написан на AngularJS, даже не представляю, какие километры кода потребовались бы, если бы автор не выбрал реактивный js фреймворк. Зато прекрасно представляю, насколько проще и красивее можно было бы всё это реализовать на VueJS, хотя это уже совсем другая тем.

В сурсах проекта мы видим метод генерации zip архива:

$scope.downloadZip = function() {
        var zip = new JSZip();

        var sourceCodes = $window.document.querySelectorAll('main .file .code.source');

        for (var i = 0; i < sourceCodes.length; i++) {
                var sourceCode = sourceCodes[i];

                var name        = sourceCode.dataset.filename;
                var content     = sourceCode.children[0].children[0].innerText;

                if (!$scope.isSymlink() && name.match(/^sites-available\//)) {
                        name = name.replace(/^sites-available\//, 'sites-enabled/');
                }

                zip.file(name, content);

                if (name.match(/^sites-available\//)) {
                        zip.file(name.replace(/^sites-available\//, 'sites-enabled/'), '../' + name, {
                                unixPermissions: parseInt('120755', 8),
                        });
                }
        }

        zip.generateAsync({
                type: 'blob',
                platform: 'UNIX',
        }).then(function(content) {
                saveAs(content, 'nginxconfig.io-' + $scope.getDomains().join(',') + '.zip');
        });

        gtag('event', $scope.getDomains().join(','), {
                event_category: 'download_zip',
        });
};


всё достаточно просто, с помощью библиотеки jszip создаётся zip, куда кладутся файлы конфигураций. После создания zip архива, js скармливает его браузеру с помощью библиотеки FileSaver.js:

saveAs(content, 'nginxconfig.io-' + $scope.getDomains().join(',') + '.zip');


где content, это полученный blob объект zip архива.

Ок, всё что мне надо было сделать, это добавить ещё одну кнопку рядом и при нажатии на неё не сохранять полученный zip архив в браузер, а получать из него base64 код. Немного пошаманив, я получил 2 метода, вместо одного downloadZip:

$scope.downloadZip = function() {
        generateZip(function (content) {
                saveAs(content, 'nginxconfig.io-' + $scope.getDomains().join(',') + '.zip');
        });

        gtag('event', $scope.getDomains().join(','), {
                event_category: 'download_zip',
        });
};
$scope.downloadBase64 = function() {
        generateZip(function (content) {
                var reader = new FileReader();
                reader.readAsDataURL(content);
                reader.onloadend = function() {
                        var base64 = reader.result.replace(/^data:.+;base64,/, '');
                        // в переменной base64 как раз нужный мне zip архив в виде base64 строки
                }
        });

        gtag('event', $scope.getDomains().join(','), {
                event_category: 'download_base64',
        });
};


Как вы могли заметить, генерацию самого zip архива я вынес в приватный метод generateZip, ну и т.к. это AngularJS, да и сам автор придерживается колбэков, не стал реализовывать его через промисы. downloadZip по прежнему на выходе делал saveAs, тогда как downloadBase64 делал немного другое. Мы создаём FileReader объект, пришедший к нам в html5 и вполне уже доступный для использования. Который, в своё время, умеет из blob делать base64 строку, точнее он делает DataURL строку, но нам это не так важно, т.к. DataURL содержит именно то, что нам надо. Бинго, небольшая заковырка ждала меня при попытке положить всё это в буфер. Автор использовал в проекте библиотеку clipboardjs, которая позволяет работать с буфером обмена без flash объектов, на основе выделенного текста. Изначально я решил класть мой base64 в элемент с display: none;, но в таком случае у меня не получалось положить его в буфер обмена, т.к. выделения не происходит. Поэтому, вместо display: none; я сделал

position: absolute;
z-index: -1;
opacity: 0;


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

echo 'base64string' | base64 --decode > config.zip


которую я просто вставлял в консоль на сервере и тут же получал zip архив со всеми конфигами.

Ну и, конечно, я закинул pull request автору, т.к. проект активный и живой, мне хочется и обновления от автора видеть и свою кнопопчку иметь) Кому интересно, вот мой форк проекта и сам pull request, где можно посмотреть что я исправил/дополнил.

Всем бодрой разработки :-)

etopsv7os4ew9wv8kkccgvvbw78.gif

© Habrahabr.ru