Каталог решений - Прокси soap-сервер. Когда 1С не может в SOAP

Прокси soap-сервер. Когда 1С не может в SOAP

Прокси soap-сервер. Когда 1С не может в SOAP

В наличии

Думаю, многие сталкивались с проблемой «своеобразной» поддержки SOAP в 1с. То wsdl не парсится, то методы не вызываются, то xdto пакет толком не читается из-за вольностей (к слову, допустимых) в xsd. Ещё хуже дело обстоит, если в soap-сообщение нужно добавить заголовки — так называемые soap-headers, которые есть в стандарте soap 1.2, и которые конечно же не поддерживает 1С.

Попробуем решить проблему и написать свой прокси-сервис «за 10 минут».

Категория:

Описание

Вступление

Кто-то может сказать: "Ой, да что там руками формировать, XML простая". Однако протокол содержит некоторое количество расширений, таких как, например, WS-Addressing и WS-Security, которые могут превратить ручное формирование в боль.

На работе мне пришлось столкнуться с довольно замороченным soap-сервером, с которым не получалось легко работать из 1С. Сегодняшняя моя статья про то, как можно разработать легковесную прослойку между 1С и soap-сервером, принимающую в себя обычный http-запрос и перекладывающую содержимое в вызов soap-сервера. Естественно, код в статье максимально упрощен для простоты восприятия. Код работающего у меня production-решения сильно отличается от указанного примера и более «архитектурный» :).

Подготовка

В качестве инструмента для решения задачи я буду использовать node.js. Почему? Во-первых, мне так удобнее: я его знаю :). Во-вторых, на нем есть простые для запуска библиотеки для построения веб-приложений, работы с soap и кластеризацией. В качестве редактора я рекомендую использовать Visual Studio Code, но это уже дело вкуса. Тренироваться будем на классическом сервисе курсов валют.

После установки node.js в командной строке вам должна быть доступа утилита npm — пакетный менеджер для node.js. Для работавших с opm — это почти тоже самое, только для node.js и мощнее :).

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

После сразу установим все библиотеки, которые нам понадобятся для нашей прослойки с помощью команды:

npm install --save soap body-parser express

Файл с wsdl положим в корень каталога с именем DailyInfo.wsdl в кодировке UTF-8.

Для достижения нашей цели нам надо решить следующие задачи:

  1. Написать веб-сервер, который сможет принимать POST запросы (это совсем не так сложно, как звучит).

  2. Подключиться к soap-серверу как клиент.

  3. Преобразовать входящий POST-запрос в вызов soap-метода и вернуть на клиент результат.

Страшно? 10 минут, помните?

Реализация – веб-сервер

Создадим скелет нашего приложения — файл index.js в корневом каталоге (если вы не указывали иное при выполнении npm init) со следующим содержимым:

// express – фреймворк для построения веб-приложений
const express = require('express');
// Преобразователь тела сообщения к объекту JavaScript. Мы его будем
// использовать для автопреобразования сообщения с Content-Type
// application/json из собственно JSON в объект.
const bodyParser = require("body-parser");

// Объявление главной функции. Async-возможность нам понадобится чуть позднее.
async function main() {
    // Порт, который будет слушать веб-сервер
    const port = 3000;
    // Создание экземпляра веб-приложения
    const app = express();

    // Указание реагировать на POST-запрос
    app.post(
        "/",                // по «пустому» ресурсу
        bodyParser.json(),  // с автоматическим преобразованием json-содержимого
        (req, res) => {     // и выводом Привет, мир :)
            res.send("Hello, World");
        }
    );

    // Запуск приложения – указание слушать порт
    // и выводить сообщение в консоль по готовности
    app.listen(port, () => console.log(`Test app listening on port ${port}!`));
}

// Точка входа
main();

Этим небольшим скриптом мы сразу же решили задачу №1 из нашего списка. Осталось запустить и проверить.

Для запуска приложения у нас есть два варианта:

  • запуск из командной строки через node index.js;

  • запуск отладчика в VSCode.

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

Останавливаем работу через Ctrl-C.

Отладчик VSCode запускается по кнопке F5. В выпадающем меню надо выбрать Node.js:

После выбора node.js на вкладке Debug console можно убедиться, что наше приложение запустилось и готово обрабатывать запросы:

Для проверки работоспособности я воспользуюсь чудесным инструментом отладки http-запросов Postman:

В ответе сервиса видим, что он не может обработать GET-запрос, что логично. Поменяем запрос на POST и получим уже ожидаемый ответ:

Реализация – soap-клиент

Перейдем ко второй части – подключение по soap-серверу в качестве клиента. Для этого в уже существующий файл нужно добавить два участка кода. В секцию подключения библиотек добавим подключение “soap” – библиотеки, с помощью которой можно как подключиться к чужому soap-серверу, так и опубликовать собственный.

const soap = require("soap");

Внутрь функции main добавим создание soap-клиента:

// создание soap-клиента на базе предварительно скачанной wsdl.
// В качестве параметра может выступать как адрес к файлу на диске,
// так и URL, по которому этот WSDL можно получить (прямо как WS-Ссылка)
const soapClient = await soap.createClientAsync("./DailyInfo.wsdl");
soapClient.setEndpoint("http://www.cbr.ru/DailyInfoWebServ/DailyInfo.asmx");

Иии… всё. Теперь через созданного клиента мы можем вызывать методы soap-сервера, как указанные с «полным» именем в виде soapClient,service.port.methodName(), так и по короткому soapClient.methodName().

Реализация – Преобразование запроса

Добавим, собственно, вызов нужного нам soap-метода. В качестве API нашего сервиса предлагаю такую простую схему: в теле POST-запроса передается JSON следующей структуры:

  • method – строка – имя вызываемого soap-метода;

  • body – произвольный – тело soap-запроса в виде js-объекта.

Таким образом, получение списка валют на определенную дату через наш промежуточный сервис может выглядеть так:

{
      "method": "GetCursOnDate",
      "body": { "On_date": "2018-01-01" }
}

Заменим наш ответ «привет, мир» на следующий код:

// Десериализованное тело запроса доступно в переменной req.body
// В случае корректного запроса req.body будет содержать два свойства:
// method и body

// Попробуем получить указатель на функцию для вызова soap-метода
const soapMethod = soapClient[req.body.method];

// Если метод не нашелся, выбросим исключение
if (soapMethod == undefined) {
    throw new Error("Wrong method name");
}

// Если все хорошо, вызовем soap-метод, передав ему в качестве параметров
// тело сообщения и обработчик результата вызова
soapMethod(req.body.body, (err, result) => {
    
    // В случае возникновения ошибки вернем ее клиенту.
    if (err) {
        res.send(err);
        return;
    }

    // Если все хорошо, переведем ответ в JSON и вернем клиенту.
    res.send(JSON.stringify(result));
});

Кода меньше, чем комментариев :). Сохраняемся, перезапускаемся и снова идем в Postman. На вкладке body укажем, что мы отправляем raw-данные с типом application/json и содержимым из примера выше:

В результате видим тело soap-ответа в виде JSON.

Реализация – вызов из 1С

Postman – это хорошо, но мы же изначально пришли с проблемой вызова из 1С. Выполнить обычный POST-запрос из 1С не составит труда, однако, я приведу пример реализации здесь, чтобы показать работу с JSON и XDTO.

Для начала добавим пакет XDTO в конфигурацию 1С. Если WSDL от поставщика soap-сервера читается, можно сразу добавить WS-ссылку. Сэмулируем проблему "нечитабельности" wsdl и добавим XDTO пакет вручную. В этом нам поможет знание о том, что WSDL содержит XSD для содержимого всех сообщений и методов.

Вытащим из WSDL все содержимое тега s:schema в отдельный файл и перенесем объявление пространства имен s из заголовка WSDL в заголовок нового файла. Получится что-то вроде такого:

Сохраним содержимое в файл с разрешением XSD, и, если все прошло успешно, полученная схема успешно импортируется в конфигуратор как XDTO пакет:

Если от вендора пришла «нечитаемая» в 1С XSD, то использование фабрики XDTO из следующего примера не имеет смысла, однако десериализацию из JSON будет просто написать по аналогии с сериализацией.

Для формирования запросов создадим внешнюю обработку со следующим кодом:

&НаСервереБезКонтекста
Процедура ВыполнитьЗапросНаСервере()

    // Создадим тело нашего запроса - параметры вызываемого soap-метода
    ТелоЗапроса = Новый Структура;
    ТелоЗапроса.Вставить("On_date", Дата(2018, 1, 1));

    // Сериализуем его в JSON
    ТекстСообщения = СериализоватьВJSON(ТелоЗапроса);

    // Проверим, что полученный JSON удовлетворяет XSD

    // Если бы у данного свойства был бы выделенный "тип объекта",
    // то мы бы могли получить тип проще...
    //ТипXDTO = ФабрикаXDTO.Тип(Метаданные.ПакетыXDTO.ПакетXDTO1.ПространствоИмен, "GetCursOnDate");

    КорневыеСвойства = ФабрикаXDTO.Пакеты.Получить(Метаданные.ПакетыXDTO.ПакетXDTO1.ПространствоИмен).КорневыеСвойства;
    СвойствоЗапросКурсовНаДату = КорневыеСвойства.Получить("GetCursOnDate");
    ТипXDTO = СвойствоЗапросКурсовНаДату.Тип;

    ЧтениеJSON = Новый ЧтениеJSON;
    ЧтениеJSON.УстановитьСтроку(ТекстСообщения);

    // Если здесь не выдалось исключения, значит, пакет корректен и его можно отправлять.
    ФабрикаXDTO.ПрочитатьJSON(ЧтениеJSON, ТипXDTO); 

    // Создадим верхнеуровневую структуру, принимаемую промежуточным сервером
    СтруктураЗапроса = Новый Структура;
    СтруктураЗапроса.Вставить("method", "GetCursOnDate");
    СтруктураЗапроса.Вставить("body", ТелоЗапроса);

    // Сериализуем его в JSON для последующей отправки
    ТекстСообщения = СериализоватьВJSON(СтруктураЗапроса);

    // Создадим новое соединение с промежуточным сервером
    Хост = "localhost";
    Порт = 3000;
    Таймаут = 30;

    Соединение = Новый HTTPСоединение(Хост, Порт, , , , Таймаут);   

    // "Корневой" адрес ресурса, как мы его объявили в app.post
    Ресурс = "/";

    // Обязательно передаем тип содержимого для работы преобразователя body-parser
    ЗаголовкиЗапроса = Новый Соответствие();
    ЗаголовкиЗапроса.Вставить("Content-type", "application/json");

    // Создаем и отправляем запрос
    Запрос = Новый HTTPЗапрос(Ресурс, ЗаголовкиЗапроса);
    Запрос.УстановитьТелоИзСтроки(ТекстСообщения);

    Ответ = Соединение.ОтправитьДляОбработки(Запрос);   
    ТелоОтвета = Ответ.ПолучитьТелоКакСтроку();

    // Десериализуем ответ сервиса из JSON в объект XDTO
    ЧтениеJSON = Новый ЧтениеJSON;
    ЧтениеJSON.УстановитьСтроку(ТелоОтвета);

    ТипXDTO = ФабрикаXDTO.Тип(Метаданные.ПакетыXDTO.ПакетXDTO1.ПространствоИмен, "GetCursOnDateResponse");
    Данные = ФабрикаXDTO.ПрочитатьJSON(ЧтениеJSON, ТипXDTO);

КонецПроцедуры

&НаСервереБезКонтекста
Функция СериализоватьВJSON(Объект)
    Запись = Новый ЗаписьJSON;
    Запись.УстановитьСтроку();
    ЗаписатьJSON(Запись, Объект);
    ТекстСообщения = Запись.Закрыть();

    Возврат ТекстСообщения;
КонецФункции

&НаКлиенте
Процедура ВыполнитьЗапрос(Команда)
    ВыполнитьЗапросНаСервере();
КонецПроцедуры

Некоторые пояснения по коду.

Первое, за что может зацепиться взгляд – использование «обычной» ЗаписиJSON вместо ФабрикиXDTO. Причина тут довольно проста – ФабрикаXDTO умеет записывать произвольные типы, но с «мусорными» тегами “#value” и “#type” (тип добавляется, только если это указано явно в настройках записи). Наш промежуточный сервер ни про какие value ничего не знает. Выхода тут два – либо научить понимать сервер, либо использовать упрощенный сериализатор на базе структуры и записи. Выбор за вами.

Второе – пляски с бубном вокруг ФабрикиXDTO. Это всего лишь проверка валидности нашего сообщения. Мы же порядочные граждане, хотим быть уверены, что мы не шлем soap-серверу что-то, чего он не ожидает. В конкретно данном случае дополнительный реверанс пришлось сделать для получения типа создаваемого свойства, т. к. исходная wsdl вообще не содержит явных описаний типов значений, а только описания свойств с вложенными описаниями типов.

А вот чтение сообщения мы уже выполним «честной» ФабрикойXDTO для получения объекта XDTO и возможности работы в объектной модели.

Ставим в конец процедуры точку останова, выполняем обработку… Вуаля:

Цель достигнута!

Кластеризация

Окей, сервис готов, можно в прод? 🙂

Если не страшно, то можно сразу и в прод, однако, я бы на вашем месте помимо обработки ошибок и общего причесывания кода добавил бы еще одну вещь. Node.js штука хоть и быстрая, но не всемогущая. Возможно вам знакома фраза, что «нода – асинхронная, но однопоточная». В новых версиях node.js уже появилась честная поддержка многопоточности, но для простоты воспользуемся другим старым и проверенным механизмом – кластеризацией. А асинхронность обработки в нашем случае есть, но нам не помешает воспользоваться дешевым ускорителем.

Ставим пакет cluster-service с помощью команды:

npm install -g cluster-service

Запускаем наше приложение в командной строке, но в вместо node укажем приложение cservice:

Наш промежуточный сервис запустился в режиме кластера с количеством потоков, равным количеству логических процессоров. Можете выполнить нагрузочное тестирование через тот же SoapUI и замерить количество обрабатываемых запросов в секунду при обычном запуске и при кластеризованном запуске – заметите ощутимую разницу.

Что там было про WS-Addressing?

Ах-да, заголовки, те самые soap-headers, которые не поддерживает 1С. Добавить их довольно просто – для этого в soapClient есть метод addSoapHeaders, в который можно передать либо готовую строку с заголовками, либо JS-объект. Попробуем реализовать добавление пары заголовков семейства WS-Addressing, а именно Action и MessageID.

Для генерации UUID сообщения установим библиотеку uuid:

npm install --save uuid

Добавим ее в секцию импорта библиотек:

const uuidv4 = require("uuid/v4");

Между проверкой указателя на soap-метод и самим вызовом soap-метода добавим заполнение заголовков:

// Создадим объект для хранения заголовков
const wsaHeader = {
    MessageID: {
        // В качестве значения для заголовка MessageID сгенерируем случайный UUID
        $value: uuidv4()
    },
    Action: {
        // Для Action передадим имя вызываемого метода, как того требует протокол
        $value: req.body.method
    }
};

// Очистим заголовки soap-запроса
soapClient.clearSoapHeaders();

// Добавим новый заголовок
soapClient.addSoapHeader(
    wsaHeader,    // объект, в котором хранятся заголовки
    "WSA",        // имя группы заголовков
    "wsa",        // префикс пространства имен
    "http://www.w3.org/2005/08/addressing" // само пространство имен
);

Убедиться в корректности отправляемых заголовков можно через тот же SoapUI или настроив логирование запросов в промежуточном сервере.

Дополнительные вопросы

— А зачем JSON? Можно гонять туда-сюда XML?
— Можно, но зачем, если есть возможность гонять более легковесный JSON, а 1С уже умеет нативно с ним работать?

— Можно ли накрыть авторизацией?
— Можно, причем и веб-приложение (для этого надо добавить еще один middle-ware с авторизацией в вызов app.post), и soap-сервер, который можно поднять в этом же приложении как сервис обратного вызова в случае асинхронного soap-обмена.

Заключение

Вот таким нехитрым способом мы смогли обойти ограничение возможностей платформы 1С, таких как отсутствие поддержки soap-headers и не полной поддержки WSDL-описания.

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

Полный код получившегося приложения, а также исходники обработки доступны в репозитории на GitHub.

P.S. В процессе написания статьи я в очередной раз вспомнил, почему я так люблю TypeScript — за ошибки во время компиляции, типизацию и более умную контекстную подсказку. Если у вас еще остались силы, то в качестве домашнего задания можете повторить этот же пример на TypeScript, благо настроить единственный json-файл с конфигом можно тоже почти автоматически.

has been added to your cart:
Оформление заказа