На главную : Статьи : Форум : Блог : Связаться с автором

Intranet Chat — Описание протокола

Данная статья содержит описание протокола Intranet Chat и призвана облегчить труд разработчиков програмного обеспечения для этого приложения. Вся информация относительно протокола получена из открытых источников, а также с помощью анализа сетевого трафика — никакого дизассемблирования программы не проводилось.

  1. Введение
  2. Формат сообщения-«обёртки»
  3. Формат «внутреннего» сообщения
  4. «Внутренние» сообщения и их параметры
  5. Жизненный цикл клиента Intranet Chat
  6. Заключение

Введение

Во время разработки продуктов для Intranet Chat (он же "ichat") автор статьи столкнулся с проблемой — отсутствие поддержки. Я думаю многим известно, что автор популярного в домашних сетях чата — Ворожун Александр — по всей видимости давно потерял интерес к своему детищу. По крайней мере именно такое впечатление складывается у пользователей при попытках получить какую-либо помощь или же ответ на вопрос по работе программы. Естественно, это значительным образом затрудняет разработку и не способствует развитию и улучшению приложения. Столкнувшись с этой проблемой, стороннему разработчику не остаётся иного выбора, кроме как обратиться к «открытым источникам».

На данный момент в интернете доступны несколько описаний протокола, но в виду явно вредительской направленности сайтов, содержащих данные публикации, автор не считает возможным на них ссылаться. Более того, эти описания не являются исчерпывающими, поэтому даже если вы знакомы с данными публикациями, в этой статье вы всё равно можете почерпнуть нечто новое. Не исключено также, что в данном описании могут быть допущены неточности. Если вы обнаружили неточность - пожалуйста, сообщите автору. По мере возможностей статья дополняется ссылками на разработанные классы из API и комментариями по их использованию.

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

Формат сообщения-«обёртки»

В Intranet Chat формат сообщений, отсылаемых от клиента серверу отличается от формата сообщений, отправляемых от сервера клиенту. Сообщения, которыми обменивается клиент и сервер — это своего рода «обёртка», транспорт для зашифрованных сообщений. Формат «вложенного» сообщения (после расшифровки) также отличается от формата «обёртки».

Формат сообщения, передающегося от клиента к серверу:
[Длина сообщения] [0x00] [Отправитель] [0x00] [Команда] [0x00] [Получатель | "*"] [0x00] [Сообщение]

Длина сообщения — количество байт, начиная с первого байта команды до завершения пакета.
0x00 — разделитель параметров (символ с кодом 0).
Отправитель — строка, определяющая отправителя сообщения. Строка должна иметь вид IP-ADDRESS[/NETBIOS-NAME][/LOGIN].

IP-ADDRESS — текстовое представление IP-адреса отправителя.
NETBIOS-NAME — нетбиос-имя компьютера-отправителя.
LOGIN — имя пользователя, под аккаунтом которого работает отправитель.
Поля NETBIOS-NAME и LOGIN могут не заполняться.
(В дальнейшем мы будем называть строки такого вида IDENT). В 
IChatAPI для представления IDENT’а используется класс IChatSender.

Команда — команда серверу, в данном случае (для обёртки) всегда FORWARD. Получатель — IDENT получателя сообщения или же "*" (звёздочка) в случае, если сообщение адресовано всем.
Сообщение — зашифрованное сообщение или пакет сообщений.

Формат сообщения, отправляемого сервером клиенту:

[Длина сообщения] [0x00] [Команда] [0x00] [Сообщение]

Значение параметров точно такое же как и для сообщения, отправляемого от клиента серверу (см. выше). Значение поля команда так же равно FORWARD (для обёртки).

В IChatAPI клиентское и серверное сообщение-обёртка представляются различными классами. Сообщение, передаваемое клиентом серверу представляется классом IChatForwardMessage, в то время как сообщение от сервера клиенту — классом IChatServerForwardMessage. Оба класса «знают» своё назначение у «умеют» переводить своё содержимое в байтовый массив. Шифруется же сообщение как правило с помощью фабрики сообщений (см. ниже).

Формат «внутреннего» сообщения

Сообщения-обёртки выполняют лишь транспортную функцию, сервер (в базовом исполнении) использует лишь поля отправителя и получателя. Это позволяет создавать «облегченные» сервера, которым нет необходимости знать «внутренний» формат сообщения, а также уметь что-либо расшифровывать. Однако достигаемый таким путём выигрыш в производительности даётся ценой значительных проблем с безопасностью. Так, не производя никаких действий над сообщениями, сервер не в состоянии определить различного рода «манипуляции» с сообщениями. Для обеспечения должной безопасности сервер должен брать на себя часть работы по анализу возможных угроз, как это сделано, например, в сервере Scepsis.

Основную же информацию несут «внутренние» сообщения. Общий формат такого сообщения представлен ниже:

[0x13] "ichat" [0x13] [0x13] [Счетчик ASCII] [0x13][0x13] [Отправитель] [0x13][0x13] [Команда] [0x13][0x13] [параметры команды] [0x13]

0x13 — это разделитель для «внутренней команды». Два разделителя подряд отделяют поля команды друг от друга, в то время как один разделитель означает конец команды.
Команда — имя команды (см. ниже).
Параметры команды — специфические параметры для каждой команды (см. ниже).

В IChatAPI для представления базового формата «внутреннего» сообщения используется класс IChatMessage. Класс является абстрактным, что не позволяет создавать его экземпляры непосредственно, но в то же время этот класс может быть чрезвычайно удобен для проведения базовых операций над сообщениями в тех случаях, когда конкретный тип сообщения не важен.

Для каждой конкретной команды существует свой собственный класс, например IChatBoardMessage существует для команды BOARD. У каждой команды существует два способа создания — конструктор и фабричный метод. Различие состоит в том, что конструктор принимает непосредственно список параметров, необходимых для создания сообщения, в то время как фабричный метод (newInstance(…)) принимает в качестве параметра экземпляр типа IChatParameterAccessor, который позволяет реализовать произвольный алгоритм чтения параметров (из произвольных источников — будь то массив параметров, список и т.д.).

Каждому классу также соответствует собственный визитор-интерфейс, позволяющий применять алгоритм, зависящий от конкретного типа команды к набору базовых сообщений IChatMessage с помощью метода acceptVisitor(com.web_visage.ichat.IChatMessageVisitor).

Все сообщения являются «immutable», то есть не позволяют производить непосредственное изменение своих полей. Для того чтобы «изменить» какое-либо поле, необходимо создать новый экземпляр сообщения или использовать updateXXX(..) методы (которые делают абсолютно то же самое). Такое решение позволяет избежать множество проблем, связанных с многозадачностью, хотя и создаёт некоторый перерасход памяти и может создавать (при неправильном использовании) дополнительную нагрузку на сборщик мусора.

«Внутренние» сообщения и их параметры

Список команд, их параметров и соответствующих им классов приведен ниже (формат {Команда [[0x13][0x13] параметры]}) :

  • ALERT [0x13][0x13] [текст сообщения] — алерт-сообщение (клиент может быть настроен на различные действие при получении алертов от пользователей). Класс IChatAlertMessage.

  • BOARD [0x13][0x13] [номер блока, начиная с нуля] [0x13][0x13] [текс блока] — сообщение для доски объявлений. Сообщения разбиваются на блоки по 300 (?) символов каждый. Класс IChatBoardMessage.

  • CONNECT [0x13][0x13] [Имя линии] [0x13][0x13] [логин пользователя] [0x13][0x13] [никнейм] [0x13][0x13] [не используется] [0x13][0x13] [0x13][0x13] [приветственное сообщение] [0x13][0x13] [получатель] [0x13][0x13] [версия] [0x13][0x13] [статус] — сообщение о подключении (как к общему чату, так и к линии). Поле получателя играет особую роль. При подключении клиент отправляет сообщение о подключении с полем получателя равным "*". Получив такое сообщение каждый клиент в свою очередь отправляет ответное сообщение о подключении с полем получателя, равным IDENT’у нашего клиента. Таким образом, необходимо анализировать это поле для того чтобы вовремя отправить сообщение о собственном подключении, иначе мы не попадём в список контактов новоподключившегося клиента. Класс IChatConnectMessage.

  • CREATE_LINE [0x13][0x13] [имя линии] [пароль для входа в линию] [отправитель] — создание линии. Класс IChatCreateLineMessage.

  • CREATE [0x13][0x13] [идентификатор приватной линии] [0x13][0x13] [не используется?] [0x13][0x13] [получатель] — создание личного чата. Класс IChatCreateMessage.

  • DISCONNECT [0x13][0x13] [имя линии] — выход из линии. Класс IChatDisconnectMessage.

  • ME [0x13][0x13] [сообщение] [0x13][0x13] [имя линии] [0x13][0x13] [получатель] — аналог ACTION в IRC (команда /me сообщение). Класс IChatMeMessage.

  • RECEIVED [0x13][0x13] [имя линии] [0x13][0x13] [текст подтверждения] — подтверждение получения сообщения. Класс IChatReceivedMessage.

  • REFRESH_BOARD — запрос на обновление доски объявлений. Класс IChatRefreshBoardMessage.

  • REFRESH [0x13][0x13] [имя линии] [0x13][0x13] [логин пользователя] [0x13][0x13] [никнейм] [0x13][0x13] [не используется] [0x13][0x13] [приветствие] [0x13][0x13] [получатель] [0x13][0x13] [версия] [0x13][0x13] [статус] — обновление списка контактов. Как и в случае с CONNECT, поле «получатель» несёт особую нагрузку. Клиент периодически отправляет запрос на обновление списка контактов, в этом случае поле получатель будет содержать "*" (звёздочку). В ответ на это сообщение каждый клиент, подключённый к данной линии должен отправить ответное REFRESH сообщение, но в поле «получатель» будет содержаться IDENT нашего клиента. Таким образом, опять же, нам необходимо анализировать это поле чтобы вовремя сигнализировать запрашивающему клиенту о своём присутствии на линии. Класс IChatRefreshMessage.

  • RENAME [0x13][0x13] [новый никнейм] — сообщение о смене имени пользователя. Класс IChatRenameMessage.

  • STATUS [0x13][0x13] [новый статус] [0x13][0x13] [сообщение статуса] — сообщение о смене пользователем статуса. «Сообщение статуса» — сообщение, которое будет выдано пользователю в ответ на попытку отправить личное сообщение. Класс IChatStatusMessage.

  • STATUS_REQ — запрос статуса пользователя. Класс IChatStatusReqMessage.

  • TEXT [0x13][0x13] [имя линии] [0x13][0x13] [текст сообщения] [0x13][0x13] [имя получателя (не IDENT)] — текст сообщения. Вид сообщения (общее или личное регулируется с помощью имени линии, см. ниже). Обратите внимание на то, что имя получателя в данном случае — это НЕ IDENT-строка. Это обращение, которое будет указано в теле сообщения, выводимого пользователю (в случае личного сообщения). Имя пользователя для личного сообщения может быть любым. Для публичного сообщение поле обычно содержит "*" (звёздочку). Класс IChatTextMessage.

Везде, где используется параметр «имя линии» используются следующие соглашения: для обозначения «общего чата» используется имя линии, равное «iTCniaM»; для обозначения «приватного сообщения» используется имя линии, равное «gsMTCI».

В IChatAPI сообщения редко создаются непосредственно. Рекомендуется использовать реализации IChatMessageFactory, например DefaultMessageFactory. Тогда в случае необходимости вы сможете полностью изменить алгоритм создания / шифрования сообщений, просто используя другую фабрику сообщений (именно так была решена задача поддержки видоизменённой «anti hack» версии от John Do). Фабрики сообщений ответственны за создание сообщений по параметрам, восстановлению сообщений из байт-формы, а также преобразование сообщений в байт-форму. Одним из следствий такой организации может быть возможность реализации клиента, для которого будет использоваться иной формат передачи данных (более экономичный, к примеру).

Статус сообщения представляется в виде одного символа и означает:
’0’ — обычный режим
’1’ — не беспокоить на массовые сообщения
’2’ — не беспокоить
’3’ — away
В API для представления статуса используется безопасное с точки зрения типов перечисление EnumStatus.

Жизненный цикл клиента Intranet Chat

Первое, что делает клиент ичата при подключении — как это ни парадоксально — отправляет сообщение об отключении себя от основной линии (для стандартного клиента это аналог выхода со всех линий). Делается это для того чтобы «убрать» устаревшие сведения о клиенте, которые возможно остались после «грязного» выхода (обрыва связи, повисания клиента и т.п.). Вслед за сообщением об отключении следует сообщение о подключении к основной линии чата, поле «получатель» установлено в "*" (звёздочку). Каждый клиент, получивший это сообщение, сигнализирует о своём присутствии на линии, отправляя ответное сообщение о подключении с полем «получатель», установленным в IDENT первого клиента. Получив это сообщение, клиент обновляет список контактов и сигнализирует о подключении (список подключений при старте). Также все клиенты, создававшие линии, отправляют сообщения о создании линии вновь подключившемуся клиенту.

После этого можно условно считать фазу подключения завершённой (на самом деле чётких временных рамок этой фазы, насколько нам извезнтно, не установлено). В дальнейшем, клиент периодически отправляет запрос на обновление списка контактов, адресуя его всем пользователям и устанавливая поле «получатель» в "*". Каждый клиент, получивший такой запрос, сигнализирует о своём нахождении на всех линиях, отправляя для каждой линии ответный рефреш-пакет с именем линии и полем «получатель», установленным в значение IDENT-строки первого клиента. Не получив ни одного рефреш-ответа в течении какого-то времени (около трёх минут), пользователь удаляется из списка контактов с выводом сообщения о выходе пользователя. В случае, если клиент получает рефреш ответ после того как пользователь был удалён из списка, пользователь добавляется в список контактов, сообщение о подключении не выводится.

Сообщения от пользователя, не находящегося в списке контактов, стандартным клиентом игнорируются.

Получив личное сообщение, пользователь отправляет ответ-подтверждение. Если в ответ на личное сообщение ответа не получено, это может означать, что клиент более недоступен и в течении трёх минут будет удалён из списка контактов (при отсутствии ответов на рефреш-запросы). Это может также означать наличие нестандартного клиента, который просто-напросто не генерирует ответных подтверждений.

В ответ на запрос статуса клиент отправляет запрашивающему свой статус и строку авто-ответа.

При создании линии клиент высылает всем пользователям имя линии и пароль. Каждый клиент должен сам ограничивать возможность присоединения пользователя к линии.

Для обновления списка контактов не-общей линии клиент высылает REFRESH-запрос с полем «имя линии» равным имени линии (в отличие от обновления списка контактов для общего чата, где это поле равно "*") и полем «получатель», также равным "*".

При создании личного чата клиент отправляет команду CREATE с именем линии, равной строке из семи произвольных цифр. Вслед за этим он отправляет команду CONNECT для присоединения к вновь созданной «линии». Отвечающая сторона также отправляет соответствующий CONNECT-пакет для подключения. Обновление приватного чата осуществляется так же как и обновление для любой другой (не общей) линии.

При выходе из чата клиент отправляет сообщение о выходе из общей линии и закрывает соединение. При получении этого сообщения клиенты удаляют отправителя из списка контактов.

Заключение

Автор надеется, что данная статья хотя бы немного облегчила понимание работы Intranet Chat и пролила немного света на использование IChatAPI для создания приложений под ичат. Большинство из описанного в жизненном цикле является обобщением собственных наблюдений, а также результатом анализа поведения собственных приложений, таких как iRCha, Scepsis и др. Некоторые из этих наблюдений, как нам кажется, не вполне очевидны и могут сберечь время разработчикам, которые только приступают к освоению Intranet Chat. Если у вас возникли какие либо пожелания, уточнения или комментарии — милости просим на наш форум.

© 2006-2007 — Авторство и copyright на все материалы — Константин Батурицкий
На главную : Статьи : Форум : Блог : Связаться с автором