Введение
Jabber — система для быстрого обмена сообщениями и информацией о присутствии (в контакт-листе) между любыми двумя пользователями Интернета на основе открытого протокола XMPP.
В отличии от той же Аськи Jabber-сеть имеет на мой взгляд более развитые возможности, а наличие расширений протокола открывает горизонты функциональности на недосягаемые для коммерческих IM-сетей, вот некоторые из них:
Открытость: протокол Jabber открыт, общедоступен и достаточно лёгок для понимания; существует множество реализаций серверов и клиентов, а также библиотек с открытым исходным кодом.
Расширяемость: с помощью пространств имён в XML можно расширить протокол Jabber для выполнения требуемых задач и для обеспечения поддержки взаимодействия между различными системами. Общие расширения разрабатываются под контролем Jabber Software Foundation.
Децентрализованность: кто угодно может запустить свой собственный сервер Jabber, что позволяет организациям и частным лицам заниматься любыми экспериментами с IM.
Безопасность: любой сервер Jabber может быть изолирован от общедоступной сети Jabber, многие из вариантов реализации сервера используют SSL при обмене между клиентом и сервером, и немало[источник не указан 39 дней] клиентов поддерживают шифрование с помощью PGP/GPG внутри протокола.
Jabber удовлетворяет многие потребности частных лиц и организаций. Но важно понимать, что он не является универсальным решением всех задач. В частности, Jabber не является:
Универсальным чат-клиентом для различных систем IM — несмотря на множество клиентов Jabber под различные платформы, они не предоставляют таких возможностей по взаимодействию с различными системами IM, которые обеспечиваются программами Miranda IM, Trillian или Pidgin: вместо этого взаимодействие между Jabber и другими системами осуществляют шлюзы, расположенные на стороне сервера.
Универсальным решением проблем взаимодействия с различными IM-системами — некоторые сервера Jabber предоставляют возможность взаимодействия с другими системами IM через шлюзы, которые транслируют протокол Jabber в протокол этих систем; однако только от самих систем зависит осуществление взаимодействия (к чему они подчас не стремятся, и даже наоборот).
Основные сведения о протоколе XMPP
В основе протокола XMPP (eXtensible Messaging and Presence Protocol) лежит язык XML. XMPP является открытым, свободным протоколом для мгновенного обмена сообщениями и информацией о присутствии в режиме околореального времени.
Изначально спроектированный легко расширяемым протокол помимо передачи текстовых сообщений поддерживает передачу голоса и файлов по сети.
Данный протокол принят как стандарт RFC.
Стандартный порт для Jabber-клиентов — 5222.
Протокол регламентируется следующими стандартами:
RFC 3920 — Extensible Messaging and Presence Protocol (XMPP): Core
RFC 3921 — Extensible Messaging and Presence Protocol (XMPP): Instant Messaging and Presence
Следует также отметить, так как протокол является текстовым, а не бинарным соответственно у этого протокола есть слабые стороны, а именно: избыточность передаваемой информации, отсутствие возможности передачи двоичных данных приводит к использованию различных способов перекодировки. В результате этого, для передачи файлов приходится использовать дополнительные протоколы, например HTTP. Если этого не избежать, то XMPP обеспечивает встроенную передачу файлов кодируя информацию используя base64. Другая двоичная информация, такая как закодированный разговор или графические иконки включаются с использованием такого же метода. Однако прежде чем двигаться дальше рассмотрим адресацию пользователей с Jabber-сетях.
Адресация пользователей в Jabber
Каждый пользователь в сети имеет уникальный идентификатор, адрес — Jabber ID (сокращённо JID). Во избежание необходимости существования сервера с полным списком всех адресов, JID подобно адресу электронной почты содержит имя пользователя (JID node) и DNS-адрес сервера (JID domain), на котором зарегистрирован пользователь, разделённые знаком (@). Например, пользователь user, зарегистрированный на сервере example.com, будет иметь следующий адрес (JID): user@example.com.
Также пользователь может подключаться, находясь в разных местах, сервер позволяет определять дополнительное значение, называемое ресурсом, который идентифицирует клиента пользователя в данный момент. Так можно включить в адрес пользователя (JID) имя его ресурса (JID resource), добавив через слэш в конце адреса.
К примеру, пусть полный адрес пользователя будет user@example.com/work, тогда сообщения, посланные на адрес user@example.com, дойдут на указанный адрес вне зависимости от имени ресурса, но сообщения для user@example.com/work дойдут на указанный адрес только при соответствующем подключённом ресурсе.
Адреса (JID) могут также использоваться без явного указания имени пользователя (с указанием имени ресурса или без такового) для системных сообщений и для контроля специальных возможностей на сервере.
Запомним эту информацию, она нам пригодятся в дальнейшем.
Структура XML-пакетов Jabber протокола (XML Streams)
Структура XML пакетов получаемых с сервера и передаваемых на него по спецификации RFC 3920 имеет следующий вид:
|--------------------|
| <stream> |
|--------------------|
| <presence> |
| <show/> |
| </presence> |
|--------------------|
| <message to='foo'> |
| <body/> |
| </message> |
|--------------------|
| <iq to='bar'> |
| <query/> |
| </iq> |
|--------------------|
| ... |
|--------------------|
| </stream> |
|--------------------|
Как вы видите, на схеме представлена иерархическая структура XML подразделенная на следующие элементы, так называемый поток XML и элементы строф XML.
Поток XML — является контейнером для хранения элементов строф XML. Поток XML начинается с открытия тэга <STREAM> (с соответствующими атрибутами и пространством имен), конец потока XML заканчивается закрытием тега </STREAM>. Во время обмена с сервером, клиент и сам сервер может посылать неограниченное количество элементов строф в потоке XML.
Строфы XML — это дискретные семантические модули представленные элементами, заключенными в потоке XML. Строфы XML являются дочерними элементами (child node) корня XML <STREAM>. Начало любой строфы XML обозначено началом элемента (например, <PRESENCE>), конец строфы XML обозначен завершающим тегом (</PRESENCE>). В примере строфы XML: <PRESENCE>, <MESSAGE>, <IQ>. Каждая строфа XML представляет собой конкретную информацию, так например строфа <MESSAGE> представляет сообщение, а <IQ> информационный запрос. Более подробно строфы будут рассмотрены далее.
Примечание: несмотря на стандарт, мной было замечено, что с некоторых серверов могут приходить пакеты, просто содержащие строфы XML, но включенные в поток XML.
Атрибуты элементов XML
При приходе XML у тегов могут быть следующие основные атрибуты:
to — кому (JID).
From — откуда (JID).
Id — уникальный идентификатор, так называемый атрибут 'системы обнаружения атак'. Позволяет конкретно идентифицировать полученные данные. Рекомендовано делать его случайным. Но в принципе это не обязательно.
xml:lang — текущий язык, кодировка данных.
Version — версия.
Теги могут включать также и дополнительные атрибуты, зависящие от передоваемых данных.
Пример строфы <PRESENCE> с некоторыми атрибутами Вы можете увидеть ниже:
<presence from='delphi-test@jabber.ru/основная'
to='delphi-test@jabber.ru/резервная'>
<show/>
</presence>
Пространства имен XML
Так как первоначально XMPP был задуман, как протокол, поддерживающий расширения, перед разработчиками встал вопрос, как можно реализовать данные расширения, не внося коррективы в основной протокол. И решение нашлось. Это решение — пространство имен, довольно известное в XML.
Пространство имён в XML — именованная совокупность имён элементов и атрибутов, служащая для обеспечения их уникальности в XML-документе. Все имена элементов в пределах пространства имён должны быть уникальны. Таким образом, реализуется различение одинаковых элементов XML или атрибутов. Для клиентов Jabber зарезервировано пространство имен "jabber:client"
Пространства имён объявляются с помощью зарезервированного XML атрибута xmlns, значение которого является названием пространства имен.
Например, элемент <QUERY> описанный пространством имен 'jabber:iq:roster' выглядит так:
<query xmlns='jabber:iq:roster'>
Подготовка
Сразу оговорюсь, что я не ставлю перед собой задачу написать полноценно работающий клиент соответствующий полному стандарту XMPP. Слишком большой труд, скажем так, однако основные методы работы с XMPP будут включены в мой исходный компонент.
В качестве основы для работы клиента мной были взяты наработки по работе с WinSock Alex-а Demchenko, используемые им в TICQClient, немного портированные, кое-где измененные и дополнительно комментированные мной, для нашего демо-клиента.
В качестве парсера XML мной был взят TjanXMLParser2, благо он бесплатный, довольно быстрый. Стандартный парсер MSXML был мной отброшен по причине, того, что некоторые XML-пакеты приходили синтаксически неправильные, что начисто отрубало желание этого парсера работать с ними.
Что касается приведенных далее листингов обмена протоколом, я постарался описать самые интересные части, если у вас кое-где возникнут вопросы, подробнее вы можете узнать в RFC. Все 800 основного RFC страниц я не смогу Вам подробно изложить, но критические места постараюсь.
Также сразу оговорюсь, что наш пример не будет поддерживать шифрование, то есть данные будут передаваться в открытом виде. Сделано это для упрощения понимания примера. Вышло, то, что вышло, а хорошо иль плохо получилось судить Вам, уважаемые коллеги.
Итак, для тестирования нашего примера, мной был зарегистрирован на сайте jabber.ru аккаунт delphi-test@jabber.ru с паролем delphi-test. Эти данные нам понадобятся для разбора протокола обмена между сервером jabber (далее — Сервер) и нашим клиентом (также — Клиент) далее.
Прохождение аутенфикации
Итак, первым действием при соединении с сервером Jabber, которым должен выполнить наш клиент — является аутенфикация. Аутенфикация будет происходить используя механизм SASL аутенфикации, описанный в в "RFC 2831 — Using Digest Authentication as a SASL Mechanism", алгоритм работы который будет рассмотрен подробнее, чуть далее.
Итак, мы установили физическое соединение с сервером, теперь нам нужно пройти аутенфикацию, для этого клиент посылает серверу следующий пакет:
<?xml version='1.0' encoding='UTF-8'?>
<stream:stream to='jabber.ru' xmlns='jabber:client'
xmlns:stream='http://etherx.jabber.org/streams' xml:l='ru' version='1.0'>
В ответ сервер пришлет подтверждение, о рукопожатии:
<?xml version='1.0'?>
<stream:stream xmlns='jabber:client'
xmlns:stream='http://etherx.jabber.org/streams'
id='3966489307'
from='jabber.ru'
version='1.0'
xml:lang='en'>
Сразу же после приема первого пакета, придет пакет, содержащий информацию о возможностях и доступных механизмах сервера. Данные возможности нужны, будут для полноценной работы с сервером:
<stream:features><starttls xmlns='urn:ietf:params:xml:ns:xmpp-tls'/>
<compression xmlns='http://jabber.org/features/compress'>
<method>zlib</method>
</compression>
<mechanisms xmlns='urn:ietf:params:xml:ns:xmpp-sasl'>
<mechanism>DIGEST-MD5</mechanism>
<mechanism>PLAIN</mechanism></mechanisms>
<register xmlns='http://jabber.org/features/iq-register'/>
</stream:features>
Что мы видим в пакете, видим, что сервер поддерживает zip компрессию при передаче пакетов, поддерживает механизм аутенфикации DIGEST-MD5, и другие возможности. Стоит также отметить, что возможности сервера зависят от самого сервера и в зависимости от программы могут изменяться. Подробнее вы можете узнать в RFC 3920. Однако нас интересует то, что сервер поддерживает механизм аутенфикации DIGEST-MD5. Отлично, скажем мы и отправим ему пакет, говорящий о том, что мы хотим пройти аутенфикацию используя механизм DIGEST-MD5.
<auth xmlns='urn:ietf:params:xml:ns:xmpp-sasl' mechanism='DIGEST-MD5'/>
После получения данного пакета сервер присылает нам, так называемый challenge-пакет:
<challenge xmlns='urn:ietf:params:xml:ns:xmpp-sasl'>
bm9uY2U9IjIyNjQ3NzQ4Iixxb3A9ImF1dGgiLGNoYXJzZXQ9dXRmLTgsYWxnb3JpdGhtPW1kNS1zZXNz
</challenge>
Данный пакет мы должны будем разобрать. Как это сделать? Ранее внимательный читатель обратил внимание, что некоторые пакеты могут передаваться в кодировке Base64. Это наш случай. Текстовый элемент содержит информацию в данной кодировке, которая после раскодирования примет следующий вид:
nonce="22647748",qop="auth",charset=utf-8,algorithm=md5-sess
Из этой строки нам понадобится значение Nonce для последующего построения ответа серверу, после чего мы подготавливаем строку ответа, которую мы передадим на сервер в ответном пакете, предварительно закодировав ее в Base64. Итак, ответная строка будет иметь следующий вид:
username="delphi-test",
realm="jabber.ru",
nonce="22647748",
cnonce="2313e069649daa0ca2b76363525059ebd",
nc=00000001,
qop=auth,
digest-uri="xmpp/jabber.ru"
,charset=utf-8,
response=16351f86cc5591312e20b4ccd880eadb
где:
username — JID-node пользователя
realm — JID-domain пользователя
nonce — Уникальный код сессии, присланный нам ранее сервером
cnonce — Уникальный код ответной клиентской сессии, сгенерированный клиентом
nc — Так называемый once count — сколько раз был использован текущий nonce. Обычно значение параметра равно 00000001, его и будем использовать. На самом деле параметр довольно интересный и стоит отдельного рассмотрения и изучения в RFC, но как показала практика его смело можно игнорировать.
digest-uri — Протокол подключения, для XMPP сервера он состоит из соединения строк "xmpp/" + JID Domain
charset — поддержка кодировки пароля и имени, в нашем случае UTF-8
И самый важный параметр response в котором заключен ключ ответа серверу, включающий в себя пароль и ответные данные в формате MD5 строящийся по определенному алгоритму.
Алгоритм построения строки ответа и параметра Response более подробно мы рассмотрим далее в подразделе "RFC 2831 использование MD5-Digest аутенфикации в SASL". Пока примем к сведению, что текущее и следующие два действие относится уже к данному алгоритму.
Итак, строку ответа, мы сформировали, закодировали в Base64 и отправляем обратно серверу (всё это должно быть в одну строчку, но, чтобы страница не расползалась, разбито на несколько):
<response xmlns='urn:ietf:params:xml:ns:xmpp-sasl'>
dXNlcm5hbWU9ImRlbHBoaS10ZXN0IixyZWFsbT0iamFiYmVyLnJ1Iixub25jZT0iMjI2ND
c3NDgiLGNub25jZT0iMjMxM2UwNjk2NDlkYWEwY2EyYjc2MzYzNTI1MDU5ZWJkIixu
Yz0wMDAwMDAwMSxxb3A9YXV0aCxkaWdlc3QtdXJpPSJ4bXBwL2phYmJlci5ydSIsY2
hhcnNldD11dGYtOCxyZXNwb25zZT0xNjM1MWY4NmNjNTU5MTMxMmUyMGI0Y2Nk
ODgwZWFkYg==
</response>
Если все нормально мы получим следующий ответ:
<challenge xmlns='urn:ietf:params:xml:ns:xmpp-sasl'>
cnNwYXV0aD1lOTg5NjZjZjUxNjliZWUzOTYzNGU5Zjk5ZTIzZDZhYg==
</challenge>
Тут нам особо ничего не нужно, подтверждаем принятие его, отправив со стороны клиента:
<response xmlns='urn:ietf:params:xml:ns:xmpp-sasl'/>
И если все прошло успешно, то получаем со стороны сервера пакет, говорящий нам о том, что аутенфикация прошла успешно:
<success xmlns='urn:ietf:params:xml:ns:xmpp-sasl'/>
Далее мы снова посылаем пакет рукопожатия:
<?xml version='1.0' encoding='UTF-8'?>
<stream:stream to='jabber.ru'
xmlns='jabber:client'
xmlns:stream='http://etherx.jabber.org/streams'
xml:l='ru' version='1.0'>
Получаем ответ:
<?xml version='1.0'?>
<stream:stream xmlns='jabber:client'
xmlns:stream='http://etherx.jabber.org/streams'
id='4096919146'
from='jabber.ru'
version='1.0'
xml:lang='en'>
После чего по стандарту мы должны связать нашего клиента с JID-ресурсом, что мы и делаем, посылая строку в формате UTF-8:
<iq type='set' id='bund_2'>
<bind xmlns='urn:ietf:params:xml:ns:xmpp-bind'>
<resource>тестовая</resource>
</bind>
</iq>
Примечание: Все листинги будут представлены в ASCII формате, хотя на самом деле прием и посылка пакетов ведется в UTF-8. Однако что бы Вам не читать крякозаблы в листингах примеров, кодировка будет в показана в ASCII.
Сервер подтверждает связывание ресурса с данным клиентом:
<iq id='bund_2' type='result'>
<bind xmlns='urn:ietf:params:xml:ns:xmpp-bind'>
<jid>delphi-test@jabber.ru/тестовая</jid>
</bind>
</iq>
Клиент посылает пакет присутствия в сети
<presence><show></show></presence>
и все, аутенфикация пройдена, значок клиента в контакт-листе становится зелененьким, теперь он может посылать и принимать сообщения.
RFC 2831 использование MD5-Digest аутенфикации в SASL
Итак, аутенфикация решает следующие задачи: Передача пароля на сервер, в закрытом виде, защиту от повторяющихся атак (monitoring nc value), защиту (monitoring nonce) в определённый промежуток времени от определённого клиента. Для того, что бы понять, как работает данный стандарт, разберем основы SASL.
Общие принципы работы SASL
Метод SASL (Simple Authentication and Security Layer) используется для добавления поддержки аутентификации в различные протоколы соединения. Для аутентификации могут быть использованы различные механизмы.
Имя требуемого механизма задаётся клиентом в команде аутентификации. Если сервер поддерживает указанный механизм, он посылает клиенту последовательность окликов (challenges), на которые клиент посылает ответы (responses), чтобы удостоверить себя. Содержимое окликов и ответов определяется используемым механизмом и может представлять собой двоичные последовательности произвольной длины. Кодировка последовательностей определяется прикладным протоколом. Вместо очередного оклика сервер может послать подтверждение аутентификации или отказ. Кодировка также определяется протоколом. Вместо ответа клиент может послать отказ от аутентификации. Кодировка опять определяется протоколом. В результате обменов откликам и ответами должна произойти аутентификация (или отказ), передача идентификатора клиента (пустой идентификатор влечёт получение идентификатора из аутентификации) серверу и, возможно, договорённость об использовании безопасного транспортного протокола (security layer), включая максимальный размер буфера шифрования.
Идентификатор клиента может отличаться от идентификатора, определяемого из аутентификации, для обеспечения возможности работы прокси.
Реализация на примере механизма MD5-Digest
Схема работы SASL для нашего клиента основана на использовании механизма MD-Digest и имеет следующий алгоритм работы:
Сервер посылает случайную строку nonce, наличие поддержки utf-8 в параметре charset для имени и пароля, алгоритм аутентификации (обязательно md5-sess) в параметре algorithm.
То есть те данные, что мы раскодировали ранее из пакета challenge:
nonce="22647748",qop="auth",charset=utf-8,algorithm=md5-sess
Клиент отвечает строкой, содержащей: идентификатор клиента username, идентификатор домена realm, полученную от сервера случайную строку nonce, случайную строку клиента cnonce, номер запроса (позволяет серверу заметить попытку replay attack) nc. параметр digest-uri (сочетание имени сервиса, имени сервера т.е. 'xmpp/' + JID Domain), строку responce подтверждающею знание пароля и ответ на оклик (MD5 от имени пользователя, realm, пароля, случайной строки сервера, случайной строки клиента, идентификатора клиента, номера запроса, уровня защиты, digest-uri; некоторые компоненты берутся в виде MD5, некоторые в исходном виде, некоторые в обоих видах), использование utf-8 для имени и пароля, принятый алгоритм шифрования и идентификатор клиента.
То есть, как вы догадались эта та строка, которую мы формируем в ответ:
username="delphi-test",
realm="jabber.ru",
nonce="22647748",
cnonce="2313e069649daa0ca2b76363525059ebd",
nc=00000001,
qop=auth,
digest-uri="xmpp/jabber.ru",
charset=utf-8,
response=16351f86cc5591312e20b4ccd880eadb
Сервер проверяет ответ на оклик и посылает ответ на ответ в похожем формате (но всё же отличающемся, чтобы клиент мог убедиться в подлинности сервера). Данный механизм слабее системы с открытыми ключами, но лучше простой CRAM-MD5.
Примечание: Стоит отметить, что может предусматриваться упрощённый протокол повторной аутентификации (начинается сразу с посылки клиентом ответа с увеличенным на 1 номером запроса).
Алгоритм вычисления строки ответа response
Алгоритм вычисления строки ответа response имеет следующую формулу:
response-value =
HEX( KD ( HEX(H(A1)),
{ nonce-value, ":" nc-value, ":",
cnonce-value, ":", qop-value, ":", HEX(H(A2)) }))
A1 = { H( { username-value, ":", realm-value, ":", passwd } ),
":", nonce-value, ":", cnonce-value }
A2 = { "AUTHENTICATE:", digest-uri-value }
Где:
Выражение { a, b, ... } — означает сложение строк a, b
HEX(n) — 16-байтовый MD5-хеш n, приведенный в 32 байтовую Hex-строку в нижнем регистре. Фактически строковое представление дайджеста MD5.
H(s) — 16-байтовый MD5-хеш строки s
KD(k, s) — объединение данных (строк) k, s
H({k, ":", s}) — 16-байтовый MD5-хеш, полученный в результате сложения строки k, ":", S
Как видите, особо ничего сложного нет. Вот алгоритм расчета реализованный мной на Delphi:
function GenResponse(UserName, realm, digest_uri, Pass, nonce, cnonce : String) : string;
const
nc = '00000001';
gop = 'auth';
var
A2, HA1, HA2,
sJID : String;
Razdel : Byte;
Context : TMD5Context;
DigestJID : TMD5Digest;
DigestHA1 : TMD5Digest;
DigestHA2 : TMD5Digest;
DigestResponse : TMD5Digest;
begin
Razdel := Ord(':');
// ВЫЧИСЛЯЕМ А1 по формуле RFC 2831
// A1 = { H( { username-value, ":", realm-value, ":", passwd } ),
// ":", nonce-value, ":", cnonce-value, ":", authzid-value }
sJID := format('%S:%S:%S', [username, realm, Pass]);
MD5Init(Context);
MD5UpdateBuffer(Context, PByteArray(@sJID[1]) , Length(sJID));
MD5Final(DigestJID, Context);
MD5Init(Context);
MD5UpdateBuffer(Context, PByteArray(@DigestJID),SizeOf(TMD5Digest));
MD5UpdateBuffer(Context, @Razdel , SizeOf(Razdel));
MD5UpdateBuffer(Context, PByteArray(@nonce[1]) , Length(nonce));
MD5UpdateBuffer(Context, @Razdel , SizeOf(Razdel));
MD5UpdateBuffer(Context, PByteArray(@cnonce[1]) , Length(cnonce));
MD5Final(DigestHA1, Context);
// ВЫЧИСЛЯЕМ А2 по формуле RFC 2831
// A2 = { "AUTHENTICATE:", digest-uri-value }
A2 := format('AUTHENTICATE:%S', [digest_uri]);
MD5Init(Context);
MD5UpdateBuffer(Context, PByteArray(@A2[1]) , Length(A2));
MD5Final(DigestHA2, Context);
// ВЫЧИСЛЯЕМ RESPONSE по формуле RFC 2831
// HEX( KD ( HEX(H(A1)),
// { nonce-value, ":" nc-value, ":",
// cnonce-value, ":", qop-value, ":", HEX(H(A2)) }))
HA1 := LowerCase( PacketToHex(@DigestHA1, SizeOf(TMD5Digest)));
HA2 := LowerCase( PacketToHex(@DigestHA2, SizeOf(TMD5Digest)));
MD5Init(Context);
MD5UpdateBuffer(Context, PByteArray(@HA1[1]),Length(HA1));
MD5UpdateBuffer(Context, @Razdel , SizeOf(Razdel));
MD5UpdateBuffer(Context, PByteArray(@nonce[1]) , Length(nonce));
MD5UpdateBuffer(Context, @Razdel , SizeOf(Razdel));
MD5UpdateBuffer(Context, PByteArray(@nc[1]) , Length(nc));
MD5UpdateBuffer(Context, @Razdel , SizeOf(Razdel));
MD5UpdateBuffer(Context, PByteArray(@cnonce[1]) , Length(cnonce));
MD5UpdateBuffer(Context, @Razdel , SizeOf(Razdel));
MD5UpdateBuffer(Context, PByteArray(@gop[1]) , Length(gop));
MD5UpdateBuffer(Context, @Razdel , SizeOf(Razdel));
MD5UpdateBuffer(Context, PByteArray(@HA2[1]),Length(HA2));
MD5Final(DigestResponse, Context);
Result := LowerCase( PacketToHex(@DigestResponse, SizeOf(TMD5Digest)) )
end;
На входе функция получает параметры рассмотренные нами ранее.
Базовые семантические модули
После того как мы прошли авторизацию, разберем основные базовые семантические модули XML-строф реализованных в протоколе, их довольно немного:
<Presence> — презентационные данные, определяют статусное состояние, видимость пользователей и управление подпиской.
<Message> — собственно сами сообщения переданные или принятые пользователем.
<IQ> — Info Query, данные информационных запросов, включают в себя сами запросы, а также результаты выполнения. Данные запросы позволяют Jabber-клиентам обменивается различными данными между собой. Информационное наполнение запроса и ответа определено в пространстве имен дочернего элемента. Дополнительные расширения протокола (XEP — XMPP Extension Protocol) очень сильно используют <iq> запросы. Подробнее об них я расскажу далее.
Отправка и прием сообщений <Message>
Прием и отправка сообщений осуществляется через XML-строфу <MESSAGE>. Так как в Jabber-е предусмотрены разные типы сообщений, то для их разграничений предусмотрен атрибут type содержащий информацию о типе сообщения.
Типы сообщений могут быть следующие:
chat — Одиночное сообщение от клиента к клиенту.
error — Сообщение об ошибке. Произошедшая ошибка связанна с предыдущим, посланным одиночным сообщением.
groupchat — Групповой чат. Данное сообщение пришло с группового чата, действующего по признаку "Одно сообщение — многим получателям".
headline — Системное сообщение, автоматически генерируется различными сервисами для шировещательной рассылки (новости, спорт, RSS-каналы и пр.) Отвечать на такие сообщение не нужно, да и не зачем.
normal — одиночное сообщение, посылаемое вне контекста взаимно-однозначного сеанса связи или группового чата. То есть это такое сообщение, на которое пользователь может дать ответ, не учитывая хронологии сеанса связи.
Дочерние элементы
У XML-строфы <Message> могут быть следующие дочерние элементы, определенные пространством имен 'jabber:client' или 'jabber:server'. По умолчанию за этими элементами зарезервировано пространство имен 'jabber:client'. Если <Message> имеет тип error то обработка такого сообщения идет в соответствии с RFC 3920 XMPP-Core.
Элемент строфы <Message> может содержать любой из следующих дочерних элементов без явного объявления пространства имен:
<subject/>, <body/>, <thread/>.
Где элемент <body> — является телом сообщения. В значении элемента содержится сам текст сообщения, предварительно перекодированный из-за ограничений XML. Элемент может содержать атрибут 'xml:lang' содержащий язык сообщения. В <Message> могут быть включены множественные экземпляры элементов <Body>, но только при условии, что каждый экземпляр обладает атрибутом 'xml:lang' с отличным языковым значением.
Например:
<message type="chat" to="получатель"
id="идентификатор"
from="отправитель">
<body>Текст (тело) сообщения</body>
</message>
Элемент <Subject> определяет тему сообщения. На него действуют те-же правила, что и для <Body>. Множественные экземпляры <Body> могут быть включены для расширения и дополнения смежных тем, но при условии, что каждый экземпляр обладает атрибутом 'xml:lang' с отличным языковым значением.
Элемент <thread> cлужит для обеспечения хронологии сеанса. Значение <thread> элемента сгенерированного отправителем должно быть послано назад в любых ответах. Этот элемент является дополнительным и обычно не используется для обмена сообщениями между пользователями. Используется он в сеансах связи. Более подробно можно прочитать о нем в RFC 3921.
Перекодировка символов текста
Поскольку символы "<" и ">" используются для обозначения самих XML тегов, то их вставка в текст сообщения недопустима (за исключением случая, когда вставлен символ ">", но никакой тег не был открыт). Поэтому для корректного формирования XML следующие символы должны быть заменены в теле сообщения при отправке оного и соответственно обратно возвращены при приеме:
"<" в "<"
">" в ">".
Таким образом, чтобы написать "2>1", нужно написать "2>1". То же самое касается и знака "&" — он заменяется "&". Также рекомендуется заменять и кавычки (хотя в большинстве случаев они хорошо распознаются и без этого). Эквивалент двойных кавычек — """
Статусы, состояния, информация о присутствии, управление подпиской <Presence>
Прием и отправка статусных сообщений, а также информации о видимости контактов и подписки на сообщения от них, осуществляется через XML-строфу <Presence>.
Атрибут type строфы <Presence> является дополнительным.
Строфа, которая не обладает атрибутом type, используется Jabber-ом, для сообщений о присутствии контакта в сети Jabber и указывает на то, что данный контакт находится в сети (онлайне) и доступен для коммуникации.
Если атрибут type присутствет в строфе <Presence>, то он управляет подпиской на сообщения и смену статусов другого контакта (объекта). Аналог подписки в IM-сетях является прохождение авторизации в ICQ.
Если атрибут включен, то он должен содержать иметь одно из следующих значений:,
unavailable — Сигнализирует, что данный контакт, больше не доступен для коммуникаций. Фактически контакт вышел в оффлайн.
subscribe — Запрос на подписку (авторизацию) от другого контакта.
subscribed — Информирует о том, что контакт разрешил авторизацию.
unsubscribe — Отправитель аннулирует подписку.
unsubscribed — Запрос на аннулирование подписки (отозвание авторизации) от другого контакта.
probe — Запрос о текущем присутствии контакта только сервером от имени пользователя.
error — Ошибка, произошедшая при доставки предыдущих данных. Обработка такого сообщения идет в соответствии с RFC 3920 XMPP-Core.
Например, запрос на подписку от контакта ivanov@jabber.ru для нашего контакта может выглядеть так:
<presence to="delphi-test@jabber.ru" type="subscribe"
from="ivanov@jabber.ru"/>
</presence>
Разрешение авторизации в ответ :
<presence to="ivanov@jabber.ru" type="subscribed"/>
А запрет (отказ) вот так:
<presence to="ivanov@jabber.ru" type="unsubscribed"/>
Дочерние элементы
XML-строфа <Presence> может содержать следующие дочерние элементы, определенные пространством имен 'jabber:client' или 'jabber:server': <show>, <status>, <priority>. По умолчанию за этими элементами зарезервировано пространство имен 'jabber:client'. Если <Message> имеет тип error то обработка такого сообщения идет в соответствии с RFC 3920 XMPP-Core.
Элемент <show> определяет статус контакта и может иметь следующие значения:
away — Отошел,
chat — Готов чатится (В сети),
dnd — Занят,
xa — Недоступен
Пустой элемент <show/> определяет статус контакта " В сети ".
Элемент <status> определяет статусное сообщение. Значением элемента является строка с текстом сообщения, например:
<presence>
<status>Смотрю фильм</status>
</presence>
Необязательный элемент <priority> определяет приоритет уровня ресурса. Значением элемента является число от -128 до 127.
Информационные запросы <IQ>
Информационные запросы <IQ> разделяются на стандартные, определенные пространством имен 'jabber:client' или 'jabber:server', обеспечивающие базовые функциональные возможности и расширенные.
Расширенные запросы, определенные дополнительными пространствами имен, описаны в различных дополнениях к протоколу XMPP.
Пространство имен расширенных запросов, может содержать любое значение, кроме зарезервированных следующих пространств имен: "jabber:client", "jabber:server" или "http://etherx.jabber.org/streams". Такое расширение позволяет придать протоколу XMPP дополнительную функциональность и гибкость. Таким образом, расширенный информационный запрос <IQ> может содержать один или более дополнительных дочерних элементов, определяющих информационное наполнение, которое расширяет значение данного запроса. Это кардинальное отличие от стандартного информационного запроса.
Стандартный запрос не может содержать, дочерние элементы, кроме элемента <error>. Наличие данного элемента в запросе показывает наличие ошибки.
Поддержка любого расширенного пространства имен является дополнительной возможностью со стороны клиента. Если клиент не понимает такое пространство имен, то есть фактически не поддерживает данное расширение, то он должен проигнорировать данный пакет. Более подробно вы можете прочитать об этом в RFC 3921.
Структурная схема обмена информационными запросами:
Запрос Ответ
---------- ----------
| |
| <iq type='get' id='1'> |
| ------------------------> |
| |
| <iq type='result' id='1'> |
| <------------------------ |
| |
| <iq type='set' id='2'> |
| ------------------------> |
| |
| <iq type='error' id='2'> |
| <------------------------ |
| |
Как мы видим, на структурной схеме обмен между клиентами происходит по такому алгоритму. Запрашивающая сторона посылает <IQ> запрос с атрибутом type равным значению "get". Данный атрибут на принимающей стороне говорит клиенту, что вы должны предоставить информацию по данному запросу (для расширенных запросов при условии, что он поддерживается клиентом). Принимающая сторона отправляет ответ с атрибутом с атрибутом type равным значению "result".
Это первый вариант обмена. Существует и второй, когда запрашивающая сторона информирует принимающую о каком-то изменении, для этого она отправляет запрос с атрибутом type равным значению "set". Данное значение атрибута говорит о том, что принимающая сторона должна обработать присланные данные. Если принимающая сторона не может по каким-либо причинам обработать присланные данные, то в ответ она посылает строфу <IQ> с атрибутом type равным значению "error" информируя запрашивающую сторону о невозможности обработки. Если принимающая сторона корректно обработала запрос c атрибутом "set" то она возвращает ответ с атрибутом равным значению "result".
Пример расширенного запроса определяющий информацию об использованном клиенте (XEP-0092 Software Version):
Запрос:
<iq from='delphi-test2@jabber.ru/QIP'
to='delphi-test@jabber.ru/тестовая'
xml:lang='ru' type='get' id='qip_30'>
<query xmlns='jabber:iq:version'/>
</iq>
Ответ:
<iq type='result' to='delphi-test2@jabber.ru/QIP'
from='delphi-test@jabber.ru/тестовая'
id='qip_30'>
<query xmlns='jabber:iq:version'>
<name>Мой клиент</name>
<version>0.5.0.1</version>
</query>
</iq>
Работа с ростер-листом (списком контактов)
Ростер-лист или аналог списка контактов в сетях ICQ в Jabber-е представлен списком, содержащим JID-контакты в виде элементов XML хранящимся на сервере от имени пользователя. Так как ростер-лист сохранен сервером от имени пользователя, то пользователь может обратиться к информации списка от любого ресурса.
Управление ростер-листом (списком) осуществляется через расширенный информационный запрос <IQ> содержащий дочерний элемент <query> c пространством имен 'jabber:iq:roster'. Элемент <query> может содержать один или более дочерних элементов <ITEM> содержащих информацию о контакте.
Уникальный идентификатор каждого элемента списка <item> — это JID контанта, формируемый в атрибуте jid Значение атрибута jid имеет форму user@domain без указания ресурса. Текущее состояние подписки пользователя (контакта) относительно элемента <item> зафиксировано в атрибуте subscription и может принимать следующие значения:
none — У пользователя нет подписки к контакту, нет подписки и к информации присутствия пользователя
to — у пользователя есть подписка к информации присутствия контакта, но у контакта нет подписки к информации присутствия пользователя
from — у контакта есть подписка к информации присутствия пользователя, но у пользователя нет подписки к информации присутствия контакта
both — у пользователя есть подписка к присутствию контакта, да и у контакта есть подписка к пользователю.
Запрос списка контактов при входе в систему
При входе в систему клиент Jabber должен послать серверу информационный запрос о получении ростер-листа.
Запрос ростер-листа клиентом:
<iq from='delphi-test@jabber.ru/тестовая'
type='get' id='roster_1'>
<query xmlns='jabber:iq:roster'/>
</iq>
Получение ростер-листа с сервера:
<iq from='delphi-test@jabber.ru'
to='delphi-test@jabber.ru/тестовая' id='roster_1'
type='result'>
<query xmlns='jabber:iq:roster'>
<item subscription='from'
name='Тест 2'
jid='delphi-test2@jabber.ru'/>
</query>
</iq>
Управление ростер-листом
Добавление или редактирование контакта. При отсутствии контакта в ростер-листе контакт будет добавлен, при наличии отредактирован.
Добавление / корректировка. Клиент посылает следующий пакет.
<iq from='delphi-test@jabber.ru/тестовая'
type='set' id='уникальный номер'>
<query xmlns='jabber:iq:roster'>
<item jid='новый/корректируемый JID'
name='Имя контакта'>
<group>Группа контакта</group>
</item>
</query>
</iq>
После добавления/обновления информации о контакте на сервере, сервер оповещает все доступные ресурсы пользователя о внесенной информации. Оповещение служит сигналом для синхронизации данных клиентов о данном контакте с данными сервера.
Оповещение сервера:
<iq to='delphi-test@jabber.ru/тестовая'
type='set'
id='уникальный номер'>
<query xmlns='jabber:iq:roster'>
<item jid='новый/корректируемый JID'
name='Имя контакта'>
<group>Группа контакта</group>
</item>
</query>
</iq>
Информация о результате:
<iq to='delphi-test@jabber.ru/тестовая' type='result' id='уникальный номер'/>
В любое время, пользователь удалить контакт из ростер-списка, для этого клиент должен послать запрос с атрибутом subscription элемента <item> равным значению 'remove':
<iq from=' delphi-test@jabber.ru/тестовая' type='set' id='roster_4'>
<query xmlns='jabber:iq:roster'>
<item jid='JID который удаляется' subscription='remove'/>
</query>
</iq>
Как и в случае с добавлением/корректировкой контакта сервер оповещает клиенты о удалении контакта. Указанием факта удаления служит атрибут subscription равным значению 'remove' в элементе <item>.
<iq to=' delphi-test@jabber.ru/тестовая' type='set' id='roster_4'>
<query xmlns='jabber:iq:roster'>
<item jid='JID который удален сервером' subscription='remove'/>
</query>
</iq>
Заключение
Как вы видите, ничего особо сложного нет. Простой Jabber-клиент с минимальной функциональностью представлен в примере. Также в архив выложен парсер TjanXMLParser2, RFC 3920, 3921.
К статье прилагается пример.