Двухколёсная автоматизация загрузки файлов на сервер из Notepad++

в 7:03, , рубрики: javascript, node.js, nodejs, велосипед, метки: , ,

Так уж вышло, что по работе, мне приходится редактировать файлы, к которым я имею доступ только через файловый менеджер CMS Bitrix, что влечёт за собой открытие множества вкладок в браузере и огромное количество ненужных телодвижений необходимых лишь для того, чтобы отредактировать несколько файлов.
Ниже я расскажу как решил эту проблему с помощью Node.js и свободного времени.

Первое что я сделал — это узнал, как вообще битрикс подходит к редактированию файлов. Всё оказалось довольно просто и ничуть не превзошло мои ожидания — текст файла отправляется формой на сервер вместе с именем файла, где php скрипт открывает указанный файл и записывает в него содержимое.
Самое логичное, что пришло мне в голову — имитировать отправку формы на тот же обработчик.
Для этого нужно сделать 3 вещи:

  • Притвориться браузером
  • Притвориться пользователем у которого есть права на редактирование файлов
  • Отправить форму

Каждый, кто хоть немного знает о http протоколе знает что за определение клиентской программы отвечает строка User-Agent в заголовке запроса.
Вот именно здесь и следует указать сигнатуру программы, под которую мы маскируемся. У меня это:

Mozilla/5.0 (Windows NT 6.1; WOW64; rv:22.0) Gecko/20100101 Firefox/22.0

Чуть сложнее дело обстоит с выдачей себя за пользователя. Методом тыка, а точнее поочерёдным удалением кук, было установлено, что для того чтобы сайт считал пользователя вошедшим — ему необходимы лишь его логин и актуальный phpsessid. Логин я и так знаю, а вот номер сессии узнать можно несколькими способами.
Во-первых, можно просто посмотреть его с помощью «Инструментов разработчика» доступных в любом браузере.
Во-вторых, раз уж мы отправляем форму — можно перед этим отправлять форму входа на сайт. чтобы он номер сессии пришёл нам в виде куки.
Учитывая, что в любом случае загрузка файлов связана со входом на сайт, я решил воспользоваться первым способом, однако вместо того чтобы каждый раз открывать FireBug, просто добавил вывод phpsessid на одну из страниц сайта.
Последнее, и главное, что нам необходимо сделать — это отправить данные на сервер, как-будто бы из формы.
Здесь тоже нет ничего сложного. Во всё том же заголовке запроса указываем Content-Type, который определяет тип прилагаемого к заголовку контента, и добавляем Content-Length, что необходимо для того чтобы сервер знал сколько контента надо принять.
После этого в теле запроса посылаем сериализованные данные, которыми являются текст файла, его имя на сервере и некоторые другие переменные необходимые обработчику формы.
Теперь, когда «расчёты» окончены, можно переходить к практике.
Для начала определим — какая вообще информация необходима для успешной загрузки файлов на сервер. Кроме указанных логина и номера сессии необходим путь к файлам на локальной машине и путь на сервере, куда эти файлы будут сохраняться.
Первый было решено передавать скрипту как аргумент, а всю остальную информацию хранить в файле в той папке, которая передаётся скрипту.
В nodejs аргументы командной строки хранятся в поле argv глобальной переменной process, причем, даже если аргументы в программу не передаются — в массиве process.argv всёравно лежат 2 строки — «node» и имя выполняемой программы, поэтому первый аргумент лежит по смещению — 2. Сначала проверяем, передан ли хоть какой-то аргумент нашему скрипту, и если его вдруг не оказалось — выходим.

if(process.argv.length<3){
    console.log("Необходим один аргумент!");
    process.exit(1);
}

Получив путь до локальной папки, открываем файл config.json который должен там лежать.
За работу с файлми в nodejs отвечает модуль fs (FileSystem). Модули в nodejs это по сути просто наборы функций, однако все мы знаем что функции в javascript не совсем такие как в других языках.
Нам же понадобится вполне заурядная функция fs.readFile(path, callback) которая, как вы и сами можете догадаться — считывает содержимое файла. Если с первым её аргументм всё понятно, то второй — это функция, которая будет выполнена, когда файл будет полностью прочтён. Причём прогамма не замрёт в ожидании этого момента, а продолжит своё выполнение. Такой подход к организации программы называется «Событийно-ориентированное программирование». Итак — читаем файл настроек в указанной папке. Его содержимое будет передано в callback функцию вторым аргументом. Первым же, будет ошибка или false если всё нормально.

var fs = require('fs'); // говорим, что будем использовать модуль fs
fs.readFile(process.argv[2]+'config.json', function (err, data) {
    if (err) throw err;
    c = JSON.parse(data);// получаем информацию из json файла и записываем в глобальную переменную
    //...что-то с этими данными делаем
});

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

//...внутри предыдущей функции
fs.readdir(process.argv[2],function(err,files){ // берём список файлов в папке
    if (err) throw err;
    files.forEach(function(el,index,array){ 
        if(el.split('.').length>1)
        if(el!='config.json' && el!='.git' && el!='.gitattributes' && el!='.gitignore') // если файл подходит
            upload(el); // загружаем его на сервер
    });
});

Получив имя файла, мы можем прочитать его содержимое и послать его на сервер. Именно это мы и сделаем. Для того чтобы послать http запрос в nodejs есть модуль http. Подключив его мы можем вызвать функцию http.request(method, path, port, hostname, headers, callback), где предпоследний аргумент буквально делает нас всемогущими, ведь он позволяет отправить запрос с каким угодно заголовком, именно здесь мы укажем User-Agent, куки и тип контента запроса. Делая запрос мы получаем его в переменную, через которую можем влиять на его поведение. Например методы write() и end(). Отправляют данные в теле запроса с той лишь разницей что write можно вызвать несколько раз подряд, а end — только в один раз т.к. сразу после его вызова последует завершение запроса.
Как было сказано ранее — данные необходимо отправлять в сериализованном виде, а именно — так как мы бы из видели в адресной строке. В nodejs есть модуль для работы с такими данными, и называется он — querystring. Он позволяет сохранять объект в строку и наоборот. Все данные формы у нас хранятся в глобальной переменной query, которую мы и сериализуем для последующей отправки.

var http = require('http');
var querystring = require('querystring');
function upload(file){
    console.log("Загрузка файла %s:",file);
    var fileData=fs.readFileSync(process.argv[2]+'\'+file); // получаем содержимое файла
    query.path=c.path+file;// указываем в форме имя отправляемого файла 
    query.filesrc=fileData.toString();
    var str=querystring.stringify(query);// сериализуем данные формы
    var len=str.length;
    var request = http.request( // выполняем запрос
    {
        method:'post',
        path:'/bitrix/admin/fileman_file_edit.php',
        port:80,
        hostname:'***.****.ru',
        headers:{
            'host':'***.****.ru',
            'User-Agent':'Mozilla/5.0 (Windows NT 6.1; WOW64; rv:22.0) Gecko/20100101 Firefox/22.0', 
            'Cookie':'PHPSESSID='+c.phpsessid+';BITRIX_SM_LOGIN='+c.login,
            'Content-Type':'application/x-www-form-urlencoded',
            'Content-Length':len
        }
    },rListener); // указываем функцию которая будет вызвана когда придёт ответ
    request.end(str); // посылаем тело запроса (сериализованную форму) и завершаем его
}

Ну и для того чтобы знать — загружен ли наш файл или труды наши были напрасны — принимаем ответ на запрос. Callback вызываемый при получении ответа, первым аргументом принимает всю информацию ответа, в том числе и статус. Всё тем же методом тыка, было обнаружено, что при успешной загрузке файла в ответ приходит редирект, в остальных же случаях в ответ приходит страница.

function rListener(response){
    var status='';
    switch(response.statusCode){
        case 302: status='Файл загружен';break; // если пришёл редирект - всё ОК
        case 200: status='Ваша сессия просрочена';break;
        default : status='Ошибка';
    }
    console.log('%s (%d).',status,response.statusCode);
}

Полный код скрипта лежит здесь.
Имея такой вот велосипед, довольно просто настроить вызов скрипта по нажатию клавиши в вашем любимом редакторе. У меня это Notepad++ с плагином NppExec.

Автор: MadridianFox

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js