# ATELIER PROFESSIONNEL DELPHI [OCT-NOV 2003] --- SUJET D'EXERCICE # # 30/10 : RESEAU, PROTOCOLES PERSONNALISES # ---------------------------------------------------------------------------- MODALITES DE RENDU ---------------------------------------------------------------------------- - Répertoires : ~/rendu/ap/delphi/network/server ~/rendu/ap/delphi/network/client - Fichiers : Fichiers .dpr, .res, .dof, .cfg, .opt, .pas, .dfm --> NE RENDEZ PAS les fichiers .exe, .dcu, .~* et .dsk - Droits : 700 pour les répertoires, 600 pour les fichiers - Date : lundi 03/11/2003 à 08:30 ---------------------------------------------------------------------------- Conseils ---------------------------------------------------------------------------- - Prenez le temps de lire *tout* le sujet avant de commencer. ---------------------------------------------------------------------------- Description générale ---------------------------------------------------------------------------- Vous allez implémenter un micro-IRC. Un premier programme, le serveur, gère les différents canaux et la liste des clients. Un canal est créé à sa première jonction (demande de jonction par un client). On ne gère pas la notion d'opérateur. Chaque canal maintient une liste des clients connectés dessus. Le serveur maintient par ailleurs la liste de tous ses clients. Un second programme, le client, se connecte à un serveur et peut alors effectuer des demandes de jonction sur canal et des demandes de query ( canaux privés sur personnes distantes). Chaque emplacement de discussion ( canal, query) dispose de sa propre fenêtre non modale (MDI ou SDI, à votre choix. Le MDI me semble néanmoins plus approprié). ---------------------------------------------------------------------------- Le serveur ---------------------------------------------------------------------------- - Nom du projet : MIServer - Répertoire : ~/rendu/ap/delphi/network/server Le serveur écoute sur le port 42123. La connexion ne suffit pas à l'enregistrement du client. Une fois connecté, le client dispose des commandes suivantes auprès du serveur : NICK user_name [full_name] LIST WHO [channel_name] WHOIS user_name JOIN channel_name MSG channel_name message LEAVE channel_name PRIV user_name message QUIT [greeting] Ces commandes sont détaillées ci-dessous. Connexion et authentification : NICK - - - - - - - - - - - - - - - - - - - A la connexion, le serveur envoie un Greeting de code 200 qui est du style qui suit (codes 200 omis volontairement ci-dessous) : Welcome on the Micro IRC Server v.0.1! Please identify with a NICK command so you can start use our services. Une connexion n'est pas enregistrée directement dans la liste des utilisateurs : elle démarre "anonyme" et le restera tant qu'elle n'aura pas fourni un nick valide, c'est-à-dire n'existant pas dans la liste des utilisateurs actuellement identifiés. Pour s'identifier, un client envoie une commande NICK avec un nickname à la suite, et optionnellement un nom complet derrière. Ces nick et nom peuvent être n'importe quel texte non vide. Il sont URL-encodés. Un nick DOIT commencer par un caractère alphabétique non accentué (A-Z a-z). Le serveur refuse tout nick ne respectant pas cette contrainte avec un 403 Forbidden nick start. Si le nick n'existe pas encore, on le stocke en association avec le TIdPeerThread de la connexion client, et on renvoie un 200 Identified. S'il est déjà pris *et que ce n'est pas le nôtre (en cas de changement de nick), on renvoie un 300 Nick in use. S'il est déjà pris et que c'est le nôtre, on renvoie un 200 Identified après avoir mis à jour le nom complet (à vide s'il n'a pas été fourni, ou à la valeur fournie). Une connexion n'est listée par les commandes idoines (WHO, WHOIS...) qu'à partir du moment où elle a un nick associé sur le serveur. TOUTES LES AUTRES COMMANDES (à l'exception de QUIT) REQUIERENT UN ETAT IDENTIFIE. Si elles sont invoquées par une connexion non identifiée, elles renvoient un 401 Unidentified. Exemple de début de session difficile :-) : > NICK toto < 300 Nick in use. > NICK @toto < 403 Forbidden nick syntax. > LIST < 401 Unidentified. > NICK #tdd < 403 Forbidden nick syntax. > NICK tdd < 200 Identified. > NICK amir < 300 Nick in use. > NICK tdd Christophe%20Porteneuve < 200 Identified. Listage des canaux : LIST - - - - - - - - - - - - - Le client peut demander la liste des canaux existants avec cette commande sans paramètre. Le retour est fourni par réponse RFC de code 200, à raison d'une ligne par canal. S'il n'existe pas de canal, une réponse 404 No channel. est renvoyée. Voici deux exemples, l'un avec 3 canaux et le second sans canal : > LIST < 200-#insia < 200-#delphi < 200 #irc > LIST < 404 No channel. Un nom de canal commence nécessairement par un # et ne peut être constitué que des caractères A-Z a-z 0-9 et _ (underscore). Le premier caractère après le # ne peut pas être un underscore. Par conséquent, le nom n'est pas URL-encodé. Listage des utilisateurs : WHO - - - - - - - - - - - - - - - - Envoyée sans argument, elle liste tous les utilisateurs du serveur. Avec un argument, elle vérifie qu'il s'agit du nom d'un canal existant et renvoie les utilisateurs sur ce canal. Les codes de retour possibles sont illustrés dans les exemples ci-dessous : > WHO < 401 Unidentified. > NICK tdd < 200 Identified. > WHO < 200-amir < 200-iria < 200-meshak < 200 tdd > LIST < 200-#insia < 200 #delphi > WHO #insia < 200-amir < 200-iria < 200 meshak > WHO #toto < 404 No such channel. La liste n'est pas forcément triée... Informations utilisateur : WHOIS - - - - - - - - - - - - - - - - - Cette commande accepte obligatoirement un argument qui est un nom d' utilisateur. Il est URL-encodé. Si le nick existe bien, on renvoie une réponse RFC de code 200 détaillant le nick, le nom complet s'il a été fourni, l'IP et le nom de la machine correspondante (si obtensible). Le nick et le nom sont bien entendu URL-encodés. Si le nick n'existe pas, on renvoie un 404 No such user. Exemples : > WHO < 200-amir < 200-iria < 200-meshak < 200 tdd > WHOIS john < 404 No such user. > WHOIS meshak < 200-meshak < 200 10.42.3.27 (Tom.insia.org) > WHOIS tdd < 200-tdd < 200-Christophe%20Porteneuve < 200 10.42.3.26 (Olive.insia.org) Opérations sur canal : JOIN, MSG, LEAVE - - - - - - - - - - - - - - - - - - - - JOIN permet de rejoindre un canal, si on est pas déjà dessus. MSG permet d'y envoyer un message public. LEAVE permet de quitter un canal (si on est bien dessus). Un nom de canal commence nécessairement par un # et ne peut être constitué que des caractères A-Z a-z 0-9 et _ (underscore). Le premier caractère après le # ne peut pas être un underscore. Par conséquent, le nom n'est pas URL-encodé. En revanche, le message envoyé avec MSG est URL-encodé, car en plus de contenir tout caractère imprimable, il peut même être multi-lignes. JOIN crée le canal à la volée s'il nexiste pas. Dans ce cas, il renvoie un 201 Channel created on the fly. Si le canal existait mais qu'on y était pas encore, il renvoie 200 Joined channel et émet une notification auprès de tous les autres clients sur ce canal. Si on y était déjà, il renvoie un 202 Already on this channel. Aspect de la notification : *** TDD joined the channel. Si le nom du canal est invalide, JOIN refuse la création et renvoie un 403 Forbidden channel name syntax. Lorsqu'un client est sur un canal, il reçoit du serveur tous les messages envoyés sur ce canal, à l'aide de commandes MSG issues du serveur, comme détaillé plus loin dans ce document. LEAVE "désinscrit" le client du canal. Il ne reçoit plus les messages qui y sont émis. S'il n'était pas sur le canal, on renvoie un 400 Not on channel. S'il y était, on renvoie un 200 Left channel et on émet une notification auprès de tous les autres clients sur ce canal. Si le canal n'existe pas, on renvoie un 404 No such channel. Aspect de la notification : *** TDD left the channel. Exemples : > LIST < 200-#insia < 200-#delphi < 200 #irc > JOIN #tdd < 201 Channel created on the fly. > JOIN #tdd < 202 Already on this channel. > JOIN #insia < 200 Joined channel. > JOIN tamère < 403 Forbidden channel name syntax. > LEAVE #yomomma < 404 No such channel. > LEAVE #delphi < 400 Not on channel. > JOIN #_ta_mere < 403 Forbidden channel name syntax. > JOIN #tamère < 403 Forbidden channel name syntax. > LEAVE #insia < 200 Left channel. MSG permet d'envoyer un message sur un canal qu'on a rejoint. La commande prend deux arguments : le nom du canal (qui n'a pas besoin d' être URL-encodé) et le message (qui est URL-encodé). Si le client est bien sur le canal, on renvoie un 200 Message sent. et on transmet le message à tous les autres clients présents dessus (voir plus loin dans ce document). Le client doit donc afficher lui-même son envoi, il ne recevra pas son propre message via le serveur. Si le client n'est pas sur le canal, on renvoie un 401 Not on channel. Si le canal n'existe pas, on renvoie un 404 No such channel. Si la syntaxe du nom de canal est invalide, on renvoie un 403 Forbidden channel name syntax. Message privé ("query") : PRIV - - - - - - - - - - - - - - - - Il est possible d'envoyer un message directement à une personne, ce qu'on appelle un message privé. Pour ce faire, on utilise la commande PRIV, qui prend deux arguments : le nick destinataire et le message. Les deux sont URL-encodés. Si le nick n'existe pas, on renvoie un 404 No such user. Si le nick est le nôtre, on renvoie un 402 Schyzophrenia. Si la syntaxe est invalide, on renvoie un 403 Forbidden nick syntax. Sinon, on renvoie un 200 Private message sent. et on transmet le message privé au destinaire (voir plus bas dans ce document). Le client doit donc afficher lui-même son envoi, il ne recevra pas son propre message via le serveur. Le serveur ne maintient pas de liste des conversations privées : les clients ne l'avertissent pas de leurs "fermetures de fenêtres"... Déconnexion : QUIT - - - - - - - - - - Cette commande peut fournir un texte de greeting (ex. "see you later!"). Cette raison sera exploitée dans les notifications sur canal. Si la connexion est identifiée, elle est d'abord supprimée de la liste des associations nicknames/threads. Le gestionnaire de commande est configuré pour un retour 200 Bye. et une déconnexion automatique à complétion du traitement de la commande. Tout canal sur lequel se trouvait le client au moment de sa déconnexion est averti de celle-ci par l'envoi d'un message système sur le canal du type : *** TDD quit the server (greeting). ---------------------------------------------------------------------------- Le client ---------------------------------------------------------------------------- - Nom du projet : MIClient - Répertoire : ~/rendu/ap/delphi/network/client Le client se présente sous forme d'une fenêtre principale, qui contient l'affichage dit "de statut", au sein duquel s'affichent les messages système (ex. greeting du serveur, NICK déjà pris, réponse à un LIST, WHO, WHOIS...). Sous la zone de statut, une zone de saisie mono-ligne permet d'envoyer des commandes. Si vous optez pour le MDI, la fenêtre de statut sera une fenêtre fille qui refuse la fermeture, ouverte au démarrage. Elle est juste minimisable. Le client écoute les messages "spontanés" du serveur (messages des autres utilisateurs) sur le port 42124. Il vérifie systématiquement que l'envoyeur a la même IP que son serveur, sans quoi il envoie un refus (voir plus loin). Connexion et authentification - - - - - - - - - - - - - - - Le client permettra la connexion uniquement une fois ces deux informations remplies par l'utilisateur : 1) adresse/nom du serveur et 2) nick. La connexion est alors immédiatement suivie par un envoi de commande NICK. Si celle-ci échoue, le client demande en boucle, au travers d'une boîte de saisie par exemple (cf. InputQuery), un nick à l'utilisateur. A chaque itération, le message d'erreur renvoyé par le serveur apparaît dans la vue de statut. Si l'utilisateur annule sa saisie, le client déconnecte avec un envoi de commande QUIT. Si un nick valide est finalement saisi, l'utilisateur peut exploiter l'interface normalement. Son nick apparaît à côté du titre de la fenêtre principale, entre crochets, avec le nom du serveur (ou l'IP si on n'a pas pu résoudre le nom). Exemple : "Micro IRC Client [tdd@olive]". Déconnexion, sortie du programme - - - - - - - - - - - - - - - - - La fermeture du programme s'assure, si l'utilisateur est connecté, d'envoyer un QUIT avant que la fenêtre principale ne se ferme complètement. L'utilisateur peut aussi quitter manuellement, sans fermer le programme. Voir "commandes transversales", plus bas dans ce document. Syntaxe utilisateur des commandes - - - - - - - - - - - - - - - - - Les commandes vues jusqu'ici sont au niveau protocole. Pour l' utilisateur, une commande démarre par le slash (/), et doit constituer le tout début de la saisie (à partir du 1er caractère). Un simple espace en début de saisie désactive le statut de commande (pratique pour en parler). Le mapping est à l'identique, sauf pour PRIV qui devient "/query". Attention justement aux commandes "implicites" d'un point de vue utilisateur (voir plus bas dans ce document). Commandes transversales - - - - - - - - - - - - Toutes les commandes sont exécutables de n'importe où. Le comportement du client varie en termes de création/activation de fenêtres. Employé après l'identification initiale, NICK permet de changer de nick ou de nom complet au fil du temps. Le retour serveur est affiché dans la fenêtre de statut. JOIN permet de rejoindre un canal. Si on y était pas déjà, une nouvelle fenêtre est créée pour ce canal, qui en porte le nom comme titre. Si on y était déjà, on réutilise la même fenêtre. Le seul message de retour affiché par le client est le code 202 Already on this channel, qui apparaît dans la fenêtre du canal. Dans tous les cas, la fenêtre du canal devient active (passe au premier plan, si vous préférez). LEAVE permet de quitter un canal. Si on y était, la fenêtre correspondante est fermée (et donc détruite). En-dehors du retour 200 Left channel, tous les autres retours serveur sont affichés dans la fenêtre de statut. PRIV permet d'envoyer des messages privés. Si on a pas encore (ou plus) de fenêtre pour la conversation privée avec le destinataire, on crée cette fenêtre à la volée. Son titre est le nom du destinataire. Créée à la volée ou non, elle devient active (passe au premier plan). On y affiche notre envoi nous-mêmes (le serveur ne nous le renverra pas). En- dehors du retour 200 Private message sent, tous les autres retours serveur sont affichés dans la fenêtre de statut. WHO affiche son listing dans la fenêtre de statut, où qu'on le tape. WHOIS affiche son résultat dans la fenêtre de statut, où qu'on le tape, A L'EXCEPTION d'une frappe depuis la fenêtre de query : il s'affiche alors dans la fenêtre courante. Commandes implicites - - - - - - - - - - - Dans la zone de saisie d'une fenêtre de canal, si le texte saisi ne démarre pas par une commande, on suppose un /msg sur le canal courant. Dans la zone de saisie d'une fenêtre de canal, taper simplement /who suppose le canal courant. Il faut le taper dans la fenêtre de statut pour obtenir un WHO global du serveur. Dans la zone de saisie d'une fenêtre de query, si le texte saisi ne démarre pas par une commande, on suppose un /query sur l'utilisateur destinataire de la fenêtre courante. Dans la zone de saisie d'une fenêtre de query, /whois suppose l'utilisateur destinataire de la fenêtre courante. Messages entrants : MSG, PRIV - - - - - - - - - - - - - - - Le serveur envoie des messages spontanés aux clients pour leur transmettre des notifications (SMSG, voir plus bas) et des messages publics ou privés. Il les envoie au moyen des commandes SMSG, MSG et PRIV gérées côté client (numéro de port précisé en haut de ce chapitre). Le format de MSG est : MSG [channel_name] message Donc strictement identique au format dans le sens inverse. On ajoute simplement le message URL-décodé dans la fenêtre de canal, sans pour autant l'amener expressément au premier plan. Le format de PRIV est : PRIV [nick_source] message Donc la symétrique du format dans le sens inverse. Si on a pas encore de fenêtre de conversation privée pour l'envoyeur, on en crée une à la volée, elle passe alors au premier plan. Sinon on la laisse dans son état (minimisé, non active...). On ajoute simplement le message URL- décodé. Messages système : SMSG - - - - - - - - - - - - Le serveur peut émettre à tout moment des messages dits système, qui sont des notifications (déconnexion, arrivée sur canal, départ du canal). Le texte de ces notifications est transmis URL-encodé sur le réseau, sans préfixe distinctif (genre "***"). Le client affiche ces notifications avec un préfixe particulier (justement, généralement un triple-astérisque plus un espace : "*** le_message_ici"). Ces notifications sont soit générales, soit spécifiques à un canal. Le format de l'envoi est : SMSG [channel_name] message Si on détecte deux paramètres, on récupère le nom du canal, on vérifie qu'on a une fenêtre pour ce canal, et on y envoie le message correctement formaté. Si on n'a qu'un paramètre, on affiche le message sur la fenêtre de statut. En aucun cas, vous ne répondez au serveur suite à un SMSG. Gestion des fenêtres - - - - - - - - - - - Si vous gérez les fenêtres en MDI, faites un menu principal pour la fenêtre parent, un menu Windows dans la barre et utilisez la propriété WindowMenu de la fiche pour lui faire contenir dynamiquement la liste des fenêtres filles. Ajoutez-y aussi les options Tile, Cascade et Arrange Icons. Vous n'aurez aucun mal à trouver les méthodes qui font ça... Un Close All pourrait être bien aussi (rappel : la fenêtre de statut ne se ferme jamais). Un "Auto-Tile" qui refait un Tile quand une fenêtre s'ajoute ou se supprime, sous forme d'option cochable dans ce menu, serait un bonus. Si vous êtes en SDI, une option qui arrangerait les fenêtres à l'écran (merci SystemParametersInfo(SPI_GETWORKAREA....) et Application.Forms) serait un bonus. ---------------------------------------------------------------------------- Aspects techniques ---------------------------------------------------------------------------- - Utilisez une instance globale de TThreadList pour maintenir à jour les listes des utilisateurs et des canaux, afin de synchroniser correctement. - Utilisez des CommandHandlers pour traiter vos commandes, aussi bien côté serveur que client. Configurez-les en détail. - Pour envoyer les messages ou autres contenus potentiellement multi-lignes, utilisez TIdURL.ParamsEncode et TIdURL.URLDecode. - Les retours de type RFC sont gérables avec TIdTCPClient.LastCmdResult pour la lecture côté client, TIdCommand.Reply pour l'écriture côté serveur. --Fin du Sujet--