ICQ2000 – сделай сам (статья)
WEB-сайт: http://icq2000cc.hobi.ru
Урок №1
Прежде чем приступить к изложению своего небольшого проектика ... скажу сразу…. написан он на Делфи. Кто огорчится , кто обрадуется. Для кого языковой барьер не помеха, а для кого непреодолимое препятствие. Лично я постигал все перелести протоколов ICQ на кодах написанных на С++. Главное - видеть "главное". А мне нравится Делфи. На нем отправить пакет данных в интернет наверное проще, чем записать его в обычный файл.
Самые общие сведения о протоколахм ICQ
Существует около десятка версий ICQ-клиентов. И у каждого - своя версия протокола. Но не смотря на это, их всего два. Есть ICQ, работа, которых с сервером основана на протоколе UDP, и есть ICQ общающиеся с сервером по протоколу TCP. Немного подробнее:
ICQ на протоколе UDP
С нее, собственно, и начиналась история ICQ.
Это были версии протоколов 1,2,3,4 и 5. Это были аськи ICQ97, ICQ98, ICQ99. Т.к. использован протокол UDP, то постоянного соединения клиент-сервер не существует. Пакет передал. Получил подтверждение, и баста. Не получил подтверждение - передай повторно.
Но об этих протоколах уже можно (и нужно) забыть. Они поддерживаются сервером весьма неохотно, потому, что в какой-то момент компания Mirabilis растворилась в компании America OnLine (AOL). После этого ICQ начала работать на протоколе AOL Instant Messenger (AIM). Это и есть вторая группа протоколов ICQ.
ICQ на протоколе TCP
Это версии протоколов 7,8. А может уже и 9,10,11 и т.д.
По сути дела в ICQ20xx используется протокол от AOL Inastant Messenger. И по этому признаку эти два продукта - родные братья. Хоть я и спользовал информацию по прортоколу v8 (ICQ2000b) но рассматривать буду протокол v7 (ICQ2000a). Потому, что эта версия у меня была установлена и именно ее пакеты я использовал для анализа и отладки своего детища. Это различие не имело ровным счетом никакого значения.
Но, как говорится: "ближе к телу".
Вы вправе задать вопрос: "Как же это все будет выглядеть?"
Это выглядит примерно так.
Сами понимаете, что номера ICQ и имена клиентов - полностью вымышленные. Любые совпадения с реальными людьми - чистая случайность. Конечно же, изображения принадлежат своим уважаемым владельцам, поэтому дальше их и не будет.
Как видно на скриншоте, это не просто аська, а - мультиаська! Т.е. можно находиться в онлайне сразу под несколькими UIN-ами одновременно. Иногда это бывает полезно и даже необходимо. В интернете есть конечно примочки для одновременного запуска нескольких копий ICQ, но ничто так не умиляет, как сделанное своими руками. И все же для понимания работы протокола - это излишество, поэтому я оставил только самое необходимое.
Думаю, что не стоит в самом начале нагружать разными сводными таблицами с описанием пакетов протокола. Я буду делать это по мере необходимости. Тем более, что из всего их множества, поначалу не все они будут и нужны.
Для работы вам потребуются только стандартные компоненты Делфи-5. Нет нужды устанавливать какие-то вспомогательные библиотеки или пакеты.
С помощью моего ICQ-клиента можно:
логиниться к серверу;
отображать состояние клиентов;
передавать и принимать сообщения;
регистрировать (register) новый UIN на сервере;
удалять (unregister) UIN с сервера;
просматривать и обновлять информацию о клиентах из контактного списка;
производить поиск клиентов по имени, по e-mail, по UIN-у;
включать найденных клиентов в контактный список;
вести журнал сообщений и пакетов.
Но изначально приложение будет иметь самую минимальную функциональность. UIN и пароль у вас уже должны быть. Будем логиниться на сервере, менять свой статус, принимать сообщения. Весь TCP-трафик идет только через сервер. Так проще и этот способ в комбинации с некоторыми другими параметрами позволит скрыть ваш IP-адрес от любопытных глаз. Наверное, поэтому я не рассматриваю прямые соединения между клиентами. Разумеется, что будем рассматривать протокол v7. На нем работает ICQ2000a.
Итак, приступим...
Все пакеты данных (и от клиента к серверу, и от сервера к клиенту) упаковываются в т.н. FLAP-протокол. Он находится в самом низу иерархии. Ниже показана структура FLAP-пакета:
FLAP
Command Start byte: $2A
Channel ID byte
Sequence Number word
Data Field Length word
Data variable
Каждый FLAP-пакет имеет заголовок c фиксированной длиной и, следующий за ним блок данных (переменной длины). Длина заголовка равна 6-и байтам.
FLAP-заголовок содержит такие поля:
Однобайтовый идентификатор начала пакета (Command Start). Его значение всегда равно $2A. С ним можно сверяться при приеме пакетов.
Идентификатор канала (Channel ID). Он может принимать четыре значения:
1 - канал установления соединения;
2 - канал обмена данными (основная фаза работы: какие-либо полезные данные передаются только в этой фазе);
3 - канал ошибок. (на практике мне не попадался :);
4 - канал разъединения. (это проще, чем написано).
На 99.9% времени протокол работает в канале 2.
Последовательный номер пакета (Sequence Number). В начале обмена данными это поле устанавливается случайным образом, а затем увеличивается на единицу при передаче каждого последующего пакета. Обычно такие поля используются для обеспечения целостности данных (например, когда используется UDP-протокол). Но в нашем случае используется TCP-соединение и этого вполне достаточно для обеспечения целостности передаваемых пакетов. Просто нужно следовать правилу формирования этого поля при передаче пакетов и можно забыть о нем. (На приеме я его никак не контролировал).
Длина блока данных (Data Field Length). Указывает на длину блока данных, который следует сразу же за заголовком. Это очень важное поле. Зная его, мы знаем сколько данных нужно прочитать из входного потока. Ошибись мы хоть на один байт и синхронизация потока будет нарушена.
Блок данных FLAP-пакета. Его длина указана в FLAP-заголовке. В нем находится вся полезная информация для обмена ICQ-клиента и сервера.
При приеме (обработке) FLAP-пакетов очень важно не потерять синхронизацию пакетов ( что просто недопустимо ). Нужно всегда читать 6-и байтовый залоговок, а далее считывать только, то количество данных, которое указано в заголовке. При соблюдении этого правила можно быть уверенным, что прочитанный блок данных будет содержать достоверную информацию. Потеря данных неприемлема в AIM стандарте. Все это на самом деле не трудно обеспечить.
Труднее разобраться в структуре самого блока данных. А напичкан он весьма разнообразными структурными единицами. Видать оччень много народу постаралось для этого. Впечатление такое, что взяли и скрестили старые версии v2 - v5 ICQ-протокола с самим AIM-протоколом. Это вам еще предстоит увидеть. Вот например, для представления обычных текстовых строк , использовано 3 или 4 различных варианта. Представляете себе строку в формате C++ или в формате Pascal, с нулем в конце или без него, с однобайтовой длиной или двубайтовой, а порядок следования байтов в слове? Черт ногу сломает. А может это специально сделано? Мне кажется, что впопыхах!!!
Попробуем разобраться.
Делфи-проект nICQ в начале будет состоять из 3-х модулей: Types, Packet, Main.
В модуле Types находятся константы и объявлены некоторые записи.
В модуле Packet - процедуры и функции для записи/чтения FLAP-пакетов.(Большинство процедур из этого модуля просто адаптированы из других проектов ICQ).
Название модуля Main говорит само за себя.
До того, как начать тестирование, вам следует ввести свой UIN, password, NickName. Внесите эти данные в файл nICQ.ini:
[User]
Uin=199222333
Nick=My Nick
Password=mypass
... и можно запускать. Список контактов нам пока не нужен. Он появится позже. А сейчас будет вполне достаточно, если список контактов будет состоять только из вашего собственного UINа. Жмем на единственную кнопку, а в отладочном окне будет отображаться протокол работы. При выборе одного из пунктов Popup-меню, вызывается процедура icq_Login. Что в ней? Смотрим:
procedure TForm1.icq_Login(Status : longint); begin // определяем свой IP-адрес Local_IP := Get_my_IP; // преобразуем его в DIM_IP StrToIP(Local_IP,DIM_IP); // Запоминаем, какой будет наш статус ICQStatus := Status; // если ClientSocket открыт, то закроем его if CLI.Active then CLI.Close; // установим флажок isAuth, // это значит, что сначала мы коннектимся к серверу // авторизации. UIN и пароль передаются именно ему. isAuth := true; // устанавливаем флажок isHDR, // он говорит нам о том, что, самые первые данные , // из ClientSocket следует интерпретировать как // FLAP-заголовок isHDR := true; // заполняем поля Host и Port в ClintSocket, // адрес сервера авторизации: 'login.icq.com' // и его порт: 5190 CLI.Address :=''; CLI.Host := 'login.icq.com'; CLI.Port := 5190; // не забываем и про TMemo M(Memo,'>>>>>>>>>> login.icq.com:5190 <<<<<<<<<<<'); // ... и собственно CONNECT CLI.Open; end;
(А вот и подходящий момент, чтобы вспомнить о вашем подключении к интернету. Проблемы с получением CONNECTa могут возникнуть, если вы выходите в интернет из LAN через PROXY-сервер. Все зависит от того, как он настроен. Если он предоставляет выход в интернет только для основных сервисов (http,ftp,smtp,pop), то тут уж извините. А если на нем присутствует нормальный маскарадинг, то все будет OK).
Итак, что же дальше..? Желанный CONNECT должен наступить немного раньше конца света :) Мы подсоединились к серверу авторизации и он первым выдаст нам пакет данных. Что делать? Как принять? Куда его засунуть? Об этом мой расказ на следующей странице.
Итак, рассмотрим механизм приема FLAP-пакетов. Прием пакетов - это обработчик события onReadData нашего ClientSocket. Задача этого обработчика сводится только к приему FLAP-пакетов и формировании из них связного списка типа FIFO (первым пришел, первым и ушел). Главное корректно отработать границы пакетов.
Каждый пакет принимается в два захода:
сначала принимаем только заголовок FLAP-пакета (всего 6 байт);
затем, узнав из заголовка длину блока данных, принимаем последний (ни байтом больше, ни байтом меньше).
Приняв полный пакет, формируем из него элемент списка FIFO и присоединяем его к списку. Смотрим, как это сделано у меня. Для прима заголовка и блока данных FLAP-пакета объявлены два массива: FLAP и FLAP_DATA соответственно.
procedure TForm1.CLI_ReadData(Sender:TObject; Socket:TCustomWinSocket); var num,Bytes,fact : integer; pFIFO,CurrFIFO : PFLAP_Item; buf : array[0..100] of byte; begin // узнаем, сколько всего данных уже есть в буфере ClientSocketa num := Socket.ReceiveLength; // в icq_Login мы установили isHdr, т.к. сначала ожидаем заголовок if isHDR then begin // если есть как минимум 6 байт, то читаем 6 байт заголовка в FLAP if num>=6 then begin Socket.ReceiveBuf(FLAP,6); // из заголовка узнаем длину блока данных FLAP-пакета NeedBytes := swap(FLAP.Len); // сбрасываем в начало индекс массива FLAP_DATA index := 0; // сбпасываем, чтобы следующее чтение было в FLAP_DATA // и выходим из обработчика isHDR := false; end else begin // вообще-то ситуация, когда в Sockete меньше 6-и байт // пока никак не контролируется (возникает очень редко :) // отмечаю этот факт только в окне отладки M(Memo,'!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!'); Socket.ReceiveBuf(buf,num); M(Memo,Dim2Hex(@(buf),num)); M(Memo,'!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!'); end; // if not isHDR then чтение в FLAP_DATA end else begin // сколько байт читать уже известно: NeedBytes Bytes := NeedBytes; // читаем их в FLAP_DATA[Index] fact := Socket.ReceiveBuf(FLAP_DATA[index],Bytes); // если в Sockete было данных меньше чем нужно, // педвинем Index и NeedBytes для следующего входа в обработчик inc(index,fact); dec(NeedBytes,fact); if NeedBytes = 0 then begin // если весь блок данных FLAP-пакета уже в FLAP_DATA, // тогда выделаем память для элемента списка FIFO (PFLAP_Item) New(pFIFO); // копируем заголовок pFIFO^.FLAP := FLAP; pFIFO^.Next := nil; // выделяем память для блока данных и копируем его GetMem(pFIFO^.DATA,index); move(FLAP_DATA,PFIFO^.Data^,swap(FLAP.Len)); // добавляем указатель на PFLAP_Item в список CurrFIFO:=HeadFIFO; if HeadFIFO<>nil then begin while CurrFIFO<>nil do if CurrFIFO^.Next=nil then begin CurrFIFO^.Next:=pFIFO; break; end else CurrFIFO:=CurrFIFO^.Next; end else HeadFIFO:=pFIFO; // устанавливаем isHDR (в true) уже для прима заголовка // последующих FLAP-пакетов isHDR := true; end; end; end;
Дальнейшая обработка списка FIFO - это задача уже другой процедуры.
Итак, в обработчике события ClientSocket.onRead_Data из FLAP-пакетов формируется список FIFO. Обработку этого списка производит таймерная процедура MainT. Ее задача заключается в следующем:
взять из очереди FLAP-пакет (если очередь не пуста);
сформировать из него временный объект (запись) типа PPack. (Для его обработки в модуле Packet находятся соответствующие функции и процедуры);
направить его на вход одного из двух обработчиков;
после обработки удалить временный объект.
procedure TForm1.MainTTimer(Sender: TObject); var FindFIFO : PFLAP_Item; tmp : PPack; begin // закроем вход в таймер (реентерабельность нам не нужна) MainT.Enabled := false; // проверим не пуста ли очередь while HeadFIFO<>nil do begin // если есть ожидающие пакеты, то берем первый из них FindFIFO := HeadFIFO; // и корректируем очередь if HeadFIFO^.Next=nil then HeadFIFO := nil else HeadFIFO := HeadFIFO^.Next; // создаем временный Pak tmp := PacketNew; // переносим в него данные из пакета очереди // сначала FLAP-заголовок PacketAppend(tmp,@FindFIFO^.FLAP,sizeof(FLAP_HDR)); // затем блок данных PacketAppend(tmp,FindFIFO^.DATA,swap(FindFIFO^.FLAP.Len)); // освобождаем пакет, который из очереди FreeMem(FindFIFO^.DATA,swap(FindFIFO^.FLAP.Len)); Dispose(FindFIFO); // пропишем его дамп в файл "<твой UIN>.log" debugFILE(tmp,'< '); // если в данный момент мы соединены с сервером авторизации if isAuth then // то напавим пакет в обработчик AuthorizePart AuthorizePart(tmp) else // либо в основной обработчик WorkPart(tmp); // удалим временный Pak PacketDelete(tmp); end; // откроем вход в таймер MainT.Enabled := true; end;
Вполне логично, что дальше надо рассмотреть работу процедуры AuthorizePart, т.к. самый первый FLAP-пакет попадет именно в нее.
Перед рассмотрением работы обработчика AuthorizePart надо немного поговорить и о протоколе.
Перед тем, как подключиться к ICQ-серверу и начать работать мы должны пройти авторизацию на Authorization Server. Его адрес - login.icq.com:5190.
Необходимо:
соединиться с Authorization Server;
передать ему пакет с UINом и паролем;
получить от него IP-адрес и порт основного сервера и Cookie (256 байт случайных данных). Cookie - это будет наш пропуск при последующем (после авторизации) коннекте к основному рабочему серверу;
разьединиться с Authorization Server.
Именно к Authorization Server инициируется соединение в процедуре icq_Login.
Сервер отвечает нам маленьким пакетом:
FLAP
Command Start 2A
Channel ID 01
Sequence Number XX XX
Data Field Length 00 04
Data 00 00 00 01
В нем только лишь 00 00 00 01. Для нас - это сигнал начать передачу пакета с авторизационными данными (с UINом и паролем).
Сейчас уже пора разобраться и с форматом блока данных FLAP-пакета.
Можно сказать, что показанный выше пакет совсем не имеет никакой структуры: просто DWORD и все. В большинстве случаев в FLAP-пакете размещены данные, которые упакованы еще в один протокол: т.н. SNAC. В этом случае пакет данных выглядит так:
FLAP
Command Start 2A
Channel ID 02
Sequence Number word
Data Field Length word
SNAC
Family ID word
SubType ID word
Flags[0] byte
Flags[1] byte
Request ID dword
SNAC Data variable
SNAC
SNAC - это обычное содержимое блока данных FLAP-пакета в основной рабочей фазе соединения. Т.е. SNACи посылаются только через Сhannel ID = 2.
В любом FLAP-пакете может находиться только один пакет SNAC.
Прием (анализ) и передача SCACов - это то основное, что предстоит делать, чтобы реализовать все функции ICQ-клиента. Будь то передача списка контактов, или изменение нашего статуса, или получение и передача сообщений, или запрос информации о любом клиенте, для любого запроса и ответа на него есть свой SNAC (FamilyID, SubTypeID). Из сказанного видно, что вся смысловая информация помещена в SNACи. И UINы, и никнэймы, и и-мэйлы с хоумпэйджами. Конечно же они не просто так накиданы в SNACи. Они там размещены в юнитах, которые называются TLV.
TLV
TLV дословно означает - "Type, Length, Value" ("Тип, Длина, Значение"). Его структура такая:
TLV
(T)ype code word
(L)ength code word
(V)alue field variable length
В TLV упаковывается все, что используется в ICQ-протоколе: текстовые строки, байты, слова, двойные слова, другие массивы и т.д. и т.п.. На тип содеожимого TLV указывает Type code. Чаще всего TLV располагаются внутри SNACов, но это не является обязательным условием. Они могут также напрямую использоваться в блоке данных FLAP-пакета. Именно напрямую (т.е. без использования SNACов) TLV задействованы на этапе авторизации.
Этот механизм мы и рассмотрим именно сейчас, т.к. мы соединены уже с Authorization Server и получили от него добро в виде DWORD=00000001 на передачу нашего UINа и пароля.
procedure TForm1.AuthorizePart(p:PPack); var ss : string; T : integer; tmp : PPack; begin // позиционируемся на начало блока данных, пропустив заголовок PacketGoto(p,sizeof(FLAP_HDR)); // если FLAP-данные содержат лишь 00000001, // то это самое начало сессии if (swap(p^.Len)=4)and (swap(p^.SNAC.FamilyID)=0)and (swap(p^.SNAC.SubTypeID)=1) then begin M(Memo,'< Authorize Server CONNECT'); // каждый раз, когда начинается новая TCP-сессия, // присваиваем SEQ случайное начальное значение SEQ := random($7FFF); // в ответ надо передать пакет с UINом и паролем // создаем объект-пакет типа PPack: в нем формируется // FLAP-заголовок с Chanel_ID=1 tmp := CreatePacket(1,SEQ); // сначала надо вставить такой же DWORD=00000001 // (еще надо помнить о порядке следования байтов в DWORD !!!) PacketAppend32(tmp,DSwap(1)); // далее в поле данных добавляются несколько TLV // это наш UIN - TLV(1) TLVAppendStr(tmp,$1,s(UIN)); // и закодированный пароль - TLV(2) TLVAppendStr(tmp,$2,Calc_Pass(PASSWORD)); // описывать содержимое других TLV особого смысла нет TLVAppendStr(tmp,$3, 'ICQ Inc. - Product of ICQ (TM).2000a.4.31.1.3143.85'); TLVAppendWord(tmp,$16,$010A); TLVAppendWord(tmp,$17,$0004); // 4 - для ICQ2000a TLVAppendWord(tmp,$18,$001F); TLVAppendWord(tmp,$19,$0001); TLVAppendWord(tmp,$1A,$0C47); TLVAppendDWord(tmp,$14,$00000055); TLVAppendStr(tmp,$0F,'en'); TLVAppendStr(tmp,$0E,'us'); // посылаем пакет через ClientSocket // (здесь tmp-пакет будет также и удален) PacketSend(tmp); M(Memo,'> Auth Request (Login)'); end else // на это сервер ответит так: // его ответ содержит TLV(1) - т.е. наш UIN if (TLVReadStr(p,ss)=1)and(ss=s(UIN))then begin // если это так, то считаем следующий TLV T := TLVReadStr(p,ss); case T of // если это TLV(5) - значит это адрес и порт основного сервера 5: g>begin // BOS-IP:PORT M(Memo,'< Auth Responce (COOKIE)'); // запоминаем и адрес и порт WorkAddress := copy(ss,1,pos(':',ss)-1); WorkPort := strtoint(copy(ss,pos(':',ss)+1, length(ss)-pos(':',ss))); // за ними должен быть и TLV(6) - т.н. COOKIE (256 байт) // принимаем его прямо в переменную sCOOKIE // (он пригодится при коннекте к основному серверу) if (TLVReadStr(p,sCOOKIE)=6) then begin; // COOKIE получен и значит пора разъединяться // формируем пустой пакет с Channel_ID=4 tmp:=CreatePacket(4,SEQ); // ChID=4 // который и передаем PacketSend(tmp); // закрываем свой ClientSocket OfflineDiscconnect1Click(self); // говорим себе, что авторизация пройдена isAuth := false; // настраиваем ClientSocket на адрес:порт // основного (BOS) сервера CLI.Address := WorkAddress; CLI.Host := ''; CLI.Port := WorkPort; M(Memo,''); M(Memo,'>>> Connecting to BOS: '+ss); // и коннектимся к нему CLI.Open; { ******************************************* } { в этом месте заканчивается этап авторизации } { ******************************************* } end; end; // а, например, в случае неверного UINа или пароля // мы получим TLV(4) и TLV(8) 4,8: begin M(Memo,'< Auth ERROR'); M(Memo,'TLV($'+inttohex(T,2)+') ERROR'); M(Memo,'STRING: '+ss); if pos('http://',ss)>0 then begin // и даже можем загрузить в браузер присланный нам URL // с описанием ошибки // Web.Navigate(ss); // {это навигатор с панели компонентов Делфи} end; TLVReadStr(p,ss); M(Memo,ss); // конечно же закрываем ClientSocket OfflineDiscconnect1Click(self); M(Memo,''); end; end; end; end;
После успешного прохождения авторизации, мы подключаемся к основному рабочему серверу ICQ. Т.к. флажек isAuth уже сброшен, то диспетчер MainTTimer все пакеты будет направлять на обработчик WorkPart. Его построение во многом схоже с только, что рассмотренным обработчиком AuthorizePart.
Обработчик WorkPart выполняет всю диспетчерскую работу на протяжении всего времени, когда мы подключены к основному ICQ-серверу. Устроен он очень просто.
procedure TForm1.WorkPart(p:PPack); var ss,ss2,sErr : string; tmp : PPack; begin { иногда бывает: сервер прервал соединение. такая ситуация возникала только в одном случае: сервером зафиксирован логин с нашим UINом с другого компьютера. } if p^.FLAP.ChID = 4 then begin PacketGoto(p,sizeof(FLAP_HDR)); // Код ошибки TLVReadStr(p,ss); M(Memo,ss); // Описание ошибки TLVReadStr(p,ss2); M(Memo,ss2); // Разьединяемся OfflineDiscconnect1Click(self); sErr:='Str1: '+Dim2Hex(@(ss[1]),length(ss)); sErr:=sErr+#13#10+'Str2: '+ss2+#13#10+#13#10; ShowMessage('Another Computer Use YOUR UIN!'#13#10+#13#10+ sErr+'...i gonna to disconnect'); // Выходим из обработчика exit; end; {} { Основная секция } // позиционируемся на данные PacketGoto(p,sizeof(FLAP_HDR)+sizeof(SNAC_HDR)); // BOS Connection ACK (DWORD 00000001) // т.е. основной сервер готов с нами общаться if (swap(p^.Len)=4)and (swap(p^.SNAC.FamilyID)=0)and (swap(p^.SNAC.SubTypeID)=1) then begin M(Memo,'< BOS connection ACK'); // ... и мы ему передадим COOKIE // Sign-ON (COOKIE) SEQ := random($7FFF); tmp := CreatePacket(1,SEQ); PacketAppend32(tmp,DSwap($00000001)); TLVAppendStr(tmp,$6,sCOOKIE); PacketSend(tmp); M(Memo,'> Sign-ON (COOKIE)'); end else // if (swap(p^.SNAC.FamilyID)=1)and (swap(p^.SNAC.SubTypeID)=3) then begin M(Memo,'> "I`m ICQ client, not AIM"'); end else // ACK to "I`m ICQ Client" if (swap(p^.SNAC.FamilyID)=$1)and // ACK (swap(p^.SNAC.SubTypeID)=$18) then begin M(Memo,'< Rate Information Request'); end else // Rate Information Response if (swap(p^.SNAC.FamilyID)=$1)and (swap(p^.SNAC.SubTypeID)=$7) then begin M(Memo,'< Rate Information Response'); // ACK to Rate Information Response tmp := CreatePacket(2,SEQ); SNACAppend(tmp,$1,$8); PacketAppend32(tmp,DSwap($00010002)); PacketAppend32(tmp,DSwap($00030004)); PacketAppend16(tmp,Swap($0005)); PacketSend(tmp); M(Memo,'> ACK to Rate Response'); // Request Personal Info tmp := CreatePacket(2,SEQ); SNACAppend(tmp,$1,$0E); PacketSend(tmp); M(Memo,'> Request Personal Info'); // Request Rights for Location service tmp := CreatePacket(2,SEQ); SNACAppend(tmp,$2,$02); PacketSend(tmp); M(Memo,'> Request Rights for Location service'); // Request Rights for Buddy List tmp := CreatePacket(2,SEQ); SNACAppend(tmp,$3,$02); PacketSend(tmp); M(Memo,'> Request Rights for Buddy List'); // Request Rights for ICMB tmp := CreatePacket(2,SEQ); SNACAppend(tmp,$4,$04); PacketSend(tmp); M(Memo,'> Request Rights for ICMB'); // Request BOS Rights tmp := CreatePacket(2,SEQ); SNACAppend(tmp,$9,$02); PacketSend(tmp); M(Memo,'> Request BOS Rights'); end else // Personal Information if (swap(p^.SNAC.FamilyID)=$1)and (swap(p^.SNAC.SubTypeID)=$F) then begin M(Memo,'< Personal Information'); end else // Rights for location service if (swap(p^.SNAC.FamilyID)=$2)and (swap(p^.SNAC.SubTypeID)=$3) then begin M(Memo,'< Rights for location service'); end else // Rights for byddy list if (swap(p^.SNAC.FamilyID)=$3)and (swap(p^.SNAC.SubTypeID)=$3) then begin M(Memo,'< Rights for byddy list'); end else // Rights for ICMB if (swap(p^.SNAC.FamilyID)=$4)and (swap(p^.SNAC.SubTypeID)=$5) then begin M(Memo,'< Rights for ICMB'); end else // BOS Rights if (swap(p^.SNAC.FamilyID)=$9)and (swap(p^.SNAC.SubTypeID)=$3) then begin M(Memo,'< BOS Rights'); // Set ICMB parameters tmp := CreatePacket(2,SEQ); SNACAppend(tmp,$4,$2); PacketAppend16(tmp, swap($0000)); PacketAppend32(tmp,dswap($00000003)); PacketAppend16(tmp, swap($1F40)); PacketAppend16(tmp, swap($03E7)); PacketAppend16(tmp, swap($03E7)); PacketAppend16(tmp, swap($0000)); PacketAppend16(tmp, swap($0000)); PacketSend(tmp); M(Memo,'> Set ICMB parameters'); // Set User Info (capability) tmp := CreatePacket(2,SEQ); SNACAppend(tmp,$2,$4); // tlv(5)=capability TLVAppendStr(tmp,5,#$09#$46#$13#$49#$4C#$7F#$11#$D1+ #$82#$22#$44#$45#$53#$54#$00#$00+ #$09#$46#$13#$44#$4C#$7F#$11#$D1+ #$82#$22#$44#$45#$53#$54#$00#$00); PacketSend(tmp); M(Memo,'> Set User Info (capability)'); // Send Contact List tmp := CreatePacket(2,SEQ); SNACAppend(tmp,$3,$4); // пока включаем только свой UIN PacketAppendB_String(tmp,s(UIN)); // PacketAppendB_String(tmp,s(UIN_1)); // PacketAppendB_String(tmp,s(UIN_2)); // ... // PacketAppendB_String(tmp,s(UIN_n)); // Можно включить любой UIN, ... даже если он и не хочет :) PacketSend(tmp); M(Memo,'> Send Contact List (1)'); // если мы начинаем с режима Invisible, то передаем // Visible List, во всех других режимах - Invisible List case ICQStatus of STATE_INVISIBLE: begin // Send Visible List tmp := CreatePacket(2,SEQ); SNACAppend(tmp,$9,$5); // пока список пуст (кого включать решать вам) PacketSend(tmp); M(Memo,'> Send Visible List (0)'); end; else begin // Send Invisible List tmp := CreatePacket(2,SEQ); SNACAppend(tmp,$9,$7); // пока список пуст (кого включать решать вам) PacketSend(tmp); M(Memo,'> Send Invisible List (0)'); end; end; // case ConnectMode(true); SetStatus(ICQStatus); M(Memo,'> Set Status Code'); // Client Ready tmp := CreatePacket(2,SEQ); SNACAppend(tmp,$1,$2); PacketAppend32(tmp,dswap($00010003)); PacketAppend32(tmp,dswap($0110028A)); PacketAppend32(tmp,dswap($00020001)); PacketAppend32(tmp,dswap($0101028A)); PacketAppend32(tmp,dswap($00030001)); PacketAppend32(tmp,dswap($0110028A)); PacketAppend32(tmp,dswap($00150001)); PacketAppend32(tmp,dswap($0110028A)); PacketAppend32(tmp,dswap($00040001)); PacketAppend32(tmp,dswap($0110028A)); PacketAppend32(tmp,dswap($00060001)); PacketAppend32(tmp,dswap($0110028A)); PacketAppend32(tmp,dswap($00090001)); PacketAppend32(tmp,dswap($0110028A)); PacketAppend32(tmp,dswap($000A0003)); PacketAppend32(tmp,dswap($0110028A)); PacketSend(tmp); M(Memo,'> Client Ready'); { Здесь заканчивается утомительная процедура вхождения в связь (согласования различных параметров с сервером. Возможно, что в AOL Imstant Messenger такая процедура, что-то и значит, но в ICQ-протоколе похоже, что ничего). В этот момент считается, что мы уже в Online и другие клиенты наш статус увидят. } // А мы можем уже запрашивать у сервера полезную информацию, // например, надо запросить off-лайновые сообщения // Get offline messages tmp := CreatePacket(2,SEQ); SNACAppend(tmp,$15,$2); PacketAppend32(tmp,dswap($0001000A)); PacketAppend16(tmp, swap($0800)); PacketAppend32(tmp, UIN); PacketAppend16(tmp, swap($3C00)); PacketAppend16(tmp, swap($0200)); PacketSend(tmp); M(Memo,'> Get offline messages'); end else { здесь начинается секция обработки почти всех пакетов-ответов, которые поступят во время, пока мы подключены к ICQ-серверу. } // UIN ON-line if (swap(p^.SNAC.FamilyID)=$3)and (swap(p^.SNAC.SubTypeID)=$0B) then begin M(Memo,''); ShowUserONStatus(p); M(Memo,''); end else // UIN OFF-line if (swap(p^.SNAC.FamilyID)=$3)and (swap(p^.SNAC.SubTypeID)=$0C) then begin M(Memo,''); M(Memo,'< UIN OFF-line: '+PacketReadB_String(p)); M(Memo,''); end else // Reject notification // отказ сервера выдать статус этого UINа // (встречается очень редко) if (swap(p^.SNAC.FamilyID)=$3)and (swap(p^.SNAC.SubTypeID)=$0A) then begin M(Memo,''); M(Memo,'< Reject from UIN: '+PacketReadB_String(p)); M(Memo,''); end else // SNAC 15,3 // имеет много назначений: // - ответы с offlines messages // - ответы с UserInfo Results // - ответы с SearchUser Results // - и многое другое if (swap(p^.SNAC.FamilyID)=$15)and (swap(p^.SNAC.SubTypeID)=$3) then begin M(Memo,''); SNAC_15_3(p); M(Memo,''); end else // SNAC 4,7 Входящие сообщения (всех типов) if (swap(p^.SNAC.FamilyID)=$4)and (swap(p^.SNAC.SubTypeID)=$7) then begin M(Memo,''); SNAC_4_7(p); M(Memo,''); end else begin // и если, что-то еще не обрабатывается M(Memo,''); M(Memo,'???? Unrecognized SNAC: ????????'); M(Memo,'???? SNAC [$'+inttohex(swap(p^.SNAC.FamilyID),2)+':$'+ inttohex(swap(p^.SNAC.SubTypeID),2)+']'); M(Memo,''); end; end;
Для разбора конкретного примера возьмем ситуацию, когда мы запрашиваем у ICQ-сервера оффлайновые сообщения (т.е. те, которые накопились на сервере, пока нас не было в онлайне).
Запрос оффлайновых сообщений делаем с помощью SNAC(15,2), а ответ на него получим соответственно в SNAC(15,3). Оба этих SNACa имеют очень простой формат. Они содержат в себе только по одному TLV, а именно TLV(1). На первый взгляд все очень просто. Но... TLV(1), в свою очередь, имеет очень ветвистую структуру. (Такие особенности имеют и некоторые другие SNACи, например, SNAC(4,6) для передачи и SNAC(4,7) для приема сообщений).
В заметках к протоколу ICQv7 от Massimo Melina есть описание SNAC(15,2). Этот SNAC используется во множестве различных запросов. Я лишь выделю те строки, которые будут включены в наш запрос, а именно:
заголовок самого SNAC(15,2);
TLV(1), который включает в себя:
длину, следующих далее данных,
наш UIN,
тип запроса ($3С00),
cookie (по которому мы узнаем ответный SNAC(15,3) ).
В описании это находится вот здесь:
SNAC 15,02
TLV(1)
WORD (LE) bytes remaining, useless
UIN my uin
WORD type
WORD cookie
type = 3C00 // ask for offlines messages
nothing
type = 3E00 // ack to offline messages,
nothing type=D007
WORD subtype
subtype=9808 xml-stype in an LNTS
LNTS '' name of required data ''
subtype=1F05 // simple query info
UIN user to request info subtype=B204 // query info about user
UIN user to request info subtype=D004 // query my info
UIN my uin
..............
..............
..............
В исходном коде это выглядит так:
// Get offline messages // создаем FLAP-заголовок с Channel_ID=2 и SEQ++ tmp := CreatePacket(2,SEQ); // добавляем SNAC-заголовок SNAC(15,2) SNACAppend(tmp,$15,$2); // добавляем TLV(1) ($0001-Type, $000A-Length) PacketAppend32(tmp,dswap($0001000A)); // добавляем саму Value Для TLV(1) PacketAppend16(tmp, swap($0800));// бесполезная длина PacketAppend32(tmp, UIN); // наш UIN PacketAppend16(tmp, swap($3C00));// тип запроса PacketAppend16(tmp, swap($0200));// cookie PacketSend(tmp); M(Memo,'> Get offline messages');
Этот кусок кода сгенерирует следующий дамп:
2A 02 36 86 00 18 00 15
00 02 00 00 00 87 00 02
00 01 00 0A 08 00 XX XX
XX XX 3C 00 02 00
Разпишем его в табличном виде для лучшего восприятия:
FLAP
Command Start 2A
Channel ID 02
Sequence Number 36 86
Data Field Length 00 18
SNAC (15, 02)
Family ID 00 15
SubType ID 00 02
Flags[0] 00
Flags[1] 00
Request ID 00 87 00 02
TLV (1)
Type 00 01
Length 00 0A
Value 08 00
XX XX XX XX наш UIN
3C 00 запрос на оффлайновые сообщения
02 00 cookie
Передадим пакет и от сервера получим FLAP-пакет с таким дампом:
2A 02 74 6D 00 4D 00 15
00 03 00 01 00 87 00 02
00 01 00 3F 3D 00 XX XX
XX XX 41 00 02 00 F8 5F
F1 08 D2 07 02 0C 10 12
01 00 25 00 EF F0 E8 E2
E5 F2 0D 0A FD F2 EE 20
F2 E5 F1 F2 EE E2 EE E5
20 F1 EE EE E1 F9 E5 ED
E8 E5 20 21 21 21 0D 0A
00 00 00
И снова распишем его в таблицу:
FLAP
Command Start 2A
Channel ID 02
Sequence Number 74 6D
Data Field Length 4D 00
SNAC (15, 03)
Family ID 00 15
SubType ID 00 03
Flags[0] 00
Flags[1] 01
Request ID 00 87 00 02 (такой же как и в запросе)
TLV (1)
Type 00 01
Length 00 3F
Value 3D 00
XX XX XX XX наш UIN
41 00 тип: оффлайновое сообщение
02 00 cookie (как и в запросе)
тело сообщения
XX XX XX XX его UIN
D2 07 год (2002)
02 месяц (февраль)
0C день (12)
10 час (16)
12 минуты (18)
01 под-тип сообщения
(обычное)
00 флаги сообщения (?)
25 00 длина сообщения (37)
EF F0 E8 E2 E5 F2 0D 0A FD F2 EE 20 F2 E5 F1 F2 EE E2 EE E5 20 F1 EE EE E1 F9 E5 ED E8 E5 20 21 21 21 0D 0A 00 текст сообщения:
"привет
это тестовое сообщение !!!"
00 00 присутствют, если сообщение единственное
В протокольных заметках я выделю ту часть описания SNAC(15,3), которая соответствует таблице:
SNAC 15,03
TLV(1)
WORD (LE) bytes remaining, useless
UIN my uin
WORD message-type
WORD cookie
message-type = 4100 // offline message
UIN his uin
WORD year (LE)
BYTE month (1=jan)
BYTE day
BYTE hour (GMT time)
BYTE minutes
BYTE msg-subtype
BYTE msg-flags
LNTS msg
WORD 0000, present only in single messages
message-type = 4200 // end of offline messages
BYTE unknown, usually 0
message-type = D007
2 BYTE unknown, usually 98 08
WORD length of the following NTS
NTS ""field-type""
field-type = DataFilesIP
6 BYTE unk, usually 2A 02 44 25 00 31
message-type = DA07
3 BYTE subtype
subtype=A4010A // wp-full-request result
wp-result-info
..............
..............
..............
subtype=B4000A // ack to remove user
empty
subtype=AA000A // ack to change password
empty
И "нарешти" - код для приема SNAC(15,3). Множественные комментарии, кажется тут уже излишни.
procedure TForm1.SNAC_15_3(p:PPack); var MessageType,Cookie : word; myUIN,hisUIN : longint; year,month,day,hour,minute,typemes,subtypemes,lenmes : word; tmp : PPack; begin // просто пролетаем над началом TLV(1) PacketRead32(p); PacketRead16(p); // а дальше имена переменных объясняют больше, чем комментарии myUIN := PacketRead32(p); MessageType := swap(PacketRead16(p)); Cookie := swap(PacketRead16(p)); M(Memo,'< Cookie: $'+inttohex(Cookie,4)); case MessageType of $4100: begin // OFFLINE MESSAGE hisUIN := PacketRead32(p); M(Memo,'< Message-Type: $'+inttohex(MessageType,4)); M(Memo,'< OFFLINE MESSAGE from UIN: '+s(hisUIN)); year := PacketRead16(p); month := PacketRead8(p); day := PacketRead8(p); hour := PacketRead8(p); minute := PacketRead8(p); typemes := PacketRead8(p); subtypemes := PacketRead8(p); lenmes := PacketRead16(p); DoMsg(false,typemes,lenmes,PCharArray(@(p^.data[p^.cursor])), hisUIN,UTC2LT(year,month,day,hour,minute)); end; end; end;
Передача сообщений
Уверен, что у вас не возникло никаких проблем со скачиванием, с компиляцией, с "конфигурированием" первого проекта. Если вы вписывали в файл nICQ.ini свой пароль, то коннект был обеспечен.
Урок №2 содержит два новых модуля. SendMess и MessFrom. Каждый из них имеет свое окно. Это - передача и прием сообщений.
Чтобы полноценно передавать сообщения, необходим и такой объект в основном окне, как список контактов. Объект TTreeView напрашивается сам. Проще некуда. Тем более каждый элемент в нем может содержать указатель на связанные данные. TTreeView меня полностью устроил.
Сам список контактов будет хранится в файле <ваш_uin>.dat
Т.к. сейчас рассматриваетя только урок №2, то и заполняться этот файл будет пока только вручную. При его заполнении вполне можно пренебречь процедурой авторизации.
[ContactList]
199111222=1st_User
199111333=2nd_User
199111444=3rd_User
345345234=Иван Иваныч
188888888=Вася Пупкин
и т.д. и т.п.
Вписывайте UINов столько, сколько нужно. Только не забудьте увеличить массив TContactList, если UINов планируете больше сотни:
type TContactList = array[0..100] of TListRecord;
И еще пару слов относительно интерфейса: те кому надоели зелененькие цветочки - могут нарисовать свои значки для контактного списка. Bitmapы прилагаются.
Теперь о том как реально передаются сообщения.
Есть два типа передаваемых сообщений: Simple Message и Advanced Message.
Если UIN (для которого предназначено сообщение) находится в оффлайне - то ему шлется Simple Message. Advanced Message посылаются тем адресатам, (кажется ) если версия аськи у них не ниже ICQ2000. Из формата Advanced Message в уроке №2 используется лишь информация о Foreground Color и Background Color (это цвета раскраски текста). Использовал бы еще что-нибудь, так там больше ничего нет такого, что можно назвать advanced.
При передаче, сообщения пакуются в SNAC(4,06).
Начнем с более простого формата - Simple Message:
FLAP
Command Start 2A
Channel ID 02
Sequence Number 34 3B
Data Field Length 00 3D
SNAC (4, 06) - Send Message (Simple)
Family ID 00 04
SubType ID 00 06
Flags[0] 00
Flags[1] 00
Request ID 00 AD 00 06
53 DE 53 75
Cookie 1
16 14 BB 50 Cookie 2
00 01 msg-format: Simple Message
09
длина его UINа почти как
PascalStr
31 39 39
37 37 37
36 36 36 его UIN
(например: '199777666')
TLV (2) - сообщение здесь
T ype 00 02
L ength 00 17
V alue 05 01 00 01 01 01 01 (unk) ???
00 0E длина сообщения
+ 4
00 00 00 00 (unk) ???
D1 EE EE E1 F9 E5 ED E8 E5 21 'Сообщение!'
TLV (6) - пустой
T ype 00 06
L ength 00 00
Продолжим более сложным форматом - Advanced Message. А он действительно по-сложнее будет.
FLAP
Command Start 2A
Channel ID 02
Sequence Number 0C A3
Data Field Length 00 99
SNAC (4, 06) - Send Message (Advanced)
Family ID 00 04
SubType ID 00 06
Flags[0] 00
Flags[1] 00
Request ID 00 C3 00 06
1C D3 C4 B7
Cookie 1
23 4D 75 95 Cookie 2
00 02 msg-format: Advanced Message
09
длина его UINа почти как
PascalStr
31 39 39
37 37 37
36 36 36 его UIN
(например: '199777666')
TLV (5)
T ype 00 05
L ength 00 73
V alue 00 00 00 00 - для посылки сообщения
1C D3 C4 B7 Cookie 1
23 4D 75 95 Cookie 1
09 46 13 49
4C 7F 11 D1
82 22 44 45
53 54 00 00 4 DWORD
наши возможности ???
(capability)
TLV (A)
T ype 00 0A
L ength 00 02
V alue 00 01 00 01 - для посылки сообщения
TLV (F) - пустой (???)
T ype 00 0F
L ength 00 00
TLV (2711) - сообщение здесь
T ype 27 11
L ength 00 4B
V alue 1B 00 07 00 00
00 00 00 00 00
00 00 00 00 00
00 00 00 00 00
00 00 03 00 00
00 26 байт (unk)
00
FF FF
0E 00
FF FF
00 00 00 00 00
00 00 00 00 00
00 00 12 байт (unk)
01 msg-subtype ( 01-обычное )
00
00 00
01 00
0E 00 длина сообщения тело
сообщения
D1 EE EE E1 F9 E5 ED E8 E5 20 B9 32 2E (00) 'Сообщение №2.'
80 00 80 00 foreground color
FF FF 00 00 background color
TLV (3) - пустой
T ype 00 03
L ength 00 00 TLV(3) посылается, как запрос подтверждения
Что касается кода, то мудровать с формированием TLV я не стал. Зато получилось дешево и сердито. Одним словом - это все работает.
unit SendMess; procedure TMessageTo.SendButtonClick(Sender: TObject); var sNN,sMess,sUIN : string; tmp : PPack; sTmp : string; d1,d2 : longint; buf : TByteArray; ind,indmem : word; const capab : string{16}= #$09#$46#$13#$49#$4C#$7F#$11#$D1+ #$82#$22#$44#$45#$53#$54#$00#$00; blok : string{26} = #$1B#$00#$07#$00#$00#$00#$00#$00+ #$00#$00#$00#$00#$00#$00#$00#$00+ #$00#$00#$00#$00#$00#$00#$03#$00+ #$00#$00; x:word=0; begin sNN := NNEd.Text; sUIN := ICQEd.Text; if SendMemo.Lines.Count = 0 then exit; sMess := SendMemo.Text; // создаем новый FLAP tmp := CreatePacket(2,SEQ); // добавляем SNAC(4,6) SNACAppend(tmp,$4,$6); // генерируем Cookie-1 и Cookie-2 d1:=random($7FFFFFFF); d2:=random($7FFFFFFF); // запоминаем их: по ним мы узнаем ACKи от сервера и клиента SEQ1:=dswap(d1); SEQ2:=dswap(d2); PacketAppend32(tmp,dswap(d1)); PacketAppend32(tmp,dswap(d2)); // проверяем какой тип сообщения выбран case MesFmtBox.Checked of true: begin // advanced message // 0002 - advanced PacketAppend16(tmp,swap($0002)); // кому ? // дальше, вся последовательность формируется // в дополнительном буфере buf PacketAppendB_String(tmp,sUIN); // TLV(5) + его длина, которую впишем в конце ind:=0;fillchar(buf,sizeof(buf),'^'); PLONG(@(buf[ind]))^:=dswap($0005FFFF);inc(ind,4); // Cookie-1 и Cookie-2 PWORD(@(buf[ind]))^:=0;inc(ind,2); PLONG(@(buf[ind]))^:=dswap(d1);inc(ind,4); PLONG(@(buf[ind]))^:=dswap(d2);inc(ind,4); // Capability MOVE(capab[1],buf[ind],length(capab));inc(ind,length(capab)); //TLV(A)=0001 PLONG(@(buf[ind]))^:=dswap($000A0002);inc(ind,4); PWORD(@(buf[ind]))^:=swap($0001);inc(ind,2); //TLV(F)-пустой PLONG(@(buf[ind]))^:=dswap($000F0000);inc(ind,4); // TLV(2711) + его длина, которую впишем в конце PLONG(@(buf[ind]))^:=dswap($2711FFFF);inc(ind,4); indmem:=ind-2; // 16 байт MOVE(blok[1],buf[ind],length(blok));inc(ind,length(blok)); PBYTE(@(buf[ind]))^:=0;inc(ind,1); PWORD(@(buf[ind]))^:=swap($FFFF);inc(ind,2); PWORD(@(buf[ind]))^:=swap($0E00);inc(ind,2); PWORD(@(buf[ind]))^:=swap($FFFF);inc(ind,2); // 12 байт = 0 PLONG(@(buf[ind]))^:=$00000000;inc(ind,4); PLONG(@(buf[ind]))^:=$00000000;inc(ind,4); PLONG(@(buf[ind]))^:=$00000000;inc(ind,4); // под-Тип сообщения = 1 (обычное) PBYTE(@(buf[ind]))^:=1;inc(ind,1); PBYTE(@(buf[ind]))^:=0;inc(ind,1); PWORD(@(buf[ind]))^:=swap($0000);inc(ind,2); PWORD(@(buf[ind]))^:=swap($0100);inc(ind,2); // длина сообщения PWORD(@(buf[ind]))^:=length(sMess)+1;inc(ind,2); // сообщение move(sMess[1],buf[ind],length(sMess));inc(ind,length(sMess)); // завершающий ноль PBYTE(@(buf[ind]))^:=0;inc(ind,1); // foreground color PLONG(@(buf[ind]))^:=dswap(GetColor(SendMemo,FG));inc(ind,4); // background color PLONG(@(buf[ind]))^:=dswap(GetColor(SendMemo,BG));inc(ind,4); // вписываем фактическую длину в TLV(5) PWORD(@(buf[2]))^:=swap(ind-4); // подсчитывем фактическую длину TLV(2711) x:=length(blok)+27+length(sMess)+9; // ... и вписывем ее PWORD(@(buf[indmem]))^:=swap(x); // пепеносим данные с buf в FLAP PacketAppend(tmp,@buf,ind); // ack request ? (запрос подтверждения) // TLV(3)-пустой PacketAppend32(tmp,dswap($00030000)); end; false: g>begin // simple message // 0001 - simple PacketAppend16(tmp,swap($0001)); // кому ? PacketAppendB_String(tmp,sUIN); // tlv(2) PacketAppend16(tmp,swap(2)); // длина tlv(2) PacketAppend16(tmp,swap(13+length(sMess))); // 7 байт PacketAppend32(tmp,dswap($05010001)); PacketAppend16(tmp,swap($0101)); PacketAppend8(tmp,$01); // длина сообщения + 4 PacketAppend16(tmp,swap(4+length(sMess))); // 4 байта = 0 PacketAppend32(tmp,dswap($0)); // сообщение PacketAppend(tmp,@(sMess[1]),length(sMess)); // tlv(6) - пустой PacketAppend16(tmp,swap($0006)); PacketAppend16(tmp,0); end; end; //case // посылаем пакет Form1.PacketSend(tmp); M(SendMemo,'Sending...'); // пишем в журнал case MesFmtBox.Checked of // A - advanced true: sTmp := '[A] '; // S - simple false: sTmp := '[S] '; end; // тут и так ясно sTmp := '->'+sTmp+DateTimeToStr(Now)+' '+ sNN+' ['+sUIN+'] "'+sMess+'"'; M(Form1.Memo,sTmp); Form1.LogMessage(sTmp); if MesFmtBox.Checked then begin // если advanced SendAnime.Active := true; SendMemo.Enabled := false; SendButton.Enabled := false; MesFmtBox.Enabled := false; // окно закроется только после получения // ACKов от сервера и от клиента (или вручную) end else // если simple, то окно сразу закрывается Close; end;
Прием сообщений
Все сообщения приходят в SNAC(4,07).
У него такой же формат, как и у SNAC(4,06). Поэтому стоит сразу комментировать код:
unit Main.pas; procedure TForm1.SNAC_4_7(p:PPack); var i,cnt,T,MessageFormat,SubMode,SubMode2,Empty : word; {myUIN,} hisUIN : longint; SubType : array[0..3] of byte; MessageSubType : longint absolute SubType; tmp,tmp2,tmp3 : PPack; sTemp : string; dTemp : TByteArray; typemes, {subtypemes,} unk,modifier,lenmes : word; // для SNAC(4,0B)-подтверждения принятых advanced сообщений d1,d2 : longint; ACK : TByteArray; ind : word; NewMsg : PMsgItem; FG : array[0..3] of byte; BG : array[0..3] of byte; begin // сохраняем Cookie-1 и Cookie-2 d1:=PacketRead32(p); d2:=PacketRead32(p); // читаем формат сообщения MessageFormat := swap(PacketRead16(p)); // от кого ? sTemp := PacketReadB_String(p); // Cookie-1,Cookie-2 и некоторую другую часть пакета сохраним. // Эти данные необходимо включить в ACK на это сообщение ind:=0; PLONG(@(ACK[ind]))^:=d1; inc(ind,4); PLONG(@(ACK[ind]))^:=d2; inc(ind,4); PWORD(@(ACK[ind]))^:=swap(MessageFormat);inc(ind,2); PBYTE(@(ACK[ind]))^:=length(sTemp);inc(ind,1); MOVE(sTemp[1],ACK[ind],length(sTemp));inc(ind,length(sTemp)); PWORD(@(ACK[ind]))^:=swap($0003);inc(ind,2); // преобразуем его UIN из строки в longint try hisUIN := strtoint(sTemp); except hisUIN:=0; end; M(Memo,'< From: '+sTemp); PacketRead16(p); // узнаем сколько всего TLV во входящем пакете cnt := swap(PacketRead16(p)); // читаем все эти TLV for i:=1 to cnt do // самый интересный - TLV(6) if TLVReadStr(p,sTemp)=6 then begin { в TLV(6) - его статус } end; // анализируем каждый из форматов case MessageFormat of $0001: begin M(Memo,'< Message-format:1 (SIMPLE)'); // чтение TLV(2) в sTemp TLVReadStr(p,sTemp); // скопируем sTemp во временный PPack, // для удобства обработки tmp := PacketNew; PacketAppend(tmp,@(sTemp[1]),length(sTemp)); PacketGoto(tmp,0); // обработаем его PacketRead16(tmp); PacketRead16(tmp); PacketRead8(tmp); PacketRead16(tmp); // добрались до длины сообщения lenmes := swap(PacketRead16(tmp))-4; PacketRead32(tmp); // читаем сообщение в sTemp PacketRead(tmp,@sTemp[1],lenmes); SetLength(sTemp,lenmes); // анализ содержания сообщения DoSimpleMsg(hisUIN,sTemp); // удалим временный PPack PacketDelete(tmp); end; $0002: begin M(Memo,'< Message-format:2 (ADVANCED)'); // чтение TLV(5) в sTemp TLVReadStr(p,sTemp); // скопируем sTemp во временный PPack, // для удобства обработки tmp := PacketNew; PacketAppend(tmp,@(sTemp[1]),length(sTemp)); PacketGoto(tmp,0); // обработаем его SubMode := swap(PacketRead16(tmp)); PacketRead32(tmp); PacketRead32(tmp); PacketRead(tmp,@dTemp,16); case SubMode of $0000: begin M(Memo,'SubMode: $0000 NORMAL'); TLVReadWord(tmp,SubMode2); // TLV(F) - пустой TLVReadWord(tmp,Empty); // прием и анализ TLV(2711) T := TLVReadStr(tmp,sTemp); if T=$2711 then begin // сохраняем кусок данных для ACKа MOVE(sTemp[1],ACK[ind],47);inc(ind,47); PLONG(@(ACK[ind]))^:=0; inc(ind,4); // используем временный PPack tmp2 := PacketNew; PacketAppend(tmp2,@(sTemp[1]),length(sTemp)); PacketGoto(tmp2,0); PacketRead(tmp2,@dTemp,26); PacketRead8(tmp2); PacketRead16(tmp2); PacketRead16(tmp2); PacketRead16(tmp2); PacketRead(tmp2,@dTemp,12); // читаем ТИП сообщения typemes := PacketRead8(tmp2); {subtypemes := }PacketRead8(tmp2); unk:=swap(PacketRead16(tmp2)); modifier:=swap(PacketRead16(tmp2)); M(Memo,'Unk: $'+inttohex(unk,4)); M(Memo,'Modifier: $'+inttohex(modifier,4)); // длина сообщения lenmes := PacketRead16(tmp2); // анализ сообщения NewMsg:=DoMsg(true,typemes, lenmes,PCharArray(@(tmp2^.data[tmp2^.cursor])), hisUIN,Now2DateTime); // небольшая перемотка PacketGoto(tmp2,(tmp2^.cursor)+lenmes); // читаем Foreground и Background Colors PacketRead(tmp2,@FG,4); PacketRead(tmp2,@BG,4); if NewMsg<>nil then begin NewMsg^.FG:='$00'+inttohex(FG[2],2)+ inttohex(FG[1],2)+ inttohex(FG[0],2); NewMsg^.BG:='$00'+inttohex(BG[2],2)+ inttohex(BG[1],2)+ inttohex(BG[0],2); end; // удаление временного PPack PacketDelete(tmp2); // дозаполнение ACK PWORD(@(ACK[ind]))^:= 1; inc(ind,2); PBYTE(@(ACK[ind]))^:= 0; inc(ind,1); PLONG(@(ACK[ind]))^:= 0; inc(ind,4); PLONG(@(ACK[ind]))^:=-1; inc(ind,4); // посылка ACKа tmp3 := CreatePacket($2,SEQ); SNACAppend(tmp3,$4,$0B); PacketAppend(tmp3,@ACK[0],ind); PacketSend(tmp3); end; // Submode:$0000 end; $0001: M(Memo,'SubMode:$0001 ??? message canceled ???'); $0002: M(Memo,'SubMode:$0002 FILE-ACK'); // case SubMode end; PacketDelete(tmp); end; $0004: begin M(Memo,'< Message-format:4 '+ '(url or contacts or auth-req or userAddedYou)'); TLVReadStr(p,sTemp); tmp := PacketNew; PacketAppend(tmp,@(sTemp[1]),length(sTemp)); PacketGoto(tmp,0); hisUIN := PacketRead32(tmp); typemes := PacketRead8(tmp); {subtypemes := } PacketRead8(tmp); lenmes := PacketRead16(tmp); DoMsg(true,typemes, lenmes,PCharArray(@(tmp^.data[tmp^.cursor])), hisUIN,Now2DateTime); PacketDelete(tmp); end; else M(Memo,'< ??? SNAC 4,7; Message-format: '+s(MessageFormat)); // case MessageFormat end; end;
Урок №3
Запрос информации о клиенте,
Поиск клиентов по различным критериям и др.
Итак... Передавать и принимать сообщения уже умеем. На очереди - получение информации о клиентах, которые находятся в списке контактов; а также поиск новых клиентов по различным критериям. Такие запросы к серверу посылаются с помощью все тех же SNAC(15,2). Вспомните, как производится запрос оффлайновых сообщений.
Точно также SNAC(15,2) с типом запроса равным D007 применяется:
при всех операциях с Инфо клиентов ( и получение, и обновление своего);
при поиске клиентов по имени, по UINу, по E-mailу;
при изменении пароля;
при удалении UINа из реестра ICQ;
при многих других операциях.
Каждая из перечисленных операций определяется подтипом запроса. Приведу обобщенную таблицу SNAC(15,2) для некоторых запросов:
FLAP
Command Start 2A
Channel ID 02
Sequence Number XX XX
Data Field Length XX XX
SNAC (15, 02)
Family ID 00 15
SubType ID 00 02
Flags[0] 00
Flags[1] 00
Request ID 00 XX 00 02 (по ним можно опознать ответ)
TLV (1)
Type 00 01
Length XX XX
Value Length-2
(и что оно тут делает ?)
XX XX XX XX наш UIN
D0 07 тип запроса
XX 00 cookie
(по нему можно/нужно
опознавать ответ)
B2 04 подтип запроса
(B204 - запрос инфо клиента)
Это переменная часть зпроса.
Она определяется подтипом запроса.
Например:
При запросе инфо клиента (B2 04) или при поиске клиента по UINу (1F05) здесь следует разместить запрашиваемый UIN.
При поиске клиента по E-mail (2905) здесь будет помещена строка с искомым адресом.
При поиске по NickName, FirstName, LastName (1505) сюда помещаются соответственно три стоки.
При смене пароля (2E04) здесь будет лишь строка с паролем, а наш UIN сервер и так знает.
Теперь к Delphi-проекту добавлены еще два модуля: UInfo и SUser (User Info & Search User).
Очередные исходники 3-его урока здесь. Т.к. все рассмотренные више запросы практически однотипные, то приведу комментарии только к одному из них. Это будет поиск по NickName, FirstName, LastName:
unit SUser; procedure TSearchUser.META_Search_User(NN,FN,LN : string); var p : PPack; // промежуточный массив. // в нем накапливаются данные TLV(1) b : TByteArray; i : integer; begin if (NN='')and(FN='')and(LN='') then exit; EndOfSearch := false; // word(b[0]) - тут будет ненужная длина // (но ее надо потом корректно заполнить) // а пока переходим к 3-у елементу i:=2; // вписываем UIN (только СВОЙ - укажем явно,что из модуля Main.pas) PLONG(@(b[i]))^ := main.UIN; inc(i,4); // ТИП запроса PWORD(@(b[i]))^ := swap($D007); inc(i,2); // придумаем себе COOKIE // (можно и по-проще, но в настоящей аське // COOKIE имеет такой вид XX00) Cookie := random($FF) shl 8; PWORD(@(b[i]))^ := swap(Cookie); inc(i,2); // ПОДТИП запроса PWORD(@(b[i]))^ := swap($1505); inc(i,2); // добавляем три текстовые строки (First, Last, Nick) // у AOL новый тип строк наверное :) // впереди - длина строки, а в конце #0 // (что-то одно из них убрали бы) // длина строки PWORD(@(b[i]))^ := length(FN)+1; inc(i,2); // сама строка FirstName MOVE(FN[1],b[i],length(FN)); inc(i,Length(FN)); // завершающий #0 PBYTE(@(b[i]))^ := 0; inc(i,1); // LastName PWORD(@(b[i]))^ := length(LN)+1; inc(i,2); MOVE(LN[1],b[i],length(LN)); inc(i,Length(LN)); PBYTE(@(b[i]))^ := 0; inc(i,1); // NickName PWORD(@(b[i]))^ := length(NN)+1; inc(i,2); MOVE(NN[1],b[i],length(NN)); inc(i,Length(NN)); PBYTE(@(b[i]))^ := 0; inc(i,1); // дозаполним "ненужную" длину в начале массива PWORD(@(b[0]))^ := i-2; // создаем FLAP-пакет P:=CreatePacket(2,SEQ); // добавляем SNAC(15,2) SNACAppend(p,$15,$2); // добавляем TLV(1) с данными из промежуточного массива TLVAppend(p,1,i,@b); // шлем запрос Form1.PacketSend(p); // пишем в Memo M(Form1.Memo,'> Search Detail: Nick:'+NN+ ' First:'+FN+ ' Last:'+LN+' '+ 'Cookie:$'+inttohex(Cookie,4)); end;
Запросы других подтипов передаются аналогично. С небольшими вариациями. Оновременно можем передавать на сервер много запросов. Сервер разберется. Ведь в каждом нашем запросе есть уникальное(ый) Cookie (а также и RequestID в SNAC-заголовке). Сервер пометит свои пакеты-ответы этими же опознавательными знаками.
Я лично делаю проверку(сверку) только по Cookie. Выдавая запрос, запоминаю Cookie. А когда приходит ответ от сервера, то процедура-обработчик SNAC_15_3 просто использует WinAPI функцию PostMessage для передачи ответа окну, которое выдало запрос. В параметрах PostMessage указан Cookie из ответа сервера. Какое окно его опознает - значит тому окну и предназначен ответ.
Работа процедуры-обработчика SNAC_15_3 уже ранее рассматривалась. Сейчас она просто дополнена новыми блоками, обрабатывающими новые ответы сервера. Следует упомянуть, что на один (единственный) наш запрос сервер присылает сразу целый массив из SNAC-ответов. Это типичная ситуация.
Например: запрашиваем Инфо о клиенте SNAC(15,2) [подтип запроса B204].
В ответ получим сразу восемь SNAC-ответов.
Вот их краткие названия-описания:
main-home-info
homepage-more-info
more-email-info
additional-info
work-info
about
personal-interests
past-background
Все полученные данные теперь сохраняются в файле .dat
Взято с https://delphiworld.narod.ru