Русские документы
Ежедневные компьютерные новости RSS rusdoc.ru  Найти :
http://www.rusdoc.ru. Версия для печати.

Jabber и PHP

Раздел: Programming / PHP @ 03.05.2010 | Ключевые слова: xmpp протокол jabber

Автор: WeBi

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


Jabber и XMPP это один и тот же протокол.
XMPP - современное название протокола.
Jabber - старое название.

Протокол XMPP открыт и обмен информацией идет с помощью XML.
Именно из-за XML этот протокол имеет большой минус - избыточность в трафике. На фоне коротких сообщений эта избыточность выглядит просто огромной.
На примерах это будет видно.

Для начала расскажу теорию работы протокла jabber на примерах.
Вот пример соединения с яндексом (Я.Онлайн).

Сначала соединяемся с хостом xmpp.yandex.ru по порту 5222.
После установления соединения посылаем следующий XML

<?xml version="1.0"?>
<stream:stream xmlns:stream="http://www.rusdoc.ru/go.php?http://etherx.jabber.org/streams" version="1.0" xmlns="jabber:client" to="ya.ru" xml:lang="en" xmlns:xml="http://www.rusdoc.ru/go.php?http://www.w3.org/XML/1998/namespace">

Это что-то вроде приветствия. Обратите внимание, to="ya.ru" здесь указывается доменная часть идентификатора jabber (например от test@ya.ru)
Далее, после вашего приветсвия сервер должен ответить примерно так

<?xml version=`1.0`?>
<stream:stream xmlns=`jabber:client` xmlns:stream=`http://www.rusdoc.ru/go.php?http://etherx.jabber.org/streams` id=`3357826913` from=`ya.ru` version=`1.0` xml:lang=`en`>
<stream:features>
<starttls xmlns=`urn:ietf:params:xml:ns:xmpp-tls`/>
<compression xmlns=`http://www.rusdoc.ru/go.php?http://jabber.org/features/compress`>
<method>zlib</method>
</compression>
<mechanisms xmlns=`urn:ietf:params:xml:ns:xmpp-sasl`>
<mechanism>PLAIN</mechanism>
</mechanisms>
</stream:features>

В этом ответе сервер указывает, что может работать в защищенном режиме, поддерживает сжатие zlib, механизм авторизации и т.д.
Раз сервер умеет работать в защищенном режиме (tls), значит переводим общение с сервером в защищенный режим следующей командой

<starttls xmlns="urn:ietf:params:xml:ns:xmpp-tls"/>
После этой команды сервер должен ответить согласием примерно так

<proceed xmlns=`urn:ietf:params:xml:ns:xmpp-tls`/>
После этого ответа сервер готов работать по защищенному протоколу и нужно снова отправлять приветствие, так как было в самом начале.

<?xml version="1.0"?>
<stream:stream xmlns:stream="http://www.rusdoc.ru/go.php?http://etherx.jabber.org/streams" version="1.0" xmlns="jabber:client" to="ya.ru" xml:lang="en" xmlns:xml="http://www.rusdoc.ru/go.php?http://www.w3.org/XML/1998/namespace">

Если связь по защищенному соединению удалась и сервер получил привествие, ответ будет таким

<?xml version=`1.0`?>
<stream:stream xmlns=`jabber:client` xmlns:stream=`http://www.rusdoc.ru/go.php?http://etherx.jabber.org/streams` id=`3833292332` from=`ya.ru` version=`1.0` xml:lang=`en`>
<stream:features>
<mechanisms xmlns=`urn:ietf:params:xml:ns:xmpp-sasl`>
<mechanism>PLAIN</mechanism>
</mechanisms>
</stream:features>

Как видно, сейчас сервер уже не показывает, что поддерживает сжатие zlib, так как в защищенном режиме tls поток уже сжат.
Теперь можно начинать авторизацию. Сервер яндекса показывает, что поддерживает лишь один вариант авторизации (sasl PLAIN).
Для этого логин (без домена) и пароль кодируются с помощью base64 ( base64_encode("\x00".$user."\x00".$pass); ) и отправляем на сервер в таком виде

<auth xmlns="urn:ietf:params:xml:ns:xmpp-sasl" mechanism="PLAIN">ADEyMwAxMjM=</auth>
Если авторизация прошла, ответ будет таким

<success xmlns=`urn:ietf:params:xml:ns:xmpp-sasl`/>
После этого опять отправляем приветствие

<?xml version="1.0"?>
<stream:stream xmlns:stream="http://www.rusdoc.ru/go.php?http://etherx.jabber.org/streams" version="1.0" xmlns="jabber:client" to="ya.ru" xml:lang="en" xmlns:xml="http://www.rusdoc.ru/go.php?http://www.w3.org/XML/1998/namespace">

Сервер отвечает и показывает, какие действия доступны уже после авторизации

<?xml version=`1.0`?>
<stream:stream xmlns=`jabber:client` xmlns:stream=`http://www.rusdoc.ru/go.php?http://etherx.jabber.org/streams` id=`1839452106` from=`ya.ru` version=`1.0` xml:lang=`en`>
<stream:features>
<bind xmlns=`urn:ietf:params:xml:ns:xmpp-bind`/>
<session xmlns=`urn:ietf:params:xml:ns:xmpp-session`/>
</stream:features>


Сейчас нужно создать полный JID, то есть связать свой идентификатор с ресурсом. Jabber протокол позволяет соединяться под одним логином из нескольких мест, при этом все соединения будут оставаться в сети. Чтобы определять кто есть кто нужно добавить некую метку (ресурс, любое текстовое имя), при соединении из другого места эту метку нужно ставить другой.
В данном примере связываю с ресурсом webi

<iq type="set" id="1"><bind xmlns="urn:ietf:params:xml:ns:xmpp-bind"><resource>webi</resource></bind></iq>
Вот такой ответ сервера

<iq id=`1` type=`result`><bind xmlns=`urn:ietf:params:xml:ns:xmpp-bind`><jid>test@ya.ru/webi</jid></bind></iq>
Сервер показывает, какой получился полный JID (test@ya.ru/webi)
Полный JID не всегда будет таким как вы ожидаете. Например, talk google добавляет к имени рессурса еще случайную строку и по алгоритму гугла полный JID в этом случае мог бы получиться test@ya.ru/webi75AE39EC. Поэтому после установки ресурса нужно обязательно получить ответ от сервера и узнать какой JID присвоил сервер и его уже использовать дальше.

Далее запускаем сессию

<iq type="set" id="sess_2" to="ya.ru"><session xmlns="urn:ietf:params:xml:ns:xmpp-session"/></iq>
Получаем ответ

<iq type=`result` from=`ya.ru` id=`sess_2`><session xmlns=`urn:ietf:params:xml:ns:xmpp-session`/></iq>
<iq from=`ya.ru` to=`test@ya.ru/webi` id=`ask_version` type=`get`><query xmlns=`jabber:iq:version`/></iq>
<iq from=`ya.ru` to=`test@ya.ru/webi` id=`ping_0` type=`get`><ping xmlns=`urn:xmpp:ping`/></iq>

Ну а теперь можно запросить список контактов например так

<iq type="get" id="3"><query xmlns="jabber:iq:roster"/></iq>
Вот в таком виде выдаются контакты.

<iq from=`test@ya.ru` to=`test@ya.ru/webi` id=`3` type=`result`>
<query xmlns=`jabber:iq:roster`>
<item subscription=`both` name=`Иванова` jid=`name@ya.ru`><group>Имя группы</group></item>
<item subscription=`both` name=`Петров` jid=`name@gmail.com`/>
<item subscription=`both` jid=`name2@ya.ru`/>
<item subscription=`both` name=`Почта (test@)` jid=`lastmail.ya.ru`><group>Яндекс.Информеры</group></item>
</query>
</iq>


На данном этапе статус в сети отключен, вас не видно. Для выхода в онлайн посылаем команду.

<presence><show>chat</show><status>текстовая запись о статусе</status><priority>10</priority></presence>
В данном случае установлен статус chat и приоритет 10. Если ничего не указать в теге <show> статус будет online.

Ответом на эту команду будет список статусов контактов, получение сообщений и т.д.
Теперь если не разрывать соединение будет идти получение различной информации.

Обратите внимание на атрибут тегов id во многих запросах и ответах. Например запрос контактов

<iq type="get" id="3"><query xmlns="jabber:iq:roster"/></iq>
Атрибут id должен быть уникальным при каждом запросе, это нужно для более точной идентификации запроса, на который отвечает сервер или клиент.
В данном примере сервер выдаст список контактов и атрибут id будет указан тоже 3. То есть сервер дает ответ именно для конкретного запроса.
По правилам, вы должны контролировать этот id и посылая команду на сервер нужно проверить, соответсвует ли id в ответе сервера.
Это касается не только исходящих запросов, но и входящих к вам.
Например входящее к вам сообщение будет иметь id и может иметь запрос на подтверждение получения сообщеня. И при получении такого сообщения, клиент должен сразу отправить ответ, что сообщение доставлено, при этом указать id входящего сообщения. Про работу с сообщениями напишу ниже.

Я показал принцип общения с jabber сервером.
Сейчас рассмотрим как это все сделать с помощью php.
Обратите внимание, jabber работает в юникоде, поэтому скрипты должны быть написаны тоже в юникоде.
Если вы будете отправлять русские тексты на сервер не в юникоде, то сервер может разрывать соединение без предупреждения, либо тексты будут разрушенны.

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

<?php
function getxml($stream)
{
    
sleep(1); // перед получением информации дадим паузу, чтобы сервер успел отдать информацию
    
$xml=``;

    
// запрашивать данные 1600 раз, но не более 15 пустых строк
    
$emptyLine = 0;
    for(
$i=0; $i<1600; $i++)
    {
        
$line = fread($stream,2048);
        if(
strlen($line) == 0) {
            
$emptyLine++;
            if(
$emptyLine > 15) break;
        }
        else {
            
$xml .= $line;
        }
    }
    if(!
$xml) return false;
    return
$xml;
}
?>

Так как у данного открытого потока не будет наблюдаться конца, то получать данные из потока можно бесконечно, поэтому ограничимся получением данных 1600 раз, либо встретив 15 пустых строк.
И вот эту функцию и будем использоваться для получения ответов от сервера.

Пример демонстрирую опять же на яндексе, почтовый ящик test@ya.ru

<?php
$user
="test"; // логин до `@`
$domain="ya.ru"; // домен после `@`
$pass="123"; // пароль
$host="xmpp.yandex.ru"; // jabber сервер
$port=5222; // порт

// устанавливаем соединение с сервером
$stream = fsockopen($host,$port,$errorno,$errorstr,10);

// эти настройки необходимы, чтобы при получении данных из потока не было зависания.
// иначе при обнаружении пустой строки php зависнет в длительном ожидании
stream_set_blocking($stream,0);
stream_set_timeout($stream,3600*24);

// после соединения с сервером посылаем привествие(все как писал ранее)
$xml = `<?xml version="1.0"?>
<stream:stream xmlns:stream="http://www.rusdoc.ru/go.php?http://etherx.jabber.org/streams" version="1.0" xmlns="jabber:client" to="`
.$domain.`" xml:lang="en" xmlns:xml="http://www.rusdoc.ru/go.php?http://www.w3.org/XML/1998/namespace">`;
fwrite($stream,$xml."\n"); // отправка данных на сервер в конце ставится перенос строки \n
$xmlin=getxml($stream); // получение ответа от сервера
// обрабатываем ответ сервера, узнаем может ли сервер работать в защищенном режиме,если может переходим в защищенный режим

// посылаем команду на переход в защищенный режим
$xml = `<starttls xmlns="urn:ietf:params:xml:ns:xmpp-tls"/>`;
fwrite($stream,$xml."\n");
$xmlin=getxml($stream); // получаем ответ

// если сервер подтвердил переводим поток в защищенный режим
stream_set_blocking($stream, 1); // сначала блокировку ставим в 1
stream_socket_enable_crypto($stream, TRUE, STREAM_CRYPTO_METHOD_TLS_CLIENT); // переходим в защищенный режим
stream_set_blocking($stream, 0); // блокировку обратно ставим в 0

// после перехода в защищенный режим снова посылаем приветствие
$xml = `<?xml version="1.0"?>`;
$xml .= `<stream:stream xmlns:stream="http://www.rusdoc.ru/go.php?http://etherx.jabber.org/streams" version="1.0" xmlns="jabber:client" to="`.$domain.`" xml:lang="en" xmlns:xml="http://www.rusdoc.ru/go.php?http://www.w3.org/XML/1998/namespace">`;
fwrite($stream, $xml."\n");
$xmlin=getxml($stream); // получение ответа

// теперь проходим авторизацию
$xml = `<auth xmlns="urn:ietf:params:xml:ns:xmpp-sasl" mechanism="PLAIN">`;
$xml .= base64_encode("\x00".$user."\x00".$pass); // вот так кодируется логин пароль для этого типа авторизации
$xml .= `</auth>`;
fwrite($stream, $xml."\n");
$xmlin=getxml($stream);

// после авторизации опять посылаем приветствие
$xml = `<?xml version="1.0"?>`;
$xml .= `<stream:stream xmlns:stream="http://www.rusdoc.ru/go.php?http://etherx.jabber.org/streams" version="1.0" xmlns="jabber:client" to="`.$domain.`" xml:lang="en" xmlns:xml="http://www.rusdoc.ru/go.php?http://www.w3.org/XML/1998/namespace">`;
fwrite($stream,$xml."\n");
$xmlin=getxml($stream);

// сейчас устанавливаем имя ресурса (расположение вашего клиента)
$xml = `<iq type="set" id="2"><bind xmlns="urn:ietf:params:xml:ns:xmpp-bind"><resource>webi</resource></bind></iq>`;
fwrite($stream,$xml."\n");
$xmlin=getxml($stream);

// пошла сессия
$xml = `<iq type="set" id="sess_2" to="`.$domain.`"><session xmlns="urn:ietf:params:xml:ns:xmpp-session"/></iq>`;
fwrite($stream,$xml."\n");
$xmlin=getxml($stream);

// а теперь можно получить список контактов
$xml = `<iq type="get" id="3"><query xmlns="jabber:iq:roster"/></iq>`;
fwrite($stream,$xml."\n");
$xmlin=getxml($stream); // здесь сейчас список ваших контактов

// ну и теперь выходим в онлайн и становимся видимыми для ваших контактов
$xml = `<presence><show></show><status>мой статус онлайн</status><priority>10</priority></presence>`;
fwrite($stream,$xml."\n");
$xmlin=getxml($stream); // после выхода в онлайн здесь будут получены офлайн сообщения и дополнительная информация по статусам ваших контактов.


// теперь можно отправить сообщение например для контакта asd@asd.ru
// в поле from указываете полный JID вместе с ресурсом(он должен быть получен в ответе сервера при установке ресурса), в поле to - кому адресовано сообщение, если ресурс не известен, можно без указания ресурса.
$xml = `<message type="chat" from="test@ya.ru/webi" to="asd@asd.ru" id="et5r">`;
$xml .= `<body>тестовое письмо</body>`;
$xml .= `</message>`;
fwrite($stream,$xml."\n");
$xmlin=getxml($stream);


// Если есть необходимость, можно зациклить скрипт и оставаться подключенным и получать входящие данные
while(1)
{
    
sleep(3); // ставим паузу в 3 секунды, чтобы не создавать большую нагрузку на php
    
$xmlin=getxml($stream); // и раз в 3 секунды идет сбор данных из потока. тут будут приходить сообщения, информация о смене статусов ваших контактов и т.д.
}

?>

Данный пример показывает принцип работы с jabber сервером, но для полноценной работы нужно разбирать ответы сервера, получать сообщения и т.д.
Далее показываю как получать информацию от серера и ее обрабатывать.
Для рабора xml подойдет встроенная в php поддержка SimpleXML.

Каждый ответ от сервера посылаем на разбор во встроенную функцию simplexml_load_string().
Но предварительно удалим строку <?xml version=`1.0`?> и окружим оставшийся XML тегами <webi_xml>

<?php
$xmlin
= preg_replace ("`<\?xml.*\?>`si", "", $xmlin); // сначала удалим начальный тег < xml > если он есть
$xmlin = "<webi_xml>".$xmlin."</webi_xml>"; // окружение xml специфическим тегом, чтобы получилась обработка некоторых невалидных xml
$xml_ob=simplexml_load_string($xmlin); // получился удобный объект, который легко анализировать

?>

Если каждый ответ от сервера пропускать через этот код, то на выходе будет получаться достаточно удобный и читаемый объект.
Можете просмотреть его
print_r($xml_ob);

Для чего же нужно удалять <?xml version=`1.0`?> и окружать оставшийся XML каким то тегом?
Дело в том, что сервер может выдавать за раз сразу несколько ответов.
Например, при подключении можно получить сразу несколько оффлайн сообщений, примерно так

<message [...]>
<body [...]>первое сообщение</body>
</message>

<message [...]>
<body [...]>второе сообщение</body>
</message>

Этот пример демонстрирует как сервер сначала выдал первое сообщение, а затем выдал второе сообщение.
Отдельно каждое сообщение имеет валидный XML.
Но при получении данных из потока, эти два сообщения будут получены как один целый XML, а рассматривая эти два сообщения как одно целое, получается уже не валидный xml и при разборе будет ошибка.
Но если эти сообщения окружить любым тегом, то xml станет валидным.
Поэтому сначала удаляем строку <?xml version=`1.0`?>, если она есть, а затем окружаем оставшийся xml любым тегом, в моем примере <webi_xml> и потом уже отдаем на разбор в функцию simplexml_load_string().

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

<?php
$xml
= `<presence><show></show><status>мой статус онлайн</status><priority>10</priority></presence>`; // ставим статус онлайн
fwrite($stream,$xml."\n");
$xmlin=getxml($stream); // после появления в сети будут получены офлайн сообщения

// далее начинаем обработку полученного xml
$xmlin = preg_replace ("`<\?xml.*\?>`si", "", $xmlin); // сначала удалим начальный тег < xml > если он есть
$xmlin = "<webi_xml>".$xmlin."</webi_xml>"; // окружение xml специфическим тегом, чтобы получилась обработка некоторых невалидных xml
$xml_ob=simplexml_load_string($xmlin); // и теперь здесь находится разложенный по полочкам объект, с полученными сообщениями и т.д.

// например вот информация по первому сообщению
print $xml_ob->message[0]->body; // текст сообщения
print $xml_ob->message[0]->attributes()->from; // от кого
?>

соответствено можно циклом перебрать все пришедшие сообщения подобным способом

<?php
if(isset($xml_ob->message)) // если есть сообщения
{
    foreach (
$xml_ob->message as $message)
    {
        print
$message->body; // очередное сообщение
        
print $message->attributes()->from; // от кого сообщение
    
}
}
?>


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

Дополнительная информация по теме
Готовый PHP класс для работы с jabber
Описание протокола XMPP (jabber) на русском



Авторизация механизмом sasl DIGEST-MD5
В своих примерах я показывал как работает авторизация sasl PLAIN, сейчас расскажу про sasl DIGEST-MD5
Этот механизм авторизации считается более надежным и предподчительным. Но по моему мнению, этот метод более защищенный лишь за-за своей запутанности и некой усложненности.
sasl PLAIN поддерживают почти все jabber сервера, а вот DIGEST-MD5 поддерживают не все сервера, например яндекс на момент написания статьи поддерживал лишь sasl PLAIN атворизацию, а вот гугловский Talk поддерживает оба этих механизма, можно выбрать любой по вашему усмотрению.

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

<?xml version=`1.0`?>
<stream:stream xmlns=`jabber:client` xmlns:stream=`http://www.rusdoc.ru/go.php?http://etherx.jabber.org/streams` id=`3833292332` from=`ya.ru` version=`1.0` xml:lang=`en`>
<stream:features>
<mechanisms xmlns=`urn:ietf:params:xml:ns:xmpp-sasl`>
<mechanism>DIGEST-MD5</mechanism>
<mechanism>PLAIN</mechanism>
</mechanisms>
</stream:features>

Здесь видно, что сервер поддерживает два механизма авторизации, один из них DIGEST-MD5.
Для начала авторизации посылаем серверу команду

<auth xmlns="urn:ietf:params:xml:ns:xmpp-sasl" mechanism="DIGEST-MD5"/>
Ответ от сервера должен выглядеть примерно так

<challenge xmlns=`urn:ietf:params:xml:ns:xmpp-sasl`>bm9uY2U9IjM3MzMyMTM1MjIiLHFvcD0iYXV0aCIsY2hhcnNldD11dGYtOCxhbGdvcml0aG09bWQ1LXNlc3M=</challenge>
Это challenge-пакет, его содержимое закодированно в base64, раскодируем содержимое этого пакета с помощью php функции base64_decode(), получится что то похожее на это
nonce="3733213522",qop="auth",charset=utf-8,algorithm=md5-sess
Для последующей отправки данных понадобится значение nonce
Теперь создаем такую строку (опишу ее нижу)
username="asdasd2641",
response="780e42409d6a40ce7bb59f6d52ec9112",
charset="utf-8",
nc="00000001",
qop="auth",
nonce="3733213522",
digest-uri="xmpp/jabber.ru",
cnonce="gk8K99UVutfDTdj/wPWYt/Klc1qIZV5wLl1+Jw+tdbc="

Далее кодируем ее в base64 с помощью base64_encode() и отправляем таким образом

<response xmlns="urn:ietf:params:xml:ns:xmpp-sasl">dXNlcm5hbWU9ImFzZGFzZDI2NDEiLHJlc3BvbnNlP[=урезано=]</response>
В случае успешной авторизации сервер ответит

<challenge xmlns=`urn:ietf:params:xml:ns:xmpp-sasl`>cnNwYXV0aD05Yjg5YjA0MTU1MGQ1ZDMzYTQ5ZjRmYTZjZjk4YjBlMg==</challenge>
В этом пакете нет ничего нужного.
После этого отправим на сервер такой пакет

<response xmlns="urn:ietf:params:xml:ns:xmpp-sasl"/>
Сервер ответит

<success xmlns=`urn:ietf:params:xml:ns:xmpp-sasl`/>
Авторизация пройдена. Теперь можно отправлять стандартное привествие и далее работаете с сервером по стандартной схеме.

А сейчас разберем самый запутанный момент всего этого механизма авторизации, а именно строку

username="asdasd2641",
response="780e42409d6a40ce7bb59f6d52ec9112",
charset="utf-8",
nc="00000001",
qop="auth",
nonce="3733213522",
digest-uri="xmpp/jabber.ru",
cnonce="gk8K99UVutfDTdj/wPWYt/Klc1qIZV5wLl1+Jw+tdbc="

username логин
charset кодировка
nonce уникальный номер сессии, присланный сервером в предыдущем пакете
nc счетчик, сколько раз был использован этот nonce. обычно всегда используется 00000001
digest-uri протокол, для XMPP сервера он выглядит "xmpp/домен"
cnonce любой уникальный код, сгенерированный клиентом
response содержит пароль и другую информацию в формате MD5, построенную по определенному алгоритму.
Сейчас приведу пример, как с помощью PHP создать response.

<?php
// функция для разбора подобной строки nonce="3733213522",qop="auth",charset=utf-8,algorithm=md5-sess на массив
function explodeData($data) {
    
$data = explode(`,`, $data);
    
$pairs = array();
    
$key = false;

    foreach (
$data as $pair) {
        
$dd = strpos($pair, `=`);

        if (
$dd) {
            
$key = trim(substr($pair, 0, $dd));
            
$pairs[$key] = trim(trim(substr($pair, $dd + 1)), `"`);
        }
        else if (
strpos(strrev(trim($pair)), `"`) === 0 && $key) {
            
$pairs[$key] .= `,` . trim(trim($pair), `"`);
            continue;
        }
    }
    return
$pairs;
}

// обратная функция, создает из массива строку значений
function implodeData($data) {
    
$return = array();
    foreach (
$data as $key => $value) {
        
$return[] = $key . `="` . $value . `"`;
    }
    return
implode(`,`, $return);
}


// функция создания поля response
function response_code($data, $user, $pass) {
    
$data[`nc`]=`00000001`;
    if (isset(
$data[`qop`]) && $data[`qop`] != `auth` && strpos($data[`qop`],`auth`) !== false) {
        
$data[`qop`] = `auth`;
    }
    foreach (array(
`realm`, `cnonce`, `digest-uri`) as $key){
        if (!isset(
$data[$key])) {
            
$data[$key] = ``;
        }
    }
    
$pack = md5($user.`:`.$data[`realm`].`:`.$pass);
    if (isset(
$data[`authzid`])) {
        
$a1 = pack(`H32`,$pack).sprintf(`:%s:%s:%s`,$data[`nonce`],$data[`cnonce`],$data[`authzid`]);
    }
    else {
        
$a1 = pack(`H32`,$pack).sprintf(`:%s:%s`,$data[`nonce`],$data[`cnonce`]);
    }
    
$a2 = `AUTHENTICATE:`.$data[`digest-uri`];

    return
md5(sprintf(`%s:%s:%s:%s:%s:%s`, md5($a1), $data[`nonce`], $data[`nc`], $data[`cnonce`], $data[`qop`], md5($a2)));
}


// начинаю демонстрацию с отправки серверу команды на авторизацию методом DIGEST-MD5
$xml = `<auth xmlns="urn:ietf:params:xml:ns:xmpp-sasl" mechanism="DIGEST-MD5"/>`;
fwrite($stream,$xml."\n");
$xmlin=getxml($stream); // получаем ответ от сервера, тут должен быть challenge пакет, закодированный в base64

// Ответ сервера раскладываем на объект
$xmlin = preg_replace ("`<\?xml.*\?>`si", "", $xmlin);
$xmlin = "<webi_xml>".$xmlin."</webi_xml>";
$xml_ob=simplexml_load_string($xmlin);

$challenge=base64_decode( $xml_ob->challenge[0]); // теперь раскодируем challenge в нормальный вид
$challenge = explodeData($challenge); // раскладываем строку (nonce="3733213522",qop="auth",...) на массив

// добавление к challenge digest-uri если он не пришел в ответе от сервера
if (!isset($challenge[`digest-uri`])) {
    
$challenge[`digest-uri`] = `xmpp/`.$domain;
}

// Генерация cnonce - уникальный номер сессии
$str = ``;
mt_srand((double)microtime()*10000000);
for (
$i=0; $i<32; $i++) {
    
$str .= chr(mt_rand(0, 255));
}
$challenge[`cnonce`] = base64_encode($str);


$response=response_code($challenge, $user, $pass); // создание кодированной строки response на основании некоторых данных из challenge и логина-пароля

// теперь создаем массив значений для отправки
$response_arr = array(`username`=>$user,
`response`=>$response,
`charset`    => `utf-8`,
`nc`=>`00000001`,
`qop`=>`auth`,
);

// Добвление некоторых значений из challenge ответа сервера
foreach (array(`nonce`, `digest-uri`, `realm`, `cnonce`) as $key) {
    if (isset(
$challenge[$key])) {
        
$response_arr[$key] = $challenge[$key];
    }
}

// и теперь формирование xml на отправку. Получившийся массив переводим в строку значений и кодируем в base64
$xml = `<response xmlns="urn:ietf:params:xml:ns:xmpp-sasl">`;
$xml .= base64_encode(implodeData($response_arr));
$xml .= `</response>`;
fwrite($stream,$xml."\n"); // отправка
$xmlin=getxml($stream); // получение ответа

// ну и сразу посылаем встречный ответ и авторизация пройдена
fwrite($stream,`<response xmlns="urn:ietf:params:xml:ns:xmpp-sasl"/>`."\n");
$xmlin=getxml($stream);
?>


Ну а дальше все стандартно...


Отправка и прием сообщений

<message from="от кого" to="кому" xml:lang="ru" type="chat" id="уникальный id">
<body>текст сообщения</body>
<active xmlns="http://www.rusdoc.ru/go.php?http://jabber.org/protocol/chatstates"/>
</message>

Стандартный шаблон сообщения (входящего или исходящего)
from указывается отправитель сообщения полный JID вместе с рессурсом (test@ya.ru/resurs)
to кому адресовано. если ресурс получателя не известен, можно указать без ресурса
id уникальный номер

Сообщение с подверждением о получении выглядит так

<message from="от кого" to="кому" xml:lang="ru" type="chat" id="уникальный id">
<body>текст сообщения</body>
<active xmlns="http://www.rusdoc.ru/go.php?http://jabber.org/protocol/chatstates"/>
<request xmlns="urn:xmpp:receipts"/>
</message>

После получения сообщения с таким запросом нужно ответить

<message from="от кого " to="кому" xml:lang="ru" id="ID сообщения ">
<received xmlns="urn:xmpp:receipts"/>
</message>

В поле id должен стоять ID того сообщения, о котором подтверждается доставка.
Такой ответ может быть ответом на ваше сообщение с запросом, либо вы должны отправить такой ответ на сообщение с запросом.

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

<message from=`от кого` to=`кому` xml:lang=`ru` type=`chat` id=`уникальный id`>
<body>текст сообщения</body>
<active xmlns=`http://www.rusdoc.ru/go.php?http://jabber.org/protocol/chatstates`/>
<x xmlns=`jabber:x:delay` stamp=`20100422T05:41:59`/>
</message>

Именно строка<x xmlns=`jabber:x:delay` stamp=`20100422T05:41:59`/> говорит о том, что сообщение офлайн. Содержит в себе время отправки сообщения

JID - Jabber идентификатор
Jabber ID состоит из имени юзера, домена и ресурса. Например ( test@ya.ru/webi )
Для начала работы с jabber сервером нужно устанавить рессурс, тоесть сформировать полный JID
Как это сделать я уже писал, но послать команду для установки ресурса мало, нужно проверить ответ сервера и вытащить из ответа именно тот JID, который вернул сервер.
Так как некоторые сервера, например talk google добавляют к вашему ресурсу свои метки. Поэтому после установки ресурса узнаем JID из ответа таким образом

<?php
$xmlin
= preg_replace ("`<\?xml.*\?>`si", "", $xmlin);
$xmlin = "<webi_xml>".$xmlin."</webi_xml>";
$xml_ob=simplexml_load_string($xmlin);
$jid=$xml_ob->iq[0]->bind[0]->jid[0]; // здесь теперь тот JID, который установился на сервере, именно его теперь нужно использовать во всех исходящих операциях.
?>



Статус и приоритет
Для чего нужен статус и так все знают(в сети, занят, отсутствую и т.д.)
Но есть еще приоритет, который устанавливается в одной команде со статусом.
Приоритет нужен для того, чтобы понять какому ресурсу отдать предпочтение, если в сети несколько подключений одной учетной записи и если вам отправят сообщение без указания ресурса, то оно доставится на тот ресурс, у которого приоритет выше.
Такая схема смены статуса.

<presence>
<show>chat</show>
<status>Текстовое сообщение</status>
<priority>10</priority>
</presence>

В данном примере установлен статус chat и приоритет 10.
Статус задается в тегах show, возможны следующие варианты
away - Отошел,
chat - Готов чатится (В сети),
dnd - Занят,
xa - Недоступен
Пустой элемент <show/> определяет статус контакта "В сети".


Хитрая авторизация Google Talk
Совершенно не удивительно, что гугл придумал свой механизм авторизации X-GOOGLE-TOKEN.
Пока вы не перейдете в защищенный режим, вам будет доступен только этот механизм авторизации, по мнению гугла в незащищенном потоке только их механизм является самым защищенным.
Если перейти в защищенное соединение, то дополнительно к этому механизму авторизации добавится еще и PLAIN.
Поэтому если нужно соединиться с Google Talk, вам обязательно нужно установить защищенное соединение и авторизоваться через sasl PLAIN.
Как происходит авторизация X-GOOGLE-TOKEN я не разобрался, не смог найти нужной информации.

Ссылки по теме:

Готовый PHP класс для работы с jabber
Описание протокола XMPP (jabber)



Вернуться в раздел: Programming / PHP
© Copyright 1998-2012 Александр Томов. All rights reserved.