Интеграция Битрикс24 с внешней системой без API
Разработка
Разработка

Интеграция Битрикс24 с внешней системой без API

Время от времени к нам обращаются за помощью по интеграции Битрикс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();

Выводы

Нет ничего невозможного. Есть недостаточная степень необходимости.
Игорь Денисенко
Игорь Денисенко
Технический директор БизнесПрофи

Статьи по теме

Как выбрать хостинг для Битрикс24
«1C-Битрикс24» — универсальная и многофункциональная система управления данными, разработанная с высоким уровнем программирования относительно систем хранения данных, что делает ее требовательной к хостингу, на котором должен работать портал.
Подробнее
Как мы разработали чат-бота для Битрикс24
Расскажем, как автоматизировать рутину и сэкономить время сотрудников с помощью специализированного чат-бота, заменяющего ручную работу.
Подробнее