+-------------------------------------------------------------------+ | * Der Internet Relay Chat im Detail - erfasst mit PHP * | +-------------------------------------------------------------------+ *** Vorwort ************************************************ IRC-Clients wie mIRC, xchat oder andere kann jeder bedienen. Wir auch. Trotzdem mag es ganz interessant sein, was hinter den Kulissen abläuft, einfach um zu /sehen/, was hinter den Kulissen abläuft. ;) Der Einfachheit (und Exotik) halber wurde PHP als Werkzeug gewählt, Vorwissen wäre ganz hilfreich. Bei Unwissenheit kann aber jederzeit die Seite http://www.php.net/ bedient werden, hängt man den Funktionsnamen an, so kommt man direkt zu deren Beschreibung. Das Gelernte lässt sich übrigens ohne Probleme auf andere Sprachen übertragen, ob es nun Java, C++ oder gar Python sei. Nun denn, lasset uns zum Inhalte schreiten: *** Inhalt ************************************************ 0.) Inhalt 1.) Grundlegendes über Sockets und PHP 2.) http://www.rfc-editor.org/, dein bester Freund 3.) Ein Plan 4.) Der Schritt zur Tat 5.) Zum Schluss *** Grundlegendes über Sockets und PHP ************************************************ PHP kann mehr als man der Sprache zutrauen würde. Mittels PHPGTK ist es sogar möglich Fensteranwendungen in PHP zu entwickeln. Unser Thema ist allerdings weniger komplex, wir brauchen nur Sockets. Wer die schon mal in C verwendet hat, der wird sich jetzt schütteln und das Wort "nur" Kopfschüttelnd wiederholen, denn dort ist es ein relativ großer Aufwand, bis eine Verbindung steht. PHP aber ist einfach, und so sind auch die Sockets einfach. Wir brauchen nur folgende Funktionen: o fsockopen() [http://www.php.net/fsockopen] o fclose() [http://www.php.net/fclose] o fputs() [http://www.php.net/fputs] o fgets() [http://www.php.net/fgets] Damit haben wir alles, um eine Verbindung zu öffnen und auch was damit anzustellen. Manchen wird vielleicht auffallen, dass, außer fsockopen(), alle diese Funktionen auch für Dateioperationen gebraucht werden. Das stimmt, PHP behandelt Sockets wie Dateien. Auf finden sich noch einige Beispiele zum Gebrauch der Funktion, wem meine Ausführung nicht reicht, ist gerne eingeladen sich diese anzuschauen. Ich weiß, inzwischen jucken die Finger ganz schön und ein bischen Code wäre nett, also bitte: [CODE: Öffnen einer Verbindung zu einem IRC-Server] NULL, 'str' => NULL); $connectionHandle = fsockopen ('irc.euirc.net', 6667 , $error['no'] , $error['str'], 30); if (!$connectionHandle) // Verbindung missglückt { sprintf('Fehler: [%d] - %s', $error['no'], $error['str']); exit; } echo 'Verbindung zu irc.euirc.net:6667 geöffnet!
'; fclose ($connectionHandle); echo 'Verbindung zu irc.euirc.net:6667 beendet!'; ?> [/CODE] Das ist nicht viel und bring einem auch herzlich wenig, aber für mehr sind wir noch nicht bereit. *** http://www.rfc-editor.org/, dein bester Freund ************************************************ Damit wir nun bereit werden, surfen wir bei http://www.rfc-editor.org vorbei und schauen, was es hier zum Thema Internet Relay Chat gibt. Im großen und ganzen Interessieren uns RFC #1459 und #2812: o ftp://ftp.rfc-editor.org/in-notes/rfc1459.txt o ftp://ftp.rfc-editor.org/in-notes/rfc2812.txt Eigentlich ist #1459 obsulet, aber es gibt doch einen ganz guten Überblick über das IRC-Protokoll. #2812 hingegen ist besonders wichtig, denn es gibt uns alle Informationen, die wir für den Client brauchen. Was uns im Moment interessiert: o Befehle werden _immer_ mit einem CR-LF (chr(13).chr(10)) abgeschlossen o Befehle haben die Form: [ ":" prefix SPACE ] command [ params ] CR-LF (Bitte nachlesen: 2.3.1 Message format in Augmented BNF) o Es sieht verdammt kompliziert aus Ist es aber nicht, keine Sorge. Nehmen wir mal ein Beispiel, wir sind auf irc.arcor.de eingeloggt und plötzlich kommt folgende Nachricht: :overload!~chat@123.145-167-89.some.cool.hoster.de PRIVMSG #myChannel :moin moin! Was sagt uns das? Nun, wie oben steht, kann das anfängliche ":" ignoriert werden. Als nächstes kommt "prefix": overload!~chat@123.145-167-89.some.cool.hoster.de Was das zu bedeuten hat lernen wir später. Danach das Kommando, in unserem Fall "PRIVMSG", die Argumente sind "#myChannel :moin moin!". Manchmal kommt auch so was: PING :irc.arcor.de Diesmal ist das ":" und "prefix" ausgelassen worden und es steht sofort das Kommando, PING, da. Das Argument ist :irc.arcor.de. Was :irc.arcor.de 366 overload #myChannel :End of /NAMES list bedeutet findet ihr selber heraus ;) Wenn ihr das nun getan habt, sind wir für eine List von Befehlen bereit. Es ist nur ein Auszug, noch dazu ist er nicht genau, doch das wichtigste steht wohl dabei: Wichtig für den Login, siehe "3.1 Connection Registration": NICK USER PONG - !!!WICHTIG!!! in Verbindung mit PING (vom Server) Mehr dazu in später. QUIT[ :] - Server verlassen Bsp: QUIT :"Most lies about blondes are false." - Cincinnati Times-Star, headline Wir verlassen den Server mit der Nachricht hinter dem : JOIN [][,[],...] - Channel betreten Bsp: JOIN #myChannel,#my2ndChannel secretPassword Wir betreten #myChannel und #my2ndChannel, #my2ndChannel hat das Passwort "secretPassword" PART [ :][,[ :],...] - Channel verlassen Bsp: PART #myChannel :Ich geh schlafen... Wir verlassen den #myChannel mit der Nachticht, die unsere Intention schlafen zu gehen deutlich macht. PRIVMSG (|) : - Nachrichten senden Bsp: PRIVMSG #myChannel :Guten morgen! Wir senden die Nachricht "Guten morgen!" an alle sich im Channel befindenden Personen NOTICE (|) : - Nachrichten senden Im Prinip das gleiche wie PRIVMSG, doch: "The difference between NOTICE and PRIVMSG is that automatic replies MUST NEVER be sent in response to a NOTICE message." Zu den beiden Befehlen ganz oben in der Liste: Habt ihr euch frisch mit Server verbunden, müsst ihr euch zunächst autorisieren (authen). Das geschieht mit 2 (bzw. 3) Befehlen: NICK und USER. Zu Erst schickt ihr NICK an den Server, der euch daraufhin mitteilt, wenn etwas nicht stimmt. Bekommt ihr keine Meldung, geht es mit USER 0 * :<'Real Name'> weiter. Die Befehlsfolge lautet also für Karl Meyer: NICK karlo USER karlo 0 * :Karl Meyer Sieht einfach aus, ist es auch. Jetzt kann es eigentlich losgehen, wäre da nicht die offensichtlich von Tischtennis inspirierte Idler- verhütung. Ihr bekommt sehr bald etwas in dieser Art: PING :F2A355EB Wenn ihr nicht bald darauf den Ball mit einem lauten PONG :F2A355EB zurückschlagt, ist das Spiel verloren und die Verbindung beendet. Wer mehr Kommandos (kicken, Modes setzen, ...) haben möchte, liest sich bitte das RFC durch, oder probiert ein wenig mit Telnet rum. *** Ein Plan ************************************************ So, wir wissen nun, wie wir Messages senden und vor allem wie sie zu _uns_ kommen. Auf dieser Grundlage wissen wir, dass ein Parser vonnöten ist, im die Bestandteile einer Message aufzudröseln. Uns wird es genügen das Kommando und die Parameter zu wissen. Das Programm soll folgendes können: o Verbinden zu einem Server und einen Channel betreten o Nach dem Betreten ein "Hallo, ich bin aus PHP" in die Runde werfen o und bei den Worten "dann geh!" verschwinden. Zum Ablauf des Programms: 1) Verbinden zum Server 2) Anmelden mit NICK und USER 3) Ping - Pong ... 4) Einen Channel mit JOIN betreten 5) Mittels PRIVMSG eine Nachricht senden 6) Messages abwarten und durchgehen 7) Bei "jetzt geh!" QUIT benutzen 8) Verbindung zum Server schließen *** Der Schritt zur Tat ************************************************ Ja, mir juckte es auch schon wieder in den Fingern, und deshalb bekommt ihr jetzt endlich auch etwas kommentierten Code, den ich aber nicht auseinanderpflücken werde. Mit dem jetzigen Wissen sollte alles verständlich sein. [CODE: Der simple "Bot"]
 NULL, 'str' => NULL);
$connectionHandle = NULL;

$serverToJoin = 'irc.germany.net';
$channelToJoin = '#test';

/////////////////////////////////////////////////////////////////////

// Funktionen
function connectToServer ($server, $port)
{
	global $error;
	global $connectionHandle;
	$connectionHandle = fsockopen ($server, $port , $error['no'] , $error['str'], 50);
	return (bool)($connectionHandle);
}

function errorAndQuit ($message)
{
	global $connectionHandle;
	if ($connectionHandle)
		fclose ($connectionHandle);	
	die ('
FATAL ERROR: '.$message); } // Message + CR-LF zum Server senden function sendToServer ($string) { global $connectionHandle; if (!$connectionHandle) return false; $string .= chr(10).chr(13); $bytesWritten = fputs ($connectionHandle, $string, strlen($string)); return ($bytesWritten == strlen($string)+2); } // Message vom Server holen function getServerMessage ($len = 512) { global $connectionHandle; if (!$connectionHandle) return false; return fgets ($connectionHandle, $len); } // Message nach Maske, Kommando und Parameterparsen function parseServerMessage ($message) { $message = rtrim ($message); // CRLF abschneiden // [ ":" prefix SPACE ] command [ params ] // $mask ^ $command ^ $param list ($mask, $command, $param) = split (' ', $message, 3); if ($mask[0] != ':') // Kommando in $mask - kein Prefix vorhanden { $param = $command.$param; $command = $mask; $mask = NULL; } return array ('cmd' => $command, 'par' => $param, 'mask' => $mask); } ///////////////////////////////////////////////////////////////////// // Verbinden if (!connectToServer ($serverToJoin, 6667)) { sprintf('Fehler: [%d] - %s', $error['no'], $error['str']); exit; } echo 'Verbindung zu '.$serverToJoin.':6667 geöffnet!
'; // Autorisierung $reply = 'NICK oviBot'; if (sendToServer ($reply)) errorAndQuit ('Kann '.$reply.' nicht senden!'); else echo 'sent '.$reply.'
'; $reply = 'USER oviBot 0 * :Overload Bota'; if (sendToServer ($reply)) errorAndQuit ('Kann '.$reply.' nicht senden!'); else echo 'sent '.$reply.'
'; // MessageLoop while (!feof($connectionHandle)) { $reply = NULL; // Neues Reply erzwingen $msg = getServerMessage(); $parsedMessage = parseServerMessage ($msg); // Debuginformationen printf ('[%s] %s %s
', $parsedMessage['mask'], $parsedMessage['cmd'], $parsedMessage['par']); // auf Kommandos reagieren switch ($parsedMessage['cmd']) { case 'PING': // Pong... $reply = 'PONG '.$parsedMessage['par']; $pingNum++; break; case '376': // Für JOIN - MOTD hoert auf $reply = 'JOIN '.$channelToJoin; break; case '366': // Ende der Namensliste - wir sind im Channel $reply = 'PRIVMSG '.$channelToJoin.' :Hallo, ich bin aus PHP!'; break; case 'PRIVMSG': // überprüfen auf QUIT-Befehl list ($receiver, $message) = split (' ', $parsedMessage['par'], 2); if ($message == ':jetzt geh!') $reply = 'QUIT :Wenn mich hier keiner haben will...'; break; } // Message senden if ($reply) { if (sendToServer ($reply) == strlen($reply)) errorAndQuit ('Kann '.$reply.' nicht senden!'); else echo 'sent '.$reply.'
'; } } // Schließen if (fclose ($connectionHandle)) echo 'Verbindung zu '.$serverToJoin.':6667 beendet!'; else echo 'Verbindung zu '.$serverToJoin.':6667 NICHT beendet!'; ?>
[/CODE] *** *** Zum Schluss ************************************************ Es sei vielleicht noch anzumerken, dass PHP in /keinster Weise/ dazu geeignet ist, einen ernsthaften Bot zu programmieren. Früher oder später wird er den Pingtimeouttod sterben... Ich persönlich rate euch auch ab, einen Bot auf einem Webserver eurer Wahl laufen zu lassen, die Serverlast wird extrem hoch und ihr werdet euch über ständige Donwtimes wundern ;) Trotzdem, die Grundlegenden Dinge wurden geklärt und mit ein wenig einarbeitung in die RFCs dürfte zum Beispiel einem Quizbot auf Java- Basis nichts mehr im Wege stehen. MfG overload overload@login-club.org __ _ ________ __ / / ___ ___ (_)___ / ____/ /_ __/ /_ / / / __ \/ __ \/ / __ \/ / / / / / / __ \ / /__/ /_/ / /_/ / / / / / /___/ / /_/ / /_/ / /____/\____/\__ /_/_/ /_/\____/_/\____/_____/ / /_/ / September 2004 \____/ [ http://www.login-club.org ] [ http://board.login-club.org ] [ irc://irc.euirc.net:6667/loginclub ] Geschrieben von "overload". Weitergabe ist erwünscht, Veränderung verboten.