• Aucun résultat trouvé

1IntroductionauxsocketsavecleprotocoleDaytime TP3

N/A
N/A
Protected

Academic year: 2022

Partager "1IntroductionauxsocketsavecleprotocoleDaytime TP3"

Copied!
9
0
0

Texte intégral

(1)

TP3

1 Introduction aux sockets avec le protocole Daytime

1.1 Utilisation du service Daytime avec Telnet

Telnet est un protocole simple de connexion à distance : il permet de transmettre des caractères entre une machine locale (écran + clavier) et une machine distante1.

Par défaut, un client telnet se connecte au service TCP telnet (sur le port 23), mais il est possible de préciser le service TCP de la façon suivante :telnet M S. Dans ce cas, le client telnet se connecte au service S de la machine M.

— Quel est l’effet de la commande suivante ? telnet time.nist.gov daytime

To do(??)

On obtient simplement l’heure. 57850 17-04-07 15:40:05 50 0 0 362.1 UTC(NIST) *

Liste des serveurs daytime sur NIST : https: // tf. nist. gov/ tf-cgi/

servers. cgi.

— Décrire le protocoledaytimed’après le résultat de cette commande. Vous trouverez sur le site web de l’IETF (Internet Engineering Task Force) la spécification officielle de ce protocole, décrite dans laRFC 867.

Le client se connecte, n’a pas besoin d’envoyer de données, le serveur envoie de toutes façons la date et referme la connexion. Le client referme alors lui aussi.

http: // en. wikipedia. org/ wiki/ Daytime_ Protocol

— Il est également possible d’indiquer explicitement le numéro de port du service plutôt que le nom du service. Consultez le fichier/etc/services et cherchez avec greple numéro de port du servicedaytime. Essayez maintenant de vous connecter avec le numéro de port trouvé.

telnet time.nist.gov 13

— Avec la commande de terminal grep, retrouvez efficacement les numéros de port par défaut associés aux services (ou protocoles) suivant : http, ftp, smtp, telnet, ssh, echo.

(2)

grep daytime /etc/servicestime.nist.gov, qui est uniquement disponible en TCP. Vous remarquerez que certains protocoles, comme daytime sont à la fois disponibles avec la couche transport TCP et UDP. Ce n’est pas le cas des serveurs time.nist.gov, qui sont uniquement disponibles en TCP.

— Au CREMI, la machine teslaaccueille le service daytime en version TCP et UDP sur IPv4 et IPv6. A l’aide de la commandenc tesla 13, essayez de vous connec- ter aux 4 différentes versions de ce service : on utilisera les options -u, -4, -6.

Consultez lemanuel en ligne de cette commande pour comprendre le rôle des op- tions : man nc (faire ’q’ pour quitter le manuel). Pourquoi ne peut-on pas utiliser la commandetelnetpour faire ces expériences ? Rappelez les principales différences entre TCP et UDP. Expliquez rapidement la différence de fonctionnement du ser- vicedaytime entre TCP et UDP ?

$ nc -4 tesla 13 # TCP4

$ nc -6 tesla 13 # TCP6

$ nc -u -4 tesla 13 # UDP4

$ nc -u -6 tesla 13 # UDP6

Remarquons qu’en UDP, il n’y a pas de notion de connexion ou de session comme en TCP. Il faut donc envoyer quelque chose (même un ligne vide) en tapant sur la touche entrée pour simuler une connexion, avant de rece- voir une réponse... De même, la commande nc ne termine pas automatiquement en UDP car il n’y a pas non plus de mécanisme pour la déconnexion (fairectrl+c).

Notons que Telnet fonctionne uniquement au dessus de TCP. Par ailleurs, Telnet envoie les caractères <CR><LF> (ou "\r\n") implicitement après chaque saut de ligne, ce qui est requis par exemple pour faire des requêtes HTTP GET... Pour obtenir le même comportement avec nc il faut utiliser l’option -c !

Pour info pour plus tard : Pour forcer la terminaison d’une session Telnet, il faut faire Ctrl+Alt Gr+], puis il faut taperquit lorsque l’invite telnet>s’affiche.

1.2 Programmation d’un client Daytime avec les Socket

Considérons le programme en Python 3 ci-dessous, qui permet de se connecter au service daytime à l’aide de la bibliothèque socket, disponible sur moodle :daytime.py i m p o r t s o c k e t

s = s o c k e t . s o c k e t ( s o c k e t . AF_INET , s o c k e t . S O C K _ S T R E A M ) host = " time - c . nist . gov "

port = 13

s . c o n n e c t (( host , port ) ) data = s . recv ( 1 0 2 4 ) p r i n t( data )

(3)

$ python3 daytime.py

b’\n58902 20-02-23 20:39:36 00 0 0 515.8 UTC(NIST) * \n’

— Ouvrez votre éditeur de texte (ou environnement de programmation) préféré et recopiez le code de ce programme dans un fichierdaytime.py.

— Lancez ce programme à l’aide de la commande : python3 daytime.py

— A l’aide de la documentation (https://docs.python.org/library/socket.html), comprenez les étapes principales de ce programme et repondez aux questions sui- vantes. En particulier, expiquez le sens des constantes AF_INET et SOCK_STREAM.

Quel est le rôle de la variable s? Quelles lignes déclenchent la connexion & la déconnexion TCP/IP ? Quelle fonction sert à recevoir des données dans ce code ? Cherchez dans la documentation la fonction qui sert à envoyer des données ?

C’est la fonction send() ; la variante sendall() force l’envoi en flushant le buffer...

Il faut mieux privilégier cette dernière avec nos étudiants, ça evite des mauvaises surprises !

— Pour transformer ce programme en script exécutable, il faut commencer par ajou- ter sur la première ligne : #!/usr/bin/python3, puis il faut donner les droits d’exécution (+x) au fichierdaytime.py en tapant la commande en ligne suivante : chmod +x daytime.py. Vous pouvez maintenant lancez ce programme directe- ment : ./daytime.py

2 Socket Python : requête HTTP à la main

2.1 Echauffons-nous d’abord avec Telnet

Nous avons déjà étudié avec Wireshark ce qui se passe quand un navigateur consulte une page web, comme par exemplehttp://www.perdu.com. Revenons rapidement sur la trace capturée dans le fichier http.pcap. A l’aide de Wireshark, observez en détail dans l’en-tete HTTP la requête GET du client (trame 10).

A l’aide de la commande telnet, reproduisez la requête "HTTP GET" vers le serveur www.perdu.com en se limitant aux options Host etConnection:

$ telnet www.perdu.com 80 GET / HTTP/1.1

Host: perdu.com Connection: close

Attention, il ne faut pas oublier le double saut de ligne à la fin de la requête ! Aussi, le serveur n’est pas très patient et produit rapidement une erreur Timeout, il vaut mieux préparer le texte prêt à coller depuis un éditeur de texte.

(4)

Le protocole HTTP 1.1 impose l’utilisation du champs Host (expliquer pourquoi)...

Et par défaut la connexion TCP/IP n’est pas fermée par le serveur, ni par le client Telnet...

L’option Connection : close permet de corriger ce problème.

$ telnet www.perdu.com 80 Trying 208.97.177.124...

Connected to www.perdu.com.

Escape character is ’^]’.

GET / HTTP/1.1 Host: www.perdu.com Connection: close HTTP/1.1 200 OK

Date: Sun, 23 Feb 2020 19:57:58 GMT Server: Apache

Upgrade: h2

Connection: Upgrade

Last-Modified: Thu, 02 Jun 2016 06:01:08 GMT ETag: "cc-5344555136fe9"

Accept-Ranges: bytes Content-Length: 204 Vary: Accept-Encoding Content-Type: text/html

<html><head><title>Vous Etes Perdu ?</title></head><body><h1>Perdu sur l’Internet ?</h1><h2>Pas de panique, on va vous aider</h2><strong><pre> * <--- vous &ecirc;tes ici</pre></strong></body></html>

Connection closed by foreign host.

2.2 Programmons notre client web !

Écrivons maintenant dans le fichier httpget.py un programme Python 3 qui récupère la page d’accueil d’un site web (passé en argument), dont l’utilisation sera :

$ ./httpget.py www.perdu.com

Nous allons nous inspirer dedaytime.pyécrit précédemment. Puisque l’on veut ouvrir une connexion TCP sur le port 80, il faut adapter de la façon suivante :

— Importer le modulesysavecimport sys, afin de récupérer l’argumentsys.argv[1]

dans la variablehost.

— Mettre à jour la variable port.

— Ajoutez une variablerequestqui contient la requête HTTP (disponible sur moodle : http-request.py)

r e q u e s t = " GET / HTTP / 1 . 1 \ r \ n " \

" Host : " + host + " \ r \ n " \

(5)

— Utiliser la méthode sendall() de l’objet socket pour envoyer la requête. Atten- tion, il faut convertir la chaîne de caractères request en tableau d’octets avant de l’envoyer : request.encode("utf-8") qui retourne un tableau d’octets. Pensez bien à ajouter deux retours chariot\r\n\r\n à la fin de la requête.

— Utiliser la méthode recv() de l’object socket pour récupérer le résultat (on peut lui passer la taille 1024). Il suffit ensuite d’utiliser unprint() pour l’afficher.

— Testez votre programme avec le site web www.w3.org. Pourquoi ce dernier est-il tronqué ?

— Que votre requête soit correcte ou pas, le serveur va toujours retourner une page web du moment que la connexion est faite. Comment savoir si le serveur a bien compris votre requête ? Corrigez éventuellement votre code.

# !/ usr / bin / p y t h o n 3 i m p o r t sys

i m p o r t s o c k e t

host = sys . argv [1]

port = 80

s = s o c k e t . s o c k e t ( s o c k e t . AF_INET , s o c k e t . S O C K _ S T R E A M ) s . c o n n e c t (( host , port ) )

# ## r e q u e s t ###

r e q u e s t = " GET / HTTP / 1 . 1 \ r \ n " \

" Host : " + host + " \ r \ n " \

" C o n n e c t i o n : c l o s e \ r \ n \ r \ n "

s . s e n d a l l ( r e q u e s t . e n c o d e () )

# ## a n s w e r ( v0 ) ###

# a n s w e r = s . recv ( 1 0 2 4 )

# p r i n t ( a n s w e r . d e c o d e ( ’ utf -8 ’) , end = ’ ’)

# ## a n s w e r ( v1 ) ###

w h i l e True :

a n s w e r = s . recv ( 1 0 2 4 ) if len( a n s w e r ) == 0:

b r e a k

# p r i n t ( a n s w e r )

p r i n t( a n s w e r . d e c o d e (" utf -8 ") , end =" ")

# p r i n t ( a n s w e r . d e c o d e (" iso - 8 8 5 9 - 1 " ) , end ="") # for www . g o o g l e . com ?!?

s . c l o s e ()

(6)

2.3 Raffinements

Pour récupérer toute la page, il faut mettre une boucle autour des appels recv() et print(). Lorsque le serveur referme la connexion TCP parce que la page est finie, recv() retourne le tableau d’octets vide b"", il suffira alors de tester cela pour savoir quand s’arrêter.

Par ailleurs, il faut noter que recv() retourne un tableau d’octets qu’il faut convertir en chaîne de caractères (str) Python avec la fonction decode() avant de l’afficher avec print(). Comme la chaîne de caractères contient déjà des retours lignes ’\n’, il n’est pas nécessaire que la fonctionprint() en rajoute (option end=’’) :

a n s w e r = s . recv ( 1 0 2 4 )

# a f f i c h a g e en byte a r r a y p r i n t( a n s w e r )

# d e c o d a g e et a f f i c h a g e en s t r i n g utf -8 p r i n t( a n s w e r . d e c o d e (" utf -8 ") , end =" ")

Attention, le sitewww. google. com n’accepte pas l’UTF-8 mais l’encodage ISO Latin1...

Dans ce cas, il faut faire :

print(answer.decode("iso-8859-1"), end="")

En cas d’erreur lors des étapes de connexion, il vaut mieux afficher un gentil message à l’utilisateur plutôt que laisser l’exception terminer le programme ! On peut récupérer une exception avec la construction syntaxique suivante :

try:

# ...

e x c e p t E x c e p t i o n as e : p r i n t(" s o c k e t e r r o r ! ") sys . exit (1)

Traitez différemment les cas d’erreur pour apporter à l’utilisateur une aide spécifique à chaque cas : essayez par exemple avec localhost (sur lequel aucun serveur web ne tourne) et avectrucbidule (qui n’existe pas), cela lève une exception différente.

3 Serveur Echo en Python

Nous allons maintenant étudier la programmation d’un serveur TCP en Python, en prenant l’exemple du service echo.

Le service echo est un service des plus simples : il répète ce qu’on lui dit. Il existe à la fois en version UDP et en version TCP (et même en AppleTalk). Quel est son numéro de port ?

$ grep echo /etc/services echo 7/tcp

echo 7/udp

(7)

On ne pourra pas lancer notre serveur sur ce port-là car les ports de numéro inférieurs à 1024 sont réservés à l’utilisateur root. On utilisera donc le port 7777.

3.1 Petit rappel

Il sera utile de se référer aux documentations suivantes :

— socket : https://docs.python.org/3/library/socket.html

— string : https://docs.python.org/3/library/stdtypes.html#str

Par ailleurs, il faut rappeler que les fonctions send() & recv() de la bibliothèque socket manipulent uniquement des tableaux d’octets. Par conséquent, on aura souvent besoin de faire des conversions entre des tableaux d’octets (b"h\xc3\xa9ho") et des chaînes de caractères ("hého"). La différence entre les deux est simplement la notion d’encodage des caractères, on va prendre comme convention que les tableaux d’octets encodent les chaînes de caractères en UTF-8 :

Pour convertir une chaîne de caractères en un tableau d’octets avant de pouvoir en- voyer vers le réseau :

s = "hého"

c = s.encode("utf-8")

Et inversement, quand on a lu un tableau d’octets depuis le réseau et que l’on veut l’afficher :

c = b"h\xc3\xa9ho"

s = c.decode("utf-8")

3.2 Version TCP

Un serveur TCP fonctionne presque de la même manière qu’un client TCP, la diffé- rence essentielle est que l’on doit gérer à la fois une socket d’écoute des connexions et les sockets pour les clients. On se contente ici de gérer un client à la fois.

— Créer unesocketà l’aide de la fonctionsocket.socket(), de famillesocket.AF_INET6 (qui gère à la fois en v4 et en v6), de typesocket.SOCK_STREAM, et laisser le pro- tocole 0 pour que le système choisisse automatiquement TCP.

— Utiliser la méthodebind de la socket pour greffer notresocket au port 7777. Pour l’adresse, il suffit de spécifier le paramètre ("", 7777) pour être à l’écoute sur toutes les cartes réseaux de la machine. Attention, il faut donc mettre 2 paren- thèses, pour que ("", 7777) soit un seul argument.

— Appeler la méthode listen pour indiquer au système qu’on va accepter des connexions. Un backlog de 1 suffira.

— Dans une boucle infinie,

• Appeler la méthodeaccept pour accepter une connexion entrante. Notez bien que cette fonction vous retourne un tuple contenant en premier une nouvelle socket, représentant la connexion acceptée, dont on va appeler les méthodes recv et send, et en deuxième son adresse qui ne nous sera pas utile. Il faut bien sûr conserver la socket initiale pour le prochainaccept, pour l’instant on s’occupe seulement de cette connexion.

• Dans une boucle infinie,

(8)

— Utiliser la méthode recv pour réceptionner des données, en lui passant comme taille 1500. Si la longueur des données reçues est 0, c’est que le client s’est déconnecté et l’on peut utiliser break pour sortir de la boucle.

— utiliser la méthodesend pour ré-expédier les données à l’envoyeur. Il suffit de passer le message obtenu !

• Fermer la socket de la connexion à l’aide de la méthodeclose.

Cette version passe ainsi son temps à effectuer accept, une boucle desend/recv, et close. Pour tester, utilisez nc localhost 7777 dans un terminal à côté pendant que votre serveur tourne. Utilisez netstat -tuap(ou ss -tuap) pour remarquer la présence de votre serveur. Relancez nc plusieurs fois, pour constater que le numéro de port côté client change effectivement à chaque fois.

Il se peut que bind échoue avec l’erreur Address already in use. Si vous regardez dans netstat ou ss, vous verrez une ligne du genre

tcp6 0 0 ::1:7777 ::1:49346 FIN_WAIT2 -

Cela signifie donc que le système préfère éviter que vous relanciez un serveur sur ce port alors qu’il reste encore des connexions qui ne se sont pas terminées, et il faut alors attendre quelques minutes. Pour éviter que le système soit si précautionneux, avant l’appel à bindutilisez appeler la méthodesetsockoptpour mettre l’optionsocket.SO_REUSEADDR (dans le level socket.SOL_SOCKET) à 1. Notez que si vous avez eu ce problème, cette option ne va pas fonctionner immédiatement, car il aurait fallu que cette option soit positionnée par l’instance du serveur que vous aviez lancée. Patientez quelques minutes, et les instances suivantes, lancées avec l’option, permettront d’en lancer d’autres.

# !/ usr / bin / p y t h o n 3 i m p o r t s o c k e t

s = s o c k e t . s o c k e t ( s o c k e t . AF_INET6 , s o c k e t . S O C K _ S T R E A M , 0) s . s e t s o c k o p t ( s o c k e t . S O L _ S O C K E T , s o c k e t . S O _ R E U S E A D D R , 1) s . bind ((’ ’, 7 7 7 7 ) )

s . l i s t e n (1) w h i l e True :

sc , a = s . a c c e p t ()

p r i n t(" new c l i e n t : ", a ) w h i l e True :

msg = sc . recv ( 1 5 0 0 ) if len( msg ) == 0:

p r i n t(" c l i e n t d i s c o n n e c t e d ") sc . c l o s e ()

b r e a k

sc . s e n d a l l ( msg ) s . c l o s e ()

(9)

3.3 Version UDP (bonus)

La version UDP est relativement simple à réaliser.

— Créer une socket de famille socket.AF_INET6 et de type socket.SOCK_DGRAM.

— Utiliser ensuite la méthode bind.

— Dans une boucle infinie,

— Appeler la méthode recvfromde la socket.

— Appeler la méthode sendto de la socket pour ré-expédier le message à l’en- voyeur. Il suffit de passer le message et l’adresse obtenus !

Pour tester, vous pouvez utiliser nc -u localhost 7777. Utilisez strace pour bien observer les appels effectués par votre serveur. Utilisez netstat -tuap (ou ss -tuap) pour remarquer la présence de votre serveur. Relancez nc plusieurs fois, pour constater que le numéro de port côté client change effectivement à chaque fois.

# !/ usr / bin / p y t h o n 3 i m p o r t s o c k e t

s = s o c k e t . s o c k e t ( s o c k e t . AF_INET6 , s o c k e t . S O C K _ D G R A M , 0) s . bind ((’ ’, 7 7 7 7 ) )

w h i l e True :

msg , a = s . r e c v f r o m ( 1 5 0 0 ) s . s e n d t o ( msg , a )

Références

Documents relatifs

Par ailleurs, il faut noter que recv() retourne un tableau d’octets qu’il faut convertir en chaîne de caractères (str) Python avec la fonction decode() avant de l’afficher

[r]

1°) Écrivez un programme qui calcule la somme des entiers de 1 à n où n est choisi par l’utilisateur... 2°) Testez ce programme avec différentes valeurs de n et essayez de trouver

retourne la longueur longueur de la chaîne de caractères de la chaîne de caractères str str si si str str n’est pas spécifié, retourne la n’est pas spécifié, retourne

Quel sera le montant de ma pension de ma Retraite complémentaire.. Retournez sur

Comment la date de départ influe sur la pension de retraite.. Quelles sont les démarches

Les lois de 18Bl ne furent qu'un jalon du combat pour l'éducation contre les forces réactionnaires mais aussi contre les pesanteurs conservatrices ou

Il faut donc avec k=0 que n