Так уж вышло, что по работе, мне приходится редактировать файлы, к которым я имею доступ только через файловый менеджер 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