XMPP(jabber) на чистом 1С

На сайте infostart, есть несколько реализаций работы 1С с протоколом XMPP (jabber), но в основном они на использование сторонних библиотек (нативных), которые надо регистрировать в операционке (и только в винде), или на основе других систем — php, python и т.п. Предлагаю пример реализации отправки сообщения через этот протокол только средствами 1С.

Предварительный разбор по 1С

  • Поддержка XML у 1С реализована хорошо, а протокол XMPP — это чистый XML.
  • XMPP сервер работает с клиентами через сокеты/потоки, на 1С реализовать это уже сложнее, но работу с сервером XMPP можно расширить через модули для работы по HTTP (BOSH — http-bind).
  • Работа с Base64 — реализован в 1С через команды: Base64Значение и Base64Строка.
  • Расчет хэш-суммы по алгоритму MD5, реализован в 8.3 объектом ХешированиеДанных.

Разбор по XMPP

Простая сессия представляет из себя следующую последовательность операций:

  • Соединение с сервером
  • Создание потока
  • Аутентификация
  • Привязывание (bind) потока к ресурсу (имя@сервер/ресурс)
  • Создание сессии
  • Рассылка статуса «доступен»
  • Отправка/получение сообщений, статусы, ростер(список контактов), «визитные карточки», работа с сервисами и транспортами и т.п.
  • Рассылка статуса «отключен»
  • Закрытие потока
  • Отключение от сервера

В качестве примера (он будет достаточен), реализуем самый минимум и заморачиваться с ростером, закрытием сессий — не будем. Самое последнее действие, которое реализеум — это просто отправка сообщения.  Оговоримся сразу, в примере буду использовать сервер jabber.ru, а ники вымышленные.

1 — Приветствие, получение кода сессии SID:

<body
content='text/xml; charset=utf-8'
from='myNickXMPP@jabber.ru'
secure='true'
hold='1'
rid='678901'
to='jabber.ru'
route='http://jabber.ru/http-bind'
ver='1.10' wait='300'
xml:lang='en' xmpp:version='1.0'
xmlns='http://jabber.org/protocol/httpbind'
xmlns:xmpp='urn:xmpp:xbosh'/>

2 — Отправка серверу команды на авторизацию методом DIGEST-MD5:

<body rid='678902' sid='1234567890' xmlns='http://jabber.org/protocol/httpbind' xmlns:xmpp='urn:xmpp:xbosh'>
<auth xmlns='urn:ietf:params:xml:ns:xmpp-sasl' mechanism='DIGEST-MD5'/>
</body>

3 — Авторизация методом DIGEST-MD5:

<body rid='678903' sid='1234567890' xmlns='http://jabber.org/protocol/httpbind'>
<response xmlns='urn:ietf:params:xml:ns:xmpp-sasl'>
МАССИВ ДАННЫХ АВТОРИЗАЦИИ ЗАКОДИРОВАННЫХ BASE64
</response>
</body>

4 — После успешной авторизации, отправляем встречный ответ подтверждение:

<body rid='678904' sid='1234567890' xmlns='http://jabber.org/protocol/httpbind'>
<response xmlns='urn:ietf:params:xml:ns:xmpp-sasl'/>
</body>

5 — Повторно посылаем приветствие и команду «рестарта»:

<body
rid='678905'
sid='1234567890'
to='jabber.ru'
xml:lang='en'
xmpp:restart='true'
xmlns='http://jabber.org/protocol/httpbind'
xmlns:xmpp='urn:xmpp:xbosh'/>

6 — Устанавливаем имя ресурса (отправим пустой):

<body rid='678906' sid='1234567890' xmlns='http://jabber.org/protocol/httpbind'>
<iq id='bind_1' type='set' xmlns='jabber:client'>
<bind xmlns='urn:ietf:params:xml:ns:xmpp-bind'/>
</iq>
</body>

7 — Затем сервер требовал создать сессию… Это нужно для отсылки статусов, сообщений, да и вообще. Не будем отказывать:

<body rid='678907' sid='1234567890' xmlns='http://jabber.org/protocol/httpbind'>
<iq id='bind_2' type='set' xmlns='jabber:client'>
<session xmlns='urn:ietf:params:xml:ns:xmpp-session' />
</iq>
</body>

8 — Ну и теперь выходим в онлайн и становимся видимыми для ваших контактов:

<body rid='678908' sid='1234567890' xmlns='http://jabber.org/protocol/httpbind'>
<presence>
<show/>
<status>1c input</status>
<priority>10</priority>
</presence>
</body>

Одна из возникших проблем: на данный момент, мне не удалось отправить из 1С статус на русском языке, и таже самая проблема при отправке сообщения.

9 — Отправка сообщения другому пользователю:

<body rid='678909' sid='1234567890' xmlns='http://jabber.org/protocol/httpbind'>
<message to='poluchatel@jabber.ru' from='myNickXMPP@jabber.ru' xmlns='jabber:client' type='chat'>
<body>test message</body>
</message>
</body>

Как видите ничего сложного — нет, рассматривать ответ от сервера мы тут не будем, этого в инете достаточно. Рабочий код(пример) выложен ниже.

Разбор полёта

Обработка соединяется с сервером и отсылает сообщение, но так как это всего-навсего пример и его цель только показать, что ничего невозможного сейчас на 1С — нету, то есть недоработки и корявости(сырости) в реализации.

Хотел бы обратить внимание (на двух) уже на одной проблеме, которой пока не нашел решение(не забываем, что реализация на чистом 1С, без COM). Без решения данной проблемы — пример всёравно работает.

  1. Как сказано выше, не удалось отправить сообщение и статус на кириллице, поэтому отсылаем только на латинице.
  2. Генератор случайных чисел — работает коряво и часто вылетает по ошибке, поэтому вставил уже сгенерированый и закодированный случайны код — oUIOt7lGFY0Q3Z+/1qo0j9lhpB1MscW9Fu4MJM/nNhc=.

Вся сложность взаимодействия с XMPP из 1С, это создания ключа авторизации по особому способу:

Функция строкаРеспонсе(структураДляОтвета)
// функция создания поля response для challenge
СтрокаВозвр="";

authzid = Ложь;
authzid_знач = "";
Для  каждого првСтрк из структураДляОтвета Цикл
Если првСтрк.Ключ = "authzid" Тогда
authzid=Истина;
authzid_знач = првСтрк.Значение;
КонецЕсли;
КонецЦикла;

realm="";
Если НЕ структураДляОтвета.realm="-" Тогда
realm=структураДляОтвета.realm;
КонецЕсли;

раскНЕХ64 = мд5бз64(Объект.Пользователь+":"+realm+":"+Объект.Пароль);
Сообщить(раскНЕХ64);

а1 = ":"+структураДляОтвета.nonce+":"+структураДляОтвета.cnonce;
Если authzid Тогда
а1 = а1+":"+authzid_знач;
КонецЕсли;
а2="AUTHENTICATE:"+структураДляОтвета.digest_uri;

общаяСтрока = ""+мд5(а1,раскНЕХ64)+
":"+ структураДляОтвета.nonce +
":"+ структураДляОтвета.nc +
":"+ структураДляОтвета.cnonce +
":"+ структураДляОтвета.qop +
":"+ мд5(а2);

Сообщить("rD="+общаяСтрока);
СтрокаВозвр = мд5(общаяСтрока);

Возврат СтрокаВозвр;
КонецФункции

Функция СлучайноеЧисло(Мин,Макс,флЦелое = 0)
ГСЧ = Новый ГенераторСлучайныхЧисел();
СлучайноеЧисло = ГСЧ.СлучайноеЧисло(1, 12000);
//вместо Randomize
Для н = 1 По СлучайноеЧисло Цикл
Уник = Новый УникальныйИдентификатор;
КонецЦикла;
//генерируем GUID
Уник = СокрЛП(Новый УникальныйИдентификатор);
//оставляем только цифры
Уник = СтрЗаменить(Уник,"-","");
Уник = СтрЗаменить(Уник,"a","");
Уник = СтрЗаменить(Уник,"b","");
Уник = СтрЗаменить(Уник,"c","");
Уник = СтрЗаменить(Уник,"d","");
Уник = СтрЗаменить(Уник,"e","");
Уник = СтрЗаменить(Уник,"f","");
//знаменатель должен иметь такое же количество нулей + 1
Знаменатель = 10;
Для н = 2 По (СтрДлина(СтрЗаменить(Уник,Символы.НПП,""))) Цикл
Знаменатель = Знаменатель * 10;
КонецЦикла;
Случ = (Число(Уник)) / Знаменатель; //здесь получается дробное случайное число от 0 до 1
//преобразуем его в случайное число из заданного интервала, округляем до целого

ЧислоИзИнтервала = Мин(Макс(Окр(Мин + (Макс-Мин)*Случ),Мин),Макс);

Возврат ЧислоИзИнтервала;
КонецФункции

Функция _2()
//--//--//--
Если узл.ИмяУзла = "challenge" Тогда
неХЭШстрока = расшф64(узл.ТекстовоеСодержимое);
струк = строк_струк(неХЭШстрока);
Если струк.количество()>0 Тогда
// нормально разобрали, теперь добавляем доп.параметры

// 1-добавление к challenge digest-uri если он не пришел в ответе от сервера
digest_uri = Ложь;
Для  каждого првСтрк из струк Цикл
Если првСтрк.Ключ = "digest-uri" Тогда
digest_uri=Истина;
КонецЕсли;
КонецЦикла;
Если НЕ digest_uri Тогда
струк.Вставить("digest_uri", "xmpp/"+Объект.Домен);
КонецЕсли;
// 2-генерация cnonce - уникальный номер сессии
гуид = Новый УникальныйИдентификатор();
уникномерсесс = СтрЗаменить(гуид,"-","");
струк.Вставить("cnonce", зашф64(уникномерсесс));
струк.Вставить("nc", "00000001");
// 3-проверка в challenge -> qop пришел в ответе от сервера и правильный ли он
qop = Ложь;
Для  каждого првСтрк из струк Цикл
Если првСтрк.Ключ = "qop" Тогда
qop=Истина;
КонецЕсли;
КонецЦикла;
Если НЕ qop Тогда
струк.Вставить("qop", "auth");
Иначе
Если НЕ струк.qop = "auth" Тогда
струк.qop = "auth";
КонецЕсли;
КонецЕсли;
// 3-добавление к challenge realm если он не пришел в ответе от сервера
realm = Ложь;
Для  каждого првСтрк из струк Цикл
Если првСтрк.Ключ = "realm" Тогда
realm=Истина;
КонецЕсли;
КонецЦикла;
Если НЕ realm Тогда
струк.Вставить("realm", "-"); // надо учитывать что
КонецЕсли;
КонецЕсли;

Если струк.realm = "-" Тогда
Знач_realm="";
Иначе
Знач_realm=струк.realm;
КонецЕсли;

// 4-создание кодированной строки response на основании некоторых данных из challenge и логина-пароля
респонсеСтр=строкаРеспонсе(струк);

// формируем строку для запроса №3
строкаВозвратаДля3 =
"username="""+Объект.Пользователь+""","+
"realm="""+Знач_realm+""","+
"nonce="""+струк.nonce+""","+
"cnonce="""+струк.cnonce+""","+
"nc="+струк.nc+","+
"qop="+струк.qop+","+
"digest-uri="""+струк.digest_uri+""","+
"response="+респонсеСтр+","+
"charset=utf-8";

Объект.challenge_actv=Истина;
КонецЕсли;
//--//--//--
КонецФункции

P.S. Есть пару идей использования протокола: можно организовать свой домашний сервер ботов на 1С, который может обрабатывать некую информацию и результат обработки отсылать пользователю в том-же xml или json. Например: по заказу сделали отчет или некую конфу, закодировали часть кода и ключ раскодирования отдавать пользователю по его запросу; можно также ограничить по количеству запусков, не более 10 раз и хватит с клиента. Причем, серверную часть своего бота можете ораганизовать на любом языке (не только 1С). Приложить свои усилия по доработке можно на github.

10 Comments

  1. baton_pk

    За находчивость +, теперь о грустном:

    ТестЗапроса = «»<body rid='»»+Формат(Объект.РИД,»»ЧГ=1000000″»)+»»‘ sid='»»+Формат(Объект.СИД,»»ЧГ=1000000″»)+»»‘ xmlns=’http://jabber.org/protocol/httpbind’>
    |<presence><show/><status>1c input</status><priority>10</priority></presence>
    //|<presence><show/><status>1С вход</status><priority>10</priority></presence>
    |</body>»»;
    

    И охота XML рисовать руками? ЗаписьXML или XDTO — это же так удобно.

    Reply
  2. kodnik

    (1) С вами согласен, а так как это пример и его цель — показать возможности 1С, то намеренно оставил XML запросы в текстовом виде для наглядности и простоты запросов XMPP.

    Reply
  3. SerVer1C

    «…не получается нормально сгенерировать 32 символа с кодами от 0 до 255 и потом закодировать данную строку методом Base64, часто происходит вылет 1С по ошибке. Нужна реализация функции СлучайноеЧисло — по другому алгоритму.»

    А в чем сложность? Берешь 2 ГУИДа, склеиваешь, убираешь черточки — получаешь число из 64-х шестнадцатиричных знаков, каждая пара которых представляет собой одно число в диапазоне от 0 до 255.

    Reply
  4. kodnik

    (3) Точно!!! Но два ГУИДа — это много (64символа), хватит и одного 32 символа.

    гуид = Новый УникальныйИдентификатор();
    уникномерсесс = СтрЗаменить(гуид,»-«,»»);
    струк.Вставить(«cnonce», зашф64(уникномерсесс));
    

    Reply
  5. skyadmin

    Не подскажете в чем может быть ошибка?

    В процедуре 3 отправляется запрос

    <body
    rid=’27815′
    sid=’6s0zusd3yz’
    xmlns=’http://jabber.org/protocol/httpbind’>
    <response xmlns=’urn:ietf:params:xml:ns:xmpp-sasl’>dXNlcm5hbWU9ItC60L7QutC60L7QvdC10L3QsNCydkB0cm1zcnYiLH­JlYWxtPSJ0
    cm1zcnYiLG5vbmNlPSJyaFozVjJjOFNIQ0RwM0M4dHNsaURNdDVhRFNuQVhD­eTNY
    cVRkc2VGIixjbm9uY2U9IlpqSmtPR05sWkRCa1pXSm1ORFV3Tm1JMk5EZzBZ­V0Zs
    T1RObE1XVXlaVEk9IixuYz0wMDAwMDAwMSxxb3A9YXV0aCxkaWdlc3QtdXJp­PSJ4
    bXBwLzE5Mi4xNjguMi4yIixyZXNwb25zZT1lYWJiMjk0ZDIyZWYzMTU1ZDY1­NTVi
    Y2Y5MTRiOTI2MSxjaGFyc2V0PXV0Zi04</response>
    </body>

    Показать

    в не зашифрованном виде строка выглядит так

    username=»кокконенавv@trmsrv»,realm=»trmsrv»,nonce=»rhZ3V2c8SHCDp3C8tsliDMt5aDSnAXCy3XqTdseF»,cnonce=»ZjJkOGNlZDBkZWJmNDUwNmI2NDg0YWFlOTNlMWUyZTI=»,nc=00000001,qop=auth,digest-uri=»xmpp/192.168.2.2″,response=eabb294d22ef3155d6555bcf914b9261,charset=utf-8

    ответ от сервера приходит такой

    <body xmlns=’http://jabber.org/protocol/httpbind’ ack=’27815′><failure xmlns=»urn:ietf:params:xml:ns:xmpp-sasl»><incorrect-encoding/></failure></body>
    Reply
  6. Zixxx

    А как входящие сообщения приходят?

    Reply
  7. skyadmin

    (6) Уже не помню, отпала необходимость, после появления в платформе 1С сервиса обсуждений)

    Reply
  8. nytlenc

    (1) Сериализация? Не…. Не слышал.

    Reply
  9. rusanov_aa

    (7) так и не понял — он через глобальный сервак в интернете работает или локально можно развернуть?

    Reply
  10. skyadmin

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

    + Можно читать переписки

    — Ставиться куча служб, которые то и дело приходиться пинать..

    Сервер Jabber у меня стоял локально OpenFire

    В данной обработке есть один минус, если пользователь запустит более одного сеанса с jabber клиентом, оба зависнут намертво

    Reply

Leave a Comment

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