IRC по-нашемуSpider_NET Хакер, номер #110, стр. 110-112-1 Создаем правильный IRC-клиент без использования чужих наработокУже несколько статей подряд я знакомлю тебя с практикой программирования сетевых приложений с помощью WinSock API. Мы уже закодили несколько сетевых тулз: FTP-клиент, мыльницу, затронули программирование серверных приложений (написали свой proxy-сервер). И вот сегодня я решил рассказать тебе, как можно самостоятельно написать IRC-клиент. Конечно же, делать мы его будем вовсе не для того, чтобы он заменил собой mIRC. Мало ли, с какой (???) целью хакерская программа должна висеть на канале? Что-то раздавать, с кем-то бороться, какие-то распределенные делишки прокручивать… Курим протокол IRCПротокол IRC документирован в RFC под номером 1459. Для написания полноценного IRC-клиента я рекомендую тебе прочитать этот документ от корки до корки. Полная поддержка протокола твоей программой заслужит уважения у бывалых пользователей IRC. ПодключаемсяДля подключения к IRC-серверу нам необходимо знать адрес сервера и порт. IRC-сервер обычно прослушивает несколько портов, а выбор конкретного порта зависит от кодировки. Например, если ты хочешь юзать Windows 1251, то в большинстве случаев нужно будет выбирать порт 6667. Следующим шагом после установки соединения с сервером будет отправление информации о себе с помощью команд NICK и USER. Синтаксис их выглядит так: NICK твой ник USER параметры В качестве параметров могут быть: логин, название хоста, название сервера, реальное имя. Сразу рассмотрим реальный пример. После соединения с IRC-сервером irc.dal.net отправляем примерно следующее: NICK Spider_NET USER "inbox.ru (можно указать свой IP или любой хост)" "irc.dal.net": Antonov Igor Если в переданных командах не было ошибок, сервер нас поприветствует и выплеснет на наши головы ушат всякой полезной и бесполезной информации. Пример такой инфы представлен на рисунке. На скрине ты видишь уже отформатированные сообщения. В чистом виде все сообщения содержат дополнительную информацию: префикс, код сообщения и т.д. Префиксом сообщений является знак двоеточия «:». Кодов сообщений достаточно много, поэтому я расскажу лишь о тех, которые необходимы для написания рабочего примера. Описание остальных ты можешь взять из RFC, который ждет тебе на нашем DVD. Не все сообщения от сервера содержат цифровые коды. Некоторые сообщения сервер шлет в виде «<ник>~<хост><команда><параметры>». Чтобы правильно обработать такие сообщения, тебе необходимо знать базовые команды. Наиболее часто используемые я перечислил в табличке с одноименным названием. WinSock APIВ прошлой статье (о написании proxy-сервера) мы использовали сокеты в блокирующем режиме. Как ты уже знаешь, в этом случае приложение при выполнении какой-либо сетевой функции замирает и не реагирует на действия пользователя. В тот раз я избавился от блокировок путем вынесения кода в отдельные потоки. Сегодня можно было поступить таким же образом, но я хочу показать тебе, как работать с сокетами, используя событийную модель Windows. Такой способ избавляет от создания потоков и делает код более читабельным (при написании простых клиентских приложений это идеальный вариант). В реализации событий для сетевых функций Windows нет ничего сложного. Для работы с событиями в наборе WinSock API есть функция WSAAsyncSelect, которая описывается следующим образом. function WSAAsyncSelect (s:TSocket; HWindow: HWND; wMSG: u_int; lEvent: longInt):Integer; stdcalll; Функция принимает четыре параметра: 1) s – сокет, события которого мы будем мониторить; 2) hWindow – окно, которое будет принимать события; 3) wMSG – сообщение, которое нужно генерировать. 4) lEvent – список событий, которые необходимо мониторить. С первыми двумя параметрами я думаю все ясно, а вот третий и четвертый требуют отдельного пояснения. Итак, в третьем параметре необходимо определить сообщение, которое будет отправляться указанному во втором параметре окну при возникновении любого события, определенного в четвертом параметре. Сообщение может иметь любое имя, а значение должно быть WM_USER+1 – это свободное число, которым можно обозначить новое сообщение. Конечно, вместо WM_USER+1 можно указать любое число, но не факт, что в ОС уже не используется сообщение с таким номером. Итак, сообщение, на которое наше приложение будет реагировать, определено. Но в каких случаях его нужно посылать окну? Вот эти самые случаи и определяются в четвертом параметре. В нем нам нужно перечислить список все интересующих нас событий. В качестве значений могут быть:
function socket (af:integer; type:integer; protocol:integer):TSocket, stdcall; Перед тем как соединиться с удаленным узлом, необходимо создать сокет. Для этого нужно использовать функцию socket(). Входных параметров три: 1) af – семейство протоколов; нам потребуется лишь TCP, поэтому будем указывать AF_INET; 2) type – тип создаваемого сокета, может быть Sock_stream (для протокола TCP/IP) и sock_dgram (UDP); 3) protocol – протокол, для TCP нужно указать IPPROTO_TCP. Результатом выполнения будет новый сокет. Создав сокет, можно пробовать подключиться. Для этого в библиотеке реализована функция Connect. function Connect (S:TSocket; var name:TSockAddr; namelen:integer):Integer:stdcall; Параметрами для функции служат: 1) s – socket, созданный функцией socket; 2) name – структура SockAddr, содержащая данные, необходимые для подключения (протокол, адрес удаленного компьютера, порт); 3) namelen – размер структуры типа TSockAddr. Структура TSockAddr выглядит так: TSockAddrIN = sockaddr_in; SockAddr_in = record sin_family: u_short; //семейство протоколов sin_port: u_short; //порт, с которым нужно будет установить соединение sin_addr: TInAddr; //структура, в которой записана информация об адресе удаленного компьютера sin_zero: array[0..7] of Char; //совмещение по длине структуры sockaddr_in с sockaddr и наоборот. emd; Чтение и отправка данных удаленной стороне осуществляется с помощью функций send и recv. Они описаны следующим образом: function send (s:TSocket, var Buf; len:integer; flags:integer):Integer;stdcall; function recv (s:TSocket, var Buf; len:integer; flags:integer):Integer;stdcall; Параметры для обеих функций одинаковые: 1) s – сокет, на который нужно отправить (или принять) данные; 2) buf – буфер с данными для отправки (приема); 3) len – размер передаваемых (принимаемых) данных; 4) flags – флаги, отвечающие за метод отправки. Выполнившись, функция вернет фактическое количество отправленных/принятых байт. function CloseSocket(s:TSocket):integer;stdcall; Функция служит для закрытия сокета, переданного в качестве одного единственного параметра. Кодим IRC-клиентДля начала давай сварганим интерфейс будущей программы. Накидай на форму следующие компоненты:
Слепи из этих компонентов интерфейс на свой вкус. Мой вариант можешь увидеть на рисунке. Дизайн готов, а раз так, то пора переходить к самому интересному – к программированию. Для начала научимся соединяться с сервером. Создай обработчик события OnClick для кнопки «Подключить» и напиши в нем всего лишь одну строчку: ConnectToIrc(ServerEdit.Text, StrToInt(PortEdit.Text)); Процедуре ConnectToIrc нужно передать два параметра: адрес IRC-сервера и порт. Эти данные вводятся пользователем в соответствующие поля ввода. Процедура ConnectToIrc() нигде не описана, поэтому придется ее описать самостоятельно. Код процедуры приведен чуть ниже. Начинай переписывать код, а я расскажу, что в нем происходит. _Client := Socket(PF_INET, SOCK_STREAM, IPPROTO_IP); if (_Client) = INVALID_SOCKET then Exit; _client_addr.sin_family := AF_INET; _client_addr.sin_addr.S_addr := htonl (INADDR_ANY); _client_addr.sin_port := htons (port); _client_addr.sin_addr := lookupname(server); Connect(_Client, _client_addr, sizeof(_client_addr)); WSAAsyncSelect(_client, handle, WM_MYMESSAGE, FD_READ+FD_CLOSE); SendToSocket(_client, 'NICK '+ NickEdit.Text+'.'+#10); SendToSocket(_client, 'USER '+Copy(EmailEdit.Text, 1, pos('@', EmailEdit.Text)-1)+ ' "'+Copy(EmaiLEdit.Text, pos('@', EmailEdit.Text)+1, length(EmailEdit.Text))+ '" "'+server+'" : '+NickEdit.Text+#10); Первое, что необходимо сделать для соединения с удаленным сервером, – это создать сокет. Делается с помощью функции socket(). Мы уже неоднократно ее использовали, поэтому пояснять я не буду. Успешно создав сокет, нужно заполнить структуру типа sockaddr_in, которая содержит данные, необходимые для подключения (семейство протоколов, порт, адрес). После заполнения структуры вызываем функцию Connect(), а после установки соединения можно будет приступить к слежке за сетевыми событиями, возникающими на нашем сокете. Для этого я вызываю уже известную тебе функцию WSAAsyncSelect. В качестве окна (второй параметр), которое будет принимать сообщения, я устанавливаю handle (то есть нашу форму?), в третьем параметре указываю сообщение WM_MYMESSAGE (константа), а события (четвертый параметр) определяю как FD_READ+FD_CLOSE. Соединение установлено, мониторинг событий включен – пора отправлять серверу первые команды. Для отправки любого текста я использую самописную процедуру SendToSocket(). Я не буду приводить ее код, поскольку в нем нет ничего сложного, да и на случай непредвиденных ситуации в твоем распоряжении наш DVD, где есть полный исходник. Лучше я расскажу о процессе обработки сообщения WM_MYMESSAGE. Для этого в разделе private я объявил новую процедуру следующим образом: procedure MyMessage(var M:TMessage); message WM_MYMESSAGE; Эта процедура будет вызываться каждый раз, когда форма будет получать указанное после ключевого слова message сообщение. В теле процедуры мы должны проверять переменную LParam объекта TMessage. Проверка выглядит так: case M.LParam of FD_READ: readFromSocket(M.WParam); FD_CLOSE: CloseSocket(M.WParam); end; Если LParam равен FD_READ, это значит, что на сокет пришли данные и можно начинать их читать. За чтение у меня отвечает процедура ReadFromSocket. В качестве единственного параметра ей необходимо передать сокет, из которого надо читать данные. Сокет, на котором сработало событие, находится в переменной WParam объекта TMessage. Получив данные, их нужно разобрать. Для разбора я написал простенькую процедуру ParseCommand(s:string) (смотри одноименную врезку). Вся процедура состоит из одного сплошного IF. Например, если полученный текст содержит строку «PRIVMSG», это значит, кто-то из пользователей отправил сообщение, поэтому нам ничего не остается, кроме как попытаться определить отправителя. Изначально текст от сервера выглядит примерно так: «:Spider_NET!~spider_ne@ws.clnt.kht.ru PRIVMSG #xakep.ru :TEST#$D». Если приглядеться, то в глаза сразу бросается ник, адрес хоста, команда, канал и сам текст сообщения. Ну а раз мы знаем формат, в котором представлены данные, то нет никой сложности в том, чтобы их разрезать и представить в нужном нам виде. Например, выделение из этой строки ника происходит так: _senderNick := сopy(s, 2, pos('!', s)-2); После выполнения кода в переменную _senderNick будет помещено значение "Spider_NET". Текст сообщения выдирается аналогичным способом. Для закрепления давай рассмотрим еще одну ситуацию. Мы зашли на канал и для нормального общения нам нужно получить список присутствующих на нем людей. Из RFC мы знаем, что список пользователей, присутствующих на канале, передается в сообщении с кодом 353. Но когда же сервер посылает подобные сообщения? Ответ прост: сразу после выполнения команды JOIN, иначе говоря, как только мы зашли на канал. Все имена перечислены через пробел, поэтому для их разделения нужно запустить цикл, в котором придется постоянно копировать часть строки до следующего пробела: UsersListBox.Items.Add(Copy(s, 1, pos(' ', s)-1));) Остальные полученные от сервера команды обрабатываются тем же способом, поэтому нет смысла заострять на них внимание. Отправка сообщенийДавай взглянем на процесс отправки сообщений. Ты уже должен знать, что все сообщения отправляются с помощью команды PRIVMSG, поэтому нам нужно склеить с этой командой текст, который мы хотим отправить, и передать его в функцию SendToSocket(): SendToSocket(_client, 'PRIVMSG '+ChannelEdit.Text+' :'+MessageEdit.Text+#10); Тестируем приложениеОсновной код мы разобрали, а значит, можно попытаться скомпилировать наш проект и провести тест-драйв. Если после переписывания листингов твой проект запестрил ошибками, то бери исходник с нашего диска и внимательно сверяй. Для теста я попробовал подключиться к серверу irc.dal.net и зайти на незарегистрированный канал #fortest, где меня уже ждал мой приятель xDimon. Результат тестирования можешь увидеть на рисунках. КонецПростейший пример IRC-клиента готов, а значит, мне остается лишь откланяться и пожелать тебе удачи в написании полноценного конкурента mIRC или совершенно негласного конкурента какому-нибудь боту. Я уверен, что у тебя получится! Реализуй все возможности протокола, добавь все необходимые проверки, сделай красивый многооконный интерфейс и начинай радовать мир своим шедевром. Возможно, именно ты создашь новый лучший IRC-клиент. Удачи! Парсим полученные данныеProcedure ParseCommand(s:string) var _command:string; _senderNick:string; _senderMess:string; _user:string; i:integer; begin if (pos('PRIVMSG', s)>0) then begin _senderNick := Copy(s, 2, pos('!', s)-2); Delete(s, 1, 1); _senderMess := COpy(s, pos(':', s)+1, length(s)); LogMemo.Lines.Add('<'+_SenderNick+'> '+_SenderMess); Exit; End; if (pos('NOTICE', s)>0) then begin FormatText(copy(s, pos('***', s), length(s)-2)); Exit; End; _command := copy(s, Pos(' ', s)+1, 3); if (_command = '001') or (_command = '002') or (_command = '003') or (_command = '004') or (_command = '005') then begin Delete(s, 1, pos(_command, s)); FormatText(copy(s, pos(':', s)+1, length(s))); Exit; end; if (_command = '251') or (_command = '252') or (_command = '254') or (_command = '255') or (_command = '265') or (_command = '266') or (_command = '372') then begin Delete(s, 1, pos(_command, s)); FormatText(copy(s, pos(':', s)+1, length(s))); Exit; end; if (_command = '353') then begin Delete(s, 1, pos(_command, s)); Delete(s, 1, pos(':', s)); while pos(' ', s)>0 do begin UsersListBox.Items.Add(Copy(s, 1, pos(' ', s)-1)); Delete(s, 1, pos(' ', s)); end; Exit; end; if (pos('JOIN', s)>0) then begin _user := Copy(s, 2, pos('!', s)-2); if _user = NickEdit.Text then Exit; LogMemo.Lines.add('К нам присоединился: '+_user); UsersListBox.Items.Add(_user); Exit; end; if (pos('PART', s)>0) then begin _user := Copy(s, 2, pos('!', s)-2); if _user = NickEdit.Text then Exit; logMemo.Lines.add('Пользователь: '+_user+' покинул канал'); for I:=0 to UsersListBox.Items.Count-1 do if (_user=UsersListBox.Items.Strings[i]) then UsersListBox.Items.Delete(i); Exit; end; История IRCВ 1988 году финский студент Jarkko Oikarine представил общественности свою разработку – IRC (Internet Relay Chat), предназначенную для общения в реальном времени. В этом же году появился первый IRC-сервер – tolsun.oulu.fi. С этого времени популярность IRC начинает расти, и через небольшой промежуток времени весь мир просто влюбляется это удобное средство виртуального общения. |