четверг, 16 сентября 2010 г.

Пишем Jabber-клиент на Delphi. Часть 2

Подготовка

Сразу оговорюсь, что я не ставлю перед собой задачу написать полноценно работающий клиент соответствующий полному стандарту XMPP. Слишком большой труд, скажем так, однако основные методы работы с XMPP будут включены в мой исходный компонент.
В качестве основы для работы клиента мной были взяты наработки по работе с WinSock  Alex-а Demchenko (alex@ritlabs.com), используемые им в 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'>dXNlcm5hbWU9ImRlbHBoaS10ZXN0IixyZWFsbT0iamFiYmVyLnJ1Iixub25jZT0iMjI2NDc3NDgiLGNub25jZT0iMjMxM2UwNjk2NDlkYWEwY2EyYjc2MzYzNTI1MDU5ZWJkIixuYz0wMDAwMDAwMSxxb3A9YXV0aCxkaWdlc3QtdXJpPSJ4bXBwL2phYmJlci5ydSIsY2hhcnNldD11dGYtOCxyZXNwb25zZT0xNjM1MWY4NmNjNTU5MTMxMmUyMGI0Y2NkODgwZWFkYg==</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>

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

Комментариев нет:

Отправить комментарий