035.2 Leçon 1
Certification : |
Web Development Essentials |
---|---|
Version : |
1.0 |
Thème : |
035 Programmation de serveur NodeJS |
Objectif : |
035.2 Bases de NodeJS Express |
Leçon: |
1 sur 2 |
Introduction
Express.js, ou simplement Express, est un framework populaire qui fonctionne sur Node.js et qui est utilisé pour écrire des serveurs HTTP qui traitent les demandes des clients d’applications web. Express prend en charge de nombreuses façons de lire les paramètres envoyés par HTTP.
Script initial pour le serveur
Pour démontrer les fonctionnalités de base d’Express pour la réception et le traitement des requêtes, simulons une application qui demande certaines informations au serveur. En particulier, le serveur d’exemple :
-
Fournit une fonction
echo
, qui retourne simplement le message envoyé par le client. -
Indique au client son adresse IP sur demande.
-
Utilise des cookies pour identifier les clients connus.
La première étape est de créer le fichier JavaScript qui servira de serveur. En utilisant npm
, créez un répertoire appelé myserver
avec le fichier JavaScript :
$ mkdir myserver $ cd myserver/ $ npm init
Pour le point d’entrée, n’importe quel nom de fichier peut être utilisé. Ici, nous allons utiliser le nom de fichier par défaut : index.js
. Le listing suivant montre un fichier index.js
basique qui sera utilisé comme point d’entrée pour notre serveur :
const express = require('express')
const app = express()
const host = "myserver"
const port = 8080
app.get('/', (req, res) => {
res.send('Request received')
})
app.listen(port, host, () => {
console.log(`Server ready at http://${host}:${port}`)
})
Quelques constantes importantes pour la configuration du serveur sont définies dans les premières lignes du script. Les deux premières, express
et app
, correspondent au module express
inclus et à une instance de ce module qui exécute notre application. Nous allons ajouter les actions à effectuer par le serveur à l’objet app
.
Les deux autres constantes, host
et port
, définissent l’hôte et le port de communication associés au serveur.
Si vous avez un hôte accessible publiquement, utilisez son nom au lieu de myserver
comme valeur de host
. Si vous ne fournissez pas le nom de l’hôte, Express choisira par défaut localhost
, l’ordinateur sur lequel l’application s’exécute. Dans ce cas, aucun client extérieur ne pourra accéder au programme, ce qui peut convenir pour les tests mais n’a que peu d’intérêt en production.
Le port doit être fourni, sinon le serveur ne démarrera pas.
Ce script n’attache que deux procédures à l’objet app
: l’action app.get()
qui répond aux requêtes faites par les clients via HTTP GET
, et l’appel app.listen()
, qui est nécessaire pour activer le serveur et lui assigne un hôte et un port.
Pour démarrer le serveur, il suffit de lancer la commande node
, en fournissant le nom du script comme argument :
$ node index.js
Dès que le message Server ready at http://myserver:8080
apparaît, le serveur est prêt à recevoir des requêtes d’un client HTTP. Les demandes peuvent être faites à partir d’un navigateur situé sur le même ordinateur que celui où tourne le serveur, ou à partir d’une autre machine qui peut accéder au serveur.
Tous les détails de la transaction que nous allons voir ici sont affichés dans le navigateur si vous ouvrez une fenêtre pour la console du développeur. Alternativement, la commande curl
peut être utilisée pour la communication HTTP et vous permet d’inspecter les détails de la connexion plus facilement. Si vous n’êtes pas familier avec la ligne de commande du shell, vous pouvez créer un formulaire HTML pour soumettre des requêtes à un serveur.
L’exemple suivant montre comment utiliser la commande curl
sur la ligne de commande pour faire une requête HTTP vers le serveur nouvellement déployé :
$ curl http://myserver:8080 -v * Trying 192.168.1.225:8080... * TCP_NODELAY set * Connected to myserver (192.168.1.225) port 8080 (#0) > GET / HTTP/1.1 > Host: myserver:8080 > User-Agent: curl/7.68.0 >Accept: / > * Mark bundle as not supporting multiuse < HTTP/1.1 200 OK < X-Powered-By: Express < Content-Type: text/html; charset=utf-8 < Content-Length: 16 < ETag: W/"10-1WVvDtVyAF0vX9evlsFlfiJTT5c" < Date: Fri, 02 Jul 2021 14:35:11 GMT < Connection: keep-alive < * Connection #0 to host myserver left intact Request received
L’option -v
de la commande curl
affiche tous les en-têtes des requêtes et des réponses, ainsi que d’autres informations de débogage. Les lignes commençant par >
indiquent les en-têtes des requêtes envoyées par le client et les lignes commençant par <
indiquent les en-têtes des réponses envoyées par le serveur. Les lignes commençant par *
sont des informations générées par curl
lui-même. Le contenu de la réponse est affiché seulement à la fin, qui dans ce cas est la ligne Request received
.
L’URL du service, qui dans ce cas contient le nom d’hôte et le port du serveur (http://myserver:8080
), a été donnée comme argument à la commande curl
. Parce qu’aucun répertoire ou nom de fichier n’est donné, ceux-ci sont par défaut dans le répertoire racine /
. La barre oblique apparaît comme le fichier de requête dans la ligne > GET / HTTP/1.1
, qui est suivie dans la sortie par le nom d’hôte et le port.
En plus d’afficher les en-têtes de connexion HTTP, la commande curl
aide au développement d’applications en vous permettant d’envoyer des données au serveur en utilisant différentes méthodes HTTP et dans différents formats. Cette flexibilité facilite le débogage des problèmes et l’implémentation de nouvelles fonctionnalités sur le serveur.
Routes
Les requêtes que le client peut faire au serveur dépendent des routes qui ont été définies dans le fichier index.js
. Une route spécifie une méthode HTTP et définit un chemin (plus précisément, une URI) qui peut être demandé par le client.
Jusqu’à présent, le serveur n’a qu’une seule route configurée :
app.get('/', (req, res) => {
res.send('Request received')
})
Même s’il s’agit d’une route très simple, qui renvoie simplement un message en texte clair au client, elle suffit à identifier les composants les plus importants qui sont utilisés pour structurer la plupart des routes :
-
La méthode HTTP servie par la route. Dans l’exemple, la méthode HTTP
GET
est indiquée par la propriétéget
de l’objetapp
. -
Le chemin servi par la route. Lorsque le client ne spécifie pas de chemin pour la requête, le serveur utilise le répertoire racine, qui est le répertoire de base mis de côté pour être utilisé par le serveur web. Un exemple ultérieur dans ce chapitre utilise le chemin
/echo
, qui correspond à une demande faite àmyserver:8080/echo
. -
La fonction exécutée lorsque le serveur reçoit une requête sur cette route, généralement écrite sous forme abrégée comme une fonction flèche parce que la syntaxe
=>
pointe vers la définition de la fonction sans nom. Le paramètrereq
(abréviation de “request”) et le paramètreres
(abréviation de “response”) donnent des détails sur la connexion, transmis à la fonction par l’instance de l’application elle-même.
Méthode POST
Pour étendre les fonctionnalités de notre serveur de test, voyons comment définir une route pour la méthode HTTP POST
. Cette méthode est utilisée par les clients lorsqu’ils ont besoin d’envoyer des données supplémentaires au serveur, en plus de celles incluses dans l’en-tête de la requête. L’option --data
de la commande curl
invoque automatiquement la méthode POST
, et inclut le contenu qui sera envoyé au serveur via POST
. La ligne POST / HTTP/1.1
dans la sortie suivante montre que la méthode POST
a été utilisée. Cependant, notre serveur n’a défini qu’une méthode GET
, donc une erreur se produit lorsque nous utilisons curl
pour envoyer une requête via POST
:
$ curl http://myserver:8080/echo --data message="This is the POST request body" * Trying 192.168.1.225:8080... * TCP_NODELAY set * Connected to myserver (192.168.1.225) port 8080 (#0) > POST / HTTP/1.1 > Host: myserver:8080 > User-Agent: curl/7.68.0 >Accept: / > Content-Length: 37 > Content-Type: application/x-www-form-urlencoded > * upload completely sent off: 37 out of 37 bytes * Mark bundle as not supporting multiuse < HTTP/1.1 404 Not Found < X-Powered-By: Express < Content-Security-Policy: default-src 'none' < X-Content-Type-Options: nosniff < Content-Type: text/html; charset=utf-8 < Content-Length: 140 < Date: Sat, 03 Jul 2021 02:22:45 GMT < Connection: keep-alive < <!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <title>Error</title> </head> <body> <pre>Cannot POST /</pre> </body> </html> * Connection #0 to host myserver left intact
Dans l’exemple précédent, exécuter curl
avec le paramètre --data message="This is the POST request body"
équivaut à soumettre un formulaire contenant le champ texte nommé message
, rempli avec This is the POST request body
.
Comme le serveur est configuré avec une seule route pour le chemin /
, et que cette route ne répond qu’à la méthode HTTP GET
, l’en-tête de réponse contient la ligne HTTP/1.1 404 Not Found
. De plus, Express a automatiquement généré une réponse HTML courte avec l’avertissement Cannot POST
.
Après avoir vu comment générer une requête POST
à travers curl
, écrivons un programme Express qui peut traiter avec succès la requête.
Tout d’abord, notez que le champ Content-Type
de l’en-tête de la requête indique que les données envoyées par le client sont au format application/x-www-form-urlencoded
. Express ne reconnaît pas ce format par défaut, nous devons donc utiliser le module express.urlencoded
. Lorsque nous incluons ce module, l’objet req
— passé comme paramètre au gestionnaire de fonctions — a la propriété req.body.message
définie, qui correspond au champ message
envoyé par le client. Le module est chargé avec app.use
, qui doit être placé avant la déclaration des routes :
const express = require('express')
const app = express()
const host = "myserver"
const port = 8080
app.use(express.urlencoded({ extended: true }))
Une fois ceci est fait, il suffirait de changer app.get
en app.post
dans la route existante pour répondre aux requêtes faites via POST
et récupérer le corps de la requête :
app.post('/', (req, res) => {
res.send(req.body.message)
})
Au lieu de remplacer la route, une autre possibilité serait de simplement ajouter cette nouvelle route, car Express identifie la méthode HTTP dans l’en-tête de la requête et utilise la route appropriée. Parce que nous sommes intéressés par l’ajout de plus d’une fonctionnalité à ce serveur, il est pratique de séparer chacune d’elles avec son propre chemin, comme /echo
et /ip
.
Chemin et gestionnaire de fonctions
Après avoir défini la méthode HTTP qui répondra à la demande, nous devons maintenant définir un chemin spécifique pour la ressource et une fonction qui traite et génère une réponse au client.
Pour étendre la fonctionnalité echo
du serveur, nous pouvons définir une route en utilisant la méthode POST
avec le chemin /echo
:
app.post('/echo', (req, res) => {
res.send(req.body.message)
})
Le paramètre req
du gestionnaire de fonctions contient tous les détails de la demande stockés sous forme de propriétés. Le contenu du champ message
du corps de la requête est disponible dans la propriété req.body.message
. Dans l’exemple, ce champ est simplement renvoyé au client par l’appel res.send(req.body.message)
.
N’oubliez pas que les modifications que vous effectuez ne prennent effet qu’après le redémarrage du serveur. Comme vous exécutez le serveur depuis une fenêtre de terminal pendant les exemples de ce chapitre, vous pouvez arrêter le serveur en appuyant sur kbd:[Ctrl+C] sur ce terminal. Puis relancez le serveur par la commande node index.js
. La réponse obtenue par le client à la requête curl
que nous avons montrée auparavant est maintenant réussie :
$ curl http://myserver:8080/echo --data message="This is the POST request body" This is the POST request body
Autres moyens de transmettre et de renvoyer des informations dans une requête GET
Il peut être excessif d’utiliser la méthode HTTP POST
si seuls des messages texte courts comme celui utilisé dans l’exemple sont envoyés. Dans ce cas, les données peuvent être envoyées dans une chaîne de requête qui commence par un point d’interrogation. Ainsi, la chaîne ?message=C’est+le+message
pourrait être incluse dans le chemin de requête de la méthode HTTP GET
. Les champs utilisés dans la chaîne de requête sont disponibles pour le serveur dans la propriété req.query
. Par conséquent, un champ nommé message
est disponible dans la propriété req.query.message
.
Une autre façon d’envoyer des données via la méthode HTTP GET
est d’utiliser les paramètres de route d’Express :
app.get('/echo/:message', (req, res) => {
res.send(req.params.message)
})
La route dans cet exemple correspond aux requêtes faites avec la méthode GET
en utilisant le chemin /echo/:message
, où :message
est un caractère générique pour tout terme envoyé avec cette étiquette par le client. Ces paramètres sont accessibles dans la propriété req.params
. Avec cette nouvelle route, la fonction echo
du serveur peut être demandée plus succinctement par le client :
$ curl http://myserver:8080/echo/hello hello
Dans d’autres situations, les informations dont le serveur a besoin pour traiter la requête ne doivent pas être explicitement fournies par le client. Par exemple, le serveur a un autre moyen de récupérer l’adresse IP publique du client. Cette information est présente par défaut dans l’objet req
, dans la propriété req.ip
:
app.get('/ip', (req, res) => {
res.send(req.ip)
})
Maintenant le client peut demander le chemin /ip
avec la méthode GET
pour trouver sa propre adresse IP publique :
$ curl http://myserver:8080/ip 187.34.178.12
D’autres propriétés de l’objet req
peuvent être modifiées par le client, notamment les en-têtes de requête disponibles dans req.headers
. La propriété req.headers.user-agent
, par exemple, identifie le programme qui effectue la requête. Bien que ce ne soit pas une pratique courante, le client peut modifier le contenu de ce champ, et le serveur ne doit donc pas l’utiliser pour identifier de manière fiable un client particulier. Il est encore plus important de valider les données explicitement fournies par le client, pour éviter les incohérences de limites et de formats qui pourraient nuire à l’application.
Ajustements de la réponse
Comme nous l’avons vu dans les exemples précédents, le paramètre res
est responsable du retour de la réponse au client. De plus, l’objet res
peut modifier d’autres aspects de la réponse. Vous avez peut-être remarqué que, bien que les réponses que nous avons implémentées jusqu’à présent ne soient que de brefs messages en texte brut, l’en-tête Content-Type
des réponses utilise text/html ; charset=utf-8
. Bien que cela n’empêche pas la réponse en texte brut d’être acceptée, elle sera plus correcte si nous redéfinissons ce champ dans l’en-tête de la réponse en text/plain
avec le paramètre res.type('text/plain')
.
D’autres types d’ajustements de réponse impliquent l’utilisation de cookies, qui permettent au serveur d’identifier un client qui a déjà fait une demande. Les cookies sont importants pour les fonctionnalités avancées, comme la création de sessions privées qui associent les demandes à un utilisateur spécifique, mais nous allons ici nous contenter d’examiner un exemple simple d’utilisation d’un cookie pour identifier un client qui a déjà accédé au serveur.
Étant donné la conception modulaire d’Express, la gestion des cookies doit être installée avec la commande npm
avant d’être utilisée dans le script :
$ npm install cookie-parser
Après l’installation, la gestion des cookies doit être incluse dans le script du serveur. La définition suivante doit être incluse vers le début du fichier :
const cookieParser = require('cookie-parser')
app.use(cookieParser())
Pour illustrer l’utilisation des cookies, modifions la fonction de gestion de la route avec le chemin racine /
qui existe déjà dans le script. L’objet req
possède une propriété req.cookies
, où sont conservés les cookies envoyés dans l’en-tête de la requête. L’objet res
, quant à lui, possède une méthode res.cookie()
qui crée un nouveau cookie à envoyer au client. Le gestionnaire de fonctions, dans l’exemple suivant, vérifie si un cookie avec le nom known
existe dans la requête. Si un tel cookie n’existe pas, le serveur suppose qu’il s’agit d’un premier visiteur et lui envoie un cookie de ce nom par l’intermédiaire de l’appel res.cookie('known', '1')
. Nous attribuons arbitrairement la valeur 1
au cookie car il est censé avoir un contenu, mais le serveur ne consulte pas cette valeur. Cette application suppose simplement que la simple présence du cookie indique que le client a déjà demandé cette route auparavant :
app.get('/', (req, res) => {
res.type('text/plain')
if ( req.cookies.known === undefined ){
res.cookie('known', '1')
res.send('Welcome, new visitor!')
}
else
res.send('Welcome back, visitor');
})
Par défaut, curl
n’utilise pas de cookies dans les transactions. Mais il a des options pour stocker (-c cookies.txt
) et envoyer les cookies stockés (-b cookies.txt
) :
$ curl http://myserver:8080/ -c cookies.txt -b cookies.txt -v * Trying 192.168.1.225:8080... * TCP_NODELAY set * Connected to myserver (192.168.1.225) port 8080 (#0) > GET / HTTP/1.1 > Host: myserver:8080 > User-Agent: curl/7.68.0 >Accept: / > * Mark bundle as not supporting multiuse < HTTP/1.1 200 OK < X-Powered-By: Express < Content-Type: text/plain; charset=utf-8 * Added cookie known="1" for domain myserver, path /, expire 0 < Set-Cookie: known=1; Path=/ < Content-Length: 21 < ETag: W/"15-l7qrxcqicl4xv6EfA5fZFWCFrgY" < Date: Sat, 03 Jul 2021 23:45:03 GMT < Connection: keep-alive < * Connection #0 to host myserver left intact Welcome, new visitor!
Comme cette commande était le premier accès depuis l’implémentation des cookies sur le serveur, le client n’avait pas de cookies à inclure dans la requête. Comme prévu, le serveur n’a pas identifié le cookie dans la requête et a donc inclus le cookie dans les en-têtes de réponse, comme indiqué dans la ligne Set-Cookie : known=1 ; Path=/
de la sortie. Puisque nous avons activé les cookies dans curl
, une nouvelle requête inclura le cookie known=1
dans les en-têtes de la requête, permettant au serveur d’identifier la présence du cookie :
$ curl http://myserver:8080/ -c cookies.txt -b cookies.txt -v * Trying 192.168.1.225:8080... * TCP_NODELAY set * Connected to myserver (192.168.1.225) port 8080 (#0) > GET / HTTP/1.1 > Host: myserver:8080 > User-Agent: curl/7.68.0 >Accept: / > Cookie: known=1 > * Mark bundle as not supporting multiuse < HTTP/1.1 200 OK < X-Powered-By: Express < Content-Type: text/plain; charset=utf-8 < Content-Length: 21 < ETag: W/"15-ATq2flQYtLMYIUpJwwpb5SjV9Ww" < Date: Sat, 03 Jul 2021 23:45:47 GMT < Connection: keep-alive < * Connection #0 to host myserver left intact Welcome back, visitor
Sécurité des cookies
Le développeur doit être conscient des vulnérabilités potentielles lors de l’utilisation de cookies pour identifier les clients effectuant des requêtes. Les attaquants peuvent utiliser des techniques telles que le cross-site scripting (XSS) et le cross-site request forgery (CSRF) pour voler les cookies d’un client et ainsi se faire passer pour lui lorsqu’il effectue une demande au serveur. En général, ces types d’attaques utilisent des champs de commentaires non validés ou des URL méticuleusement construites pour insérer du code JavaScript malveillant dans la page. Lorsqu’il est exécuté par un client authentique, ce code peut copier des cookies valides et les stocker ou les transmettre à une autre destination.
C’est pourquoi, en particulier dans les applications professionnelles, il est important d’installer et d’utiliser des fonctionnalités Express plus spécialisées, connues sous le nom de middleware. Les modules express-session
ou cookie-session
permettent un contrôle plus complet et plus sûr de la gestion des sessions et des cookies. Ces composants permettent des contrôles supplémentaires pour empêcher que les cookies soient détournés de leur émetteur d’origine.
Exercices guidés
-
Comment le contenu du champ
comment
, envoyé dans une chaîne de requête de la méthode HTTP`GET`, peut-il être lu dans un gestionnaire de fonctions ? -
Écrivez une route qui utilise la méthode HTTP
GET
et le chemin/agent
pour renvoyer au client le contenu de l’en-têteuser-agent
. -
Express.js possède une fonctionnalité appelée paramètres de route, où un chemin tel que
/user/:name
peut être utilisé pour recevoir le paramètrename
envoyé par le client. Comment accéder au paramètrename
dans la fonction de gestion de la route ?
Exercices d’exploration
-
Si le nom d’hôte d’un serveur est
myserver
, quelle route d’Express recevrait la soumission dans le formulaire ci-dessous ?<form action="/contact/feedback" method="post"> ... </form>
-
Pendant le développement du serveur, le programmeur ne parvient pas à lire la propriété
req.body
, même après avoir vérifié que le client envoie correctement le contenu via la méthode HTTPPOST
. Quelle est la cause probable de ce problème ? -
Que se passe-t-il lorsque le serveur a une route définie sur le chemin
/user/:name
et que le client fait une requête sur/user/
?
Résumé
Cette leçon explique comment écrire des scripts Express pour recevoir et traiter des requêtes HTTP. Express utilise le concept de routes pour définir les ressources disponibles pour les clients, ce qui vous donne une grande flexibilité pour construire des serveurs pour tout type d’application web. Cette leçon aborde les concepts et procédures suivants :
-
Les routes qui utilisent les méthodes HTTP
GET
et HTTPPOST
. -
Comment les données du formulaire sont stockées dans l’objet
request
. -
Comment utiliser les paramètres de route.
-
Personnalisation des en-têtes de réponse.
-
Gestion basique des cookies.
Réponses aux exercices guidés
-
Comment le contenu du champ
comment
, envoyé dans une chaîne de requête de la méthode HTTP`GET`, peut-il être lu dans un gestionnaire de fonctions ?Le champ
comment
est disponible dans la propriétéreq.query.comment
. -
Écrivez une route qui utilise la méthode HTTP
GET
et le chemin/agent
pour renvoyer au client le contenu de l’en-têteuser-agent
.app.get('/agent', (req, res) => { res.send(req.headers.user-agent) })
-
Express.js possède une fonctionnalité appelée paramètres de route, où un chemin tel que
/user/:name
peut être utilisé pour recevoir le paramètrename
envoyé par le client. Comment accéder au paramètrename
dans la fonction de gestion de la route ?Le paramètre
name
est accessible dans la propriétéreq.params.name
.
Réponses aux exercices d’exploration
-
Si le nom d’hôte d’un serveur est
myserver
, quelle route d’Express recevrait la soumission dans le formulaire ci-dessous ?<form action="/contact/feedback" method="post"> ... </form>
app.post('/contact/feedback', (req, res) => { ... })
-
Pendant le développement du serveur, le programmeur ne parvient pas à lire la propriété
req.body
, même après avoir vérifié que le client envoie correctement le contenu via la méthode HTTPPOST
. Quelle est la cause probable de ce problème ?Le programmeur n’a pas inclus le module
express.urlencoded
, qui permet à Express d’extraire le corps d’une requête. -
Que se passe-t-il lorsque le serveur a une route définie sur le chemin
/user/:name
et que le client fait une requête sur/user/
?Le serveur émettra une réponse
404 Not Found
, car la route nécessite que le paramètre:name
soit fourni par le client.