Реализация протокола удаленного вызова процедур в формате JSON (JSON-RPC)

Удалённый вызов процедур (от англ. Remote Procedure Call, RPC) — технология, позволяющая программам вызывать функции и процедуры на удалённых компьютерах (более точная формулировка: «вызывать функции или процедуры в другом адресном пространстве», т.е. это не обязательно другой компьютер). В данной публикации описана реализация этой технологии в 1С. В качестве протокола для передачи сообщений используется JSON (JSON-RPC), что позволяет удалённым системам (например, веб-приложениям) вызывать функции 1С (только те функции, которые определит разработчик).

 

Общие сведения.

 

Существует множество способов реализации обмена данными сторонних программ с 1С с использованием HTTP-сервисов. Данная реализация позволяет определить один HTTP-сервис и просто добавлять нужные процедуры в общий модуль без переработки (изменения существующих или добавления новых) HTTP-сервисов. В качестве формата протокола используется JSON.

Ниже приведено краткое описание JSON-RPC 2.0.

Запрос должен содержать три обязательных свойства:

  • method — Строка с именем вызываемого метода.
  • params — Массив данных, которые должны передаваться методу, как параметры.
  • id — Значение любого типа, которое используется для установки соответствия между запросом и ответом.

Так же должна быть передана версия протокола (в нашем случае "2.0") в параметре jsonrpc. В спецификации протокола этот параметр не указан, как обязательный, но в реализации данной публикации его указание обязательно.

Пример запроса для вызова процедуры ПолучитьСчетаКлиента(ИНН, НачалоПериода, КонецПериода) (данная процедура использована в качестве примера, конкретные процедуры определяет разработчик):

{"jsonrpc": "2.0", "method": "ПолучитьСчетаКлиента", "params": ["7705260674", "2024-09-01", "2024-09-30"], "id": 1}

Сервер (в нашем случае — решение на 1С) отсылает ответ, содержащий следующие свойства:

  • result — Результат выполнения метода. Если во время выполнения метода произошла ошибка, то это свойство должно быть установлено в null.
  • error — Код ошибки, если произошла ошибка во время выполнения метода, иначе null.
  • id — То же значение идентификатора, что и в запросе, к которому относится данный ответ (используется для сопоставления самого запроса и его результата, например, если асинхронно отправляется сразу несколько запросов).

Пример ответа:

{"jsonrpc": "2.0", "result": [{"uid": "422f9d5c-1032-11e5-92f1-0050568b35ac", "num": "ТД00-000001", "date": "2024-09-11T16:06:15", "sum": 127125}], "id": 1}

Более подробно с протоколом JSON-RPC можно ознакомиться на википедии.

В данной публикации удалённый вызов процедур 1С реализован в виде расширения. В этом расширении присутствует HTTP-сервис rpc_JSONRPC2. После публикации http-сервиса на веб-сервере сторонние системы (в качестве которой может выступать и другая база 1С) смогут выполнять запросы по адресу http://<адрес_базы>/hs/jsonrpc2.

Вызываемые удалённые процедуры должны располагаться в общем модуле расширения rpc_УдаленныеПроцедурыПереопределяемый.

Особенности реализации 1С.

Для упрощения создания собственных удалённых процедур были реализованы следующие особенности:

  • Передаваемые в запросе JSON строковые значения параметров true и false преобразуются в тип 1С Булево.
  • Передаваемые в запросе JSON строковые значения параметров типа 2024-01-10Т10:23:54 (дата в формате XML) преобразуются в тип 1С Дата.
  • Передаваемые в запросе JSON строковые значения параметров типа GUID преобразуются в тип 1С УникальныйИдентификатор.
  • Удалённая процедура (в 1С функция) должна возвращать только значения, которые могут быть сериализованы в JSON. Исключение составляет тип ТаблицаЗначений. В типовом механизме сериализации JSON этот тип не может использоваться, но в данном решении для упрощения возврата ответа (чтобы разработчик не писал лишний код) таблица значений преобразуется в массив структур.

Для передачи ошибок используется вызов исключения.

Например, вот так можно реализовать обработку ошибок в приведённой выше в качестве примера процедуре ПолучитьСчетаКлиента:

Функция ПолучитьСчетаКлиента(ИНН, НачалоПериода, КонецПериода) Экспорт

Контрагент = НайтиКонтрагентаПоИНН(ИНН);
Если Контрагент.Пустая() Тогда
ТекстСообщения = СтрШаблон(НСтр("ru='Не найден клиент с ИНН: %1'"), ИНН);
ВызватьИсключение ТекстСообщения;
КонецЕсли;

...

КонецФункции

В таком случае, если в запросе будет передан ИНН контрагента, отсутствующего в информационной базе, то клиентом будет получен ответ следующего вида:

{"jsonrpc": "2.0", "error": {"code": -32800, "message": "Не найден клиент с ИНН: 0777123413"}, "id": 1}

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

  • -32600, "Invalid JSON-RPC" — возвращается, если передан неверный формат сообщения (не удалось прочитать JSON) или отсутствуют обязательные параметры;
  • -32700, "Parse error" — возвращается, если переданы неверные параметры (например, отсутствует параметр method или параметр params не является массивом и т.п.);
  • -32601, "Procedure not found" — вызываемая функция отсутствует в общем модуле rpc_УдаленныеПроцедурыПереопределяемый;
  • 32800, <текст ошибки> — ошибка, возникшая во время выполнения метода (см. пример ответа с ошибкой выше).

 

Перейдём от теории к практике.

 

В качестве примера будем использовать конфигурацию Управление торговлей, редакция 11 (конкретный релиз для примера не имеет значения).

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

В данном случае порядок действий будет следующим:

1. Добавляем в конфигурацию расширение из этой публикации и публикуем HTTP-сервис rpc_JSONRPC2.

2. Открываем общий модуль расширения rpc_УдаленныеПроцедурыПереопределяемый и реализуем процедуру ПолучитьСчетаКлиента:

Функция ПолучитьСчетаКлиента(ИНН, НачалоПериода, КонецПериода) Экспорт

Контрагент = НайтиКонтрагентаПоИНН(ИНН); // Реализация данной функции не представляет проблем, оставим её "за скобками".
Если Контрагент.Пустая() Тогда
ТекстСообщения = СтрШаблон(НСтр("ru='Не найден клиент с ИНН: %1'"), ИНН);
ВызватьИсключение ТекстСообщения;
КонецЕсли;

ТаблицаСчетов = Новый ТаблицаЗначений;
ТаблицаСчетов.Колонки.Добавить("uid", Новый ОписаниеТипов("УникальныйИдентификатор"));
ТаблицаСчетов.Колонки.Добавить("num", Новый ОписаниеТипов("Строка",, Новый КвалификаторыСтроки(11)));
ТаблицаСчетов.Колонки.Добавить("date", Новый ОписаниеТипов("Дата",,, Новый КвалификаторыДаты(ЧастиДаты.ДатаВремя)));
ТаблицаСчетов.Колонки.Добавить("sum", Новый ОписаниеТипов("Число", Новый КвалификаторыЧисла(15, 2)));

Запрос = Новый Запрос(
"ВЫБРАТЬ
| СчетНаОплатуКлиенту.Ссылка КАК Ссылка,
| СчетНаОплатуКлиенту.Номер КАК num,
| СчетНаОплатуКлиенту.Дата КАК date,
| СчетНаОплатуКлиенту.СуммаДокумента КАК sum
|ИЗ
| Документ.СчетНаОплатуКлиенту КАК СчетНаОплатуКлиенту
|ГДЕ
| СчетНаОплатуКлиенту.Контрагент = &Контрагент
| И СчетНаОплатуКлиенту.Дата МЕЖДУ &НачалоПериода И &КонецПериода");
Запрос.УстановитьПараметр("Контрагент", Контрагент);
Запрос.УстановитьПараметр("НачалоПериода", НачалоДня(НачалоПериода));
Запрос.УстановитьПараметр("КонецПериода", КонецДня(КонецПериода));

Выборка = Запрос.Выполнить().Выбрать();
Пока Выборка.Следующий() Цикл
СтрокаТаблицы = ТаблицаСчетов.Добавить();
ЗаполнитьЗначенияСвойств(СтрокаТаблицы, Выборка);
СтрокаТаблицы.uid = Выборка.Ссылка.УникальныйИдентификатор();
КонецЦикла;

Возврат ТаблицаСчетов;

КонецФункции

 

3. Важный момент. Чтобы была возможность выполнять удаленный вызов данной функции нужно добавить её имя в процедуру УдаленныеПроцедуры общего модуля rpc_УдаленныеПроцедуры. Ниже приведён пример такой процедуры:

Функция УдаленныеПроцедуры() Экспорт

УдаленныеПроцедуры = Новый Массив;

///////////////////////////////////////////////////////////////////////////////////////////////////////
// Необходимо добавить следующие строки:
//
УдаленныеПроцедуры.Добавить("ПолучитьСчетаКлиента");
УдаленныеПроцедуры.Добавить("АннулироватьСчетКлиента");
///////////////////////////////////////////////////////////////////////////////////////////////////////

Возврат УдаленныеПроцедуры;

КонецФункции

Это сделано для безопасности с целью исключения выполнения произвольного кода, непредусмотренного разработчиком. Дело в том, что для вызова процедур используется метод 1С Выполнить:

Выполнить("Результат = rpc_УдаленныеПроцедурыПереопределяемый." + ИмяМетода + "(" + ... + ")";

Злоумышленник может в параметре JSON params передать строку, например для описанной выше функции

{"jsonrpc": "2.0", "method": "ПолучитьСчетаКлиента("7705260674", ‘20240901’, ‘20240930’); ВредныйМодуль.УдалитьВсе()", …}

В результате чего будет выполнен код:

Выполнить("Результат = rpc_УдаленныеПроцедурыПереопределяемый.ПолучитьСчетаКлиента(""7705260674"", '20240901', '20240930'); ВредныйМодуль.УдалитьВсе() ...

Т.е. после выполнения предусмотренного разработчиком метода "ПолучитьСчетаКлиента" начнёт выполняться произвольный код, который выполняться не должен!

Благодаря добавлению имён процедур в методе УдаленныеПроцедуры общего модуля rpc_УдаленныеПроцедуры такая возможность исключается. Для системы в таком случае имя метода будет не "ПолучитьСчетаКлиента", а ПолучитьСчетаКлиента("7705260674", ‘20240901, ‘20240930); ВредныйМодуль.УдалитьВсе(). Такая строка не определена в методе УдаленныеПроцедуры общего модуля rpc_УдаленныеПроцедуры, поэтому ни какой код выполняться не будет, а будет выдано сообщение об ошибке.

 

Теперь веб-программисту достаточно будет выполнить POST запрос по адресу http://<адрес_базы>/hs/jsonrpc2 в формате вида

{"jsonrpc": "2.0", "method": "ПолучитьСчетаКлиента", "params": ["7705260674", "2024-09-01", "2024-09-30"], "id": 1}

чтобы получить необходимые данные.

Всё работает, всё хорошо, но возникает новое требование — дать возможность клиенту из личного кабинета аннулировать счёт на оплату. И вот разработчик личного кабинета просит разработчика 1С реализовать такую возможность.

Для этого достаточно вновь открыть общий модуль расширения rpc_УдаленныеПроцедурыПереопределяемый и реализовать новую процедуру АннулироватьСчетКлиента(ИдентификаторСчета).

Процедура АннулироватьСчетКлиента(ИдентификаторСчета) Экспорт

СчетКлиента = Документы.СчетНаОплатуКлиенту.ПолучитьСсылку(ИдентификаторСчета);

МассивСчетов = Новый Массив;
МассивСчетов.Добавить(СчетКлиента);

Попытка
Документы.СчетНаОплатуКлиенту.УстановитьПризнакАннулирован(МассивСчетов);
Исключение
ЗаписьЖурналаРегистрации("JSON-RPC.АнулироватьСчетКлиента",
УровеньЖурналаРегистрации.Ошибка,,,
КраткоеПредставлениеОшибки(ИнформацияОбОшибке()));
КонецПопытки;

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

Теперь, когда клиент в личном кабинете в меню, например, выберет команду "Аннулировать счёт", веб-разработчик просто выполнит POST запрос к базе 1С вида

{"jsonrpc": "2.0", "method": "АнулироватьСчетКлиента", "params": ["422f9d5c-1032-11e5-92f1-0050568b35ac"]}

Хочу обратить внимание, что данная процедура не предполагает возвращение результата, поэтому передавать параметр id не нужно (в терминах JSON-RPC это называется уведомлением). Примечание. Использовано для примера, в реальности, конечно, нужно возвращать ошибку, если по какой-то причине не удалось аннулировать счёт, например, если отсутствует счёт с таким уникальным идентификатором.

Расширение предназначено для любой конфигурации на платформе не ниже 8.3.6. Протестировано на платформе версии 8.3.15.1656.

11 Comments

  1. zeegin

    https://its.1c.ru/db/v8std#content:770:hdoc

    https://ru.m.wikipedia.org/wiki/%D0%92%D0%BD%D0%B5%D0%B4%D1%80%D0%B5%D0%BD%D0%B8%D0­%B5_SQL-%D0%BA%D0%BE%D0%B4%D0%B0

    Функции Вычислить/Выполнить надо вызывать в безопасном режиме.

    Все передаваемые параметры надо экранировать.

    Не безопасно.

    Reply
  2. ltfriend

    (2) вот из-за заголовка я и не нашёл ту публикацию. Перед своей реализации JSON-RPC я же помнил, что была подобная статья и я там даже оставлял свой комментарий, но перед созданием своей публикации найти её не смог.

    Reply
  3. ltfriend
    Reply
  4. zeegin

    (4) В данном случае, конечно, имелось ввиду инъекция не в запросы, а в квери. Т.е. вообще в любое поле, в квери, в заголовки запросов, в коммандную строку. Главное не допустить исполнения произвольного кода.

    Безопасно — это не выполнять произвольного кода. В случаях RPC как и впринципе в REST лучше делать контроллер, который будет определять кому передать управление, т.е. то, что определил разработчик.

    Я бы сделал просто в переопределяемом модуле процедуру:

    ПриОбработкеЗапроса(ИмяМетода, Параметры)

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

    Если ИмяМетода = «ПолучитьСчетаКлиента» Тогда

    МодульРеализацииОбработки.ПолучитьСчетаКлиента(Параметры);

    КонецЕсли;

    Reply
  5. ltfriend

    (5) соглашусь, что так безопасней, но требует больше кода. Идея была в том, чтобы разработчик просто добавлял новую функцию в модуль rpc_УдаленныеПроцедурыПереопределяемый и она тут же становилась доступна для вызова без дополнительного кода

    …
    ИначеЕсли ИмяМетода = «…» Тогда
    ….
    КонецЕсли
    

    Поэтому я пошёл на компромисс. Сделал отдельную функцию, где разработчик определяет доступные функции. С одной стороны кода стала не намного больше. По сути, добавить одну строку

    УдаленныеПроцедуры.Добавить(«<имя_процедуры>»)

    но с другой стороны, по моему мнению, решает проблему с безопасностью.

    Reply
  6. zeegin

    (6) Ну тогда правильно сделать в rpc_УдаленныеПроцедурыПереопределяемый процедуру

    Процедура ПриОпределенииДопустимыхУдаленныхПроцедур(Процедуры) Экспорт
    
    Процедуры.Добавить(«ПолучитьСчетаКлиента»);
    
    КонецПроцедуры

    https://its.1c.ru/db/v8std#content:553

    3.2. Переопределяемые общие модули должны содержать только экспортные процедуры.

    Ну и всегда надо помнить, что несмотря на то, что HTTP сервисы это серверный контекст, то, что приходит оттуда — идеологически — это клиентский вызов.

    https://its.1c.ru/db/v8std#content:678

    2. Проникновение небезопасного кода на сервер и его выполнение.

    Любые возможности конфигурации по выполнению «внешнего» кода или произвольных текстов запросов на сервере, не являющихся частью самого прикладного решения, представляют серьезную опасность.

    Это значит, что на все вызываемые процедуры надо накладывать ограничение

    3. Серверные процедуры и функции должны возвращать в форму только окончательный результат расчета.

    При этом если пользователь, используемый для удаленного вызова не имеет собственного профиля доступа, или еще хуже, используется привилегированный режим, то требование:

    https://its.1c.ru/db/v8std#content:485

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

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

    Reply
  7. JohnyDeath

    (4)

    А вот с передачей параметров ни каких проблем с безопасностью я не обнаружил.

    А если вот так:

    {«jsonrpc»: «2.0», «method»: «ПолучитьСчетаКлиента», «params»: [«ВредныйМодуль.УдалитьВсе()», «2019-09-01», «2019-09-30»], «id»: 1}
    Reply
  8. ltfriend

    (8) То значением переданного параметра в функцию будет строка «ВредныйМодуль.УдалитьВсе()».

    Строка кода, который выполняется имеет следующий вид

    Выполнить(Результат = rpc_УдаленныеПроцедурыПереопределяемый.<Имя_Метода>(Параметры[0], Параметры[1], …, Параметры[N]));
    
    Reply
  9. duhh

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

    Вот такой вызов будет безопасным?

    Процедура ВыполнитьПроцедуру(ВходящиеДанные)
    
    ДанныеВыполнения = ВходящиеДанные.Данные;
    
    // Безопасность — проверка имени метода на корректность. Взял из БСП
    Попытка
    Тест = Новый Структура(ДанныеВыполнения.Method, ДанныеВыполнения.Method);
    Если Тест = Неопределено Тогда
    ВызватьИсключение НСтр(«ru = ‘Синтетический тест'»);
    КонецЕсли;
    Исключение
    ВходящиеДанные.Ошибка = Истина;
    ВходящиеДанные.ТекстОтвета = НСтр(«ru=’Некорректное значение параметра ИмяМетода.'»);
    Возврат;
    КонецПопытки;
    
    // Безопасность — проверка параметров на корректность.
    Попытка
    Если Не ТипЗнч(ДанныеВыполнения.Params) = Тип(«Структура») Тогда
    ВызватьИсключение НСтр(«ru = ‘Тест параметров'»);
    КонецЕсли;
    Исключение
    ВходящиеДанные.Ошибка = Истина;
    ВходящиеДанные.ТекстОтвета = НСтр(«ru=’Некорректное значение параметра Params.'»);
    Возврат;
    КонецПопытки;
    
    СтрокаВыполнения = ДанныеВыполнения.Method + «(» + «{Параметры}» + «)»;
    
    Если ДанныеВыполнения.Свойство(«Params») Тогда
    СтрокаВыполнения = СтрЗаменить(СтрокаВыполнения, «{Параметры}», «ДанныеВыполнения.Params»);
    Иначе
    СтрокаВыполнения = СтрЗаменить(СтрокаВыполнения, «{Параметры}», «»);
    КонецЕсли;
    
    Попытка
    Результат = Вычислить(СтрокаВыполнения);
    

    Показать

    Reply
  10. alexqc

    В принципе, для безопасности вызова можно проверить что имя метода точно представляет собой идентификатор (последовательность только букв и цифр).

    Более того, достаточно проверить отсутствие в имени символа «(» — без скобок никакой команды не напишешь.

    А вообще, лично я бы вместо общего модуля делал это в виде макета-словаря: в простейшем виде это будет 1-я колонка — имя метода в rpc, 2-я — полный путь вызова (т.е как ОбщийМодуль.ФункцияМодуля либо например РегистрыНакопления.НужныйРегистр.ФункцияМодуляМенеджера).

    Reply

Leave a Comment

Ваш адрес email не будет опубликован. Обязательные поля помечены *