Skip to main content

Время от времени к нам обращаются за помощью по интеграции Битрикс24 с внешними системами. Чаще всего разработка таких интеграций не вызывает особых трудностей, если система известная, имеет известный API и, на всякий случай, техподдержку.

Становится интереснее, когда внешняя система — какой-нибудь самописный ERP-софт, разработчиков которого в последний раз видели 10 лет назад. Как правило, в таких случаях про API и документацию речи не идёт. Отказаться от интеграции тоже нельзя, потому что часть критически важных для бизнеса процессов оказываются завязаны на этой внешней системе.

Ситуация

Например так было у одного из наших клиентов, про которого мы рассказывали в статье про разработку чат-ботов. Нужно было передавать информацию о внесении платежа из Битрикс24 во внешнюю учетную систему, где они хранились, но при этом было несколько проблем:

  1. Единственный способ работать с системой — через интерфейс в браузере. Никакого API в ней не предусмотрено.
  2. Разработчики системы в компании клиента уже давно не работают и обратиться с вопросом по доработке API не к кому.
  3. Мы могли бы доработать систему самостоятельно, но она написана на ASP.NET, с которым мы не работаем.
  4. При заполнении формы создания платежа система, по какой-то неведомой логике, дозаполняет часть полей перед отправкой. Чтобы разобраться, как это происходит, нужно реверсить минифицированный JS, потому что где лежат исходники тоже никто не знает.
  5. С учетом предыдущего пункта подделать все нужные http-запросы curl’ом тоже не представляется возможным: мы не знаем, как рассчитать некоторые требуемые поля.

Интеграция

В таком случае одним из вариантов решения проблемы будет полная имитация действий пользователя в браузере на сервере с Битрикс24. В качестве инструмента будем использовать Puppeteer, который является обёрткой над всем известным Google Chrome.

Чтобы поставить библиотеку пишем так:

$ npm install –save puppeteer

Для работы браузера нужен X-сервер. На линуксовых серверах без графического интерфейса X-сервера чаще всего нет. Нужно его поставить, список пакетов берём отсюда. Для веб-окружения Битрикс24 зависимости выглядят так:

$ yum -y install pango.x86_64 libXcomposite.x86_64 libXcursor.x86_64 libXdamage.x86_64 libXext.x86_64 libXi.x86_64 libXtst.x86_64 cups-libs.x86_64 libXScrnSaver.x86_64 libXrandr.x86_64 GConf2.x86_64 alsa-lib.x86_64 atk.x86_64 gtk3.x86_64 ipa-gothic-fonts xorg-x11-fonts-100dpi xorg-x11-fonts-75dpi xorg-x11-utils xorg-x11-fonts-cyrillic xorg-x11-fonts-Type1 xorg-x11-fonts-misc

Сразу после установки можно попробовать запустить тестовый скрипт из документации библиотеки:

const puppeteer = require(‘puppeteer’);
(async () => {
const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.goto(‘https://example.com’);
await page.screenshot({path: ‘example.png’});
await browser.close();
})();

Сохраняем скрипт в директории на сервере и запускаем на выполнение:

$ node example.js

Если всё получилось, в директории со скриптом должен появится файл example.png со скрином открытой в браузере страницы. Да, дебажить в дальнейшем придётся скриншотами 🙂

paybot.php

Нам нужно будет вызывать эту команду из Битрикс24, который написан на PHP. Сделаем это так:

protected static function doCreatePayment($clientId, $amount, $comment = ”, $recipientId = ”)
{

// Передавать данные будем через переменные окружения
$env = [

// Поля платежа
‘CLIENT_ID’ => $clientId,
‘AMOUNT’ => $amount,
‘COMMENT’ => Loc::getMessage(‘MAGNIFICO_PAYBOT_DEFAULT_COMMENT’).($comment ? ‘ (‘.$comment.’)’ : ”),
‘RECIPIENT_ID’ => $recipientId,
// Пути к бинарникам хрома и NodeJS берём из настроек модуля
‘CHROME_BIN’ => \Bitrix\Main\Config\Option::get(‘magnifico.paybot’, ‘chrome_bin’),
‘NODEJS_BIN’ => \Bitrix\Main\Config\Option::get(‘magnifico.paybot’, ‘nodejs_bin’),
// Логин и пароль для авторизации в внешней системе
‘AUTH_USERNAME’ => \Bitrix\Main\Config\Option::get(‘magnifico.paybot’, ‘auth_username’),
‘AUTH_PASSWORD’ => \Bitrix\Main\Config\Option::get(‘magnifico.paybot’, ‘auth_password’),

];
// Проверяем существование хрома
if (!is_executable($env[‘CHROME_BIN’])) {

throw new \Exception(Loc::getMessage(‘MAGNIFICO_PAYBOT_CHROME_ERROR’));

}
// То же самое для NodeJS
if (!is_executable($env[‘NODEJS_BIN’])) {

throw new \Exception(Loc::getMessage(‘MAGNIFICO_PAYBOT_NODEJS_ERROR’));

}
// Объявляем дескрипторы для чтения результатов вызова с stdout/stderr
$descriptors = [

1 => [‘pipe’, ‘w’],
2 => [‘pipe’, ‘w’],

];
// Запускаем подготовленный скрипт
$process = proc_open($env[‘NODEJS_BIN’] . ‘ ‘ . ‘paybot.js’, $descriptors, $pipes, Loader::getLocal(‘/modules/’.static::MODULE_ID.’/browser’), $env);
// Выходим, если не получилось запустит
if (false === $process) {

throw new \Exception(Loc::getMessage(‘MAGNIFICO_PAYBOT_PROCESS_ERROR’));

}
// Читаем содержимое объявленных ранее дескрипторов
list($stdout, $stderr) = [stream_get_contents($pipes[1]), stream_get_contents($pipes[2])];

AddMessage2Log([‘stdout’ => $stdout, ‘stderr’ => $stderr], static::MODULE_ID);
// Если код возврата ненулевой – была ошибка, и в stderr будет её текст
if (0 !== proc_close($process)) {

throw new \Exception(‘:!: ‘.$stderr);

}
// Тотальный успех
return true;

}

Хотелось бы, чтобы в один момент времени на сервере было запущено не более, чем один инстанс хрома. В противном случае, если одновременно придёт много запросов на создание платежа, может закончиться оперативная память и сервер ляжет. Чтобы этого не случилось – реализуем простейший мутекс через механизм блокировок в MySQL.

public static function createPayment($clientId, $amount, $comment = ”, $recipientId = ”)
{

$connection = \Bitrix\Main\Application::getInstance()->getConnection();

$connection->queryExecute(‘SELECT GET_LOCK(“‘.__CLASS__.'”, 60)’);

$success = static::doCreatePayment($clientId, $amount, $comment, $recipientId);

$connection->queryExecute(‘SELECT RELEASE_LOCK(“‘.__CLASS__.'”)’);

return $success;

}

Теперь вызовом createPayment можно создавать платежи. Со стороны PHP — остается только добавить в админке страницу настроек модуля:

настройки модуля

paybot.js

Весь скрипт умещается в одном файле. Нам достаточно одного try .. catch, чтобы любая возникшая ошибка просто уходила бы в stderr и дальше обрабатывалась в PHP:

const puppeteer = require(‘puppeteer’);

(async () => {

try {

// here be dragons
} catch (err) {
console.error(‘[ERROR]’, err);
process.exit(1);

}
process.exit(0);

})();

Перед началом работы нужно запустить браузер:

console.log(‘[DEBUG] Launching browser…’);
const browser = await puppeteer.launch({executablePath: process.env[‘CHROME_BIN’], args: [‘–no-sandbox’, ‘–disable-setuid-sandbox’]});

После чего открыть вкладку в созданном браузере:

console.log(‘[DEBUG] Creating new page…’);
const page = await browser.newPage();

Когда в системе возникает какая-либо ошибка, она по старинке выдаст её алертом и скрипт наглухо зависнет, поэтому молча соглашаемся со всеми алертами:

console.log(‘[DEBUG] Disabling javascript alerts…’);
page.on(‘dialog’, async (dialog) => {

await dialog.accept();

});

Авторизуемся в системе, вводя полученный из PHP логин и пароль. Наличие ошибки авторизации определяем по специфической строке в теле страницы.

console.log(‘[DEBUG] Opening authorization page…’);
await page.goto(‘http://example.com/office/login.asp’);

console.log(‘[DEBUG] Enter credentials…’);
await page.evaluate((env) => {

document.querySelector(‘input[name=”CODE”]’).value = env[‘AUTH_USERNAME’];
document.querySelector(‘input[name=”PASSWORD”]’).value = env[‘AUTH_PASSWORD’];
document.querySelector(‘input[type=”submit”]’).click();

}, process.env);

console.log(‘[DEBUG] Waiting for authorization…’);
await page.waitForNavigation({waitUntil: ‘load’});
if ((await page.content()).search(‘Неправильные пароль или код’) > -1) {

throw new Error(‘Ошибка авторизации’);

}

Переходим на форму создания платежа и также заполняем её данными из PHP:

console.log(‘[DEBUG] Opening payment form…’);
await page.goto(‘http://example.com/office/operation.asp?CID=’ + process.env[‘CLIENT_ID’]);

console.log(‘[DEBUG] Checking if client exists…’);
if ((await page.content()).search(‘НЕТ ДОСТУПА ИЛИ НЕ ОПОЗНАН’) > -1) {

throw new Error(‘Клиент не существует’);

}

console.log(‘[DEBUG] Enter payment fields…’);
await page.evaluate((env) => {

if (env[‘AMOUNT’]) {

const realsum = document.querySelector(‘input[name=”realsum”]’);
realsum.value = env[‘AMOUNT’];
(‘function’ === typeof realsum.onchange) && realsum.onchange();

}

if (env[‘COMMENT’]) {

const comment = document.querySelector(‘input[name=”comment”]’);
comment.value = env[‘COMMENT’];
(‘function’ === typeof comment.onchange) && comment.onchange();

}

if (env[‘RECIPIENT_ID’]) {

const credit = document.querySelector(‘select[name=”Credit”]’);
credit.value = env[‘RECIPIENT_ID’];
(‘function’ === typeof credit.onchange) && credit.onchange();

}

document.querySelector(‘input[name=”OK”]’).click();

}, process.env);

console.log(‘[DEBUG] Waiting for payment creation…’);
await page.waitForNavigation({waitUntil: ‘load’});

И не забываем закрыть браузер:

console.log(‘[DEBUG] Closing browser…’);
await browser.close();