035.2 Lezione 1
Certificazione: |
Web Development Essentials |
---|---|
Versione: |
1.0 |
Argomento: |
035 Programmazione Server con Node.js |
Obiettivo: |
035.2 Fondamenti di Node.js Express |
Lezione: |
1 di 2 |
Introduzione
Express.js, o semplicemente Express, è un popolare framework che gira su Node.js ed è usato per realizzare server HTTP che gestiscono le richieste dei client delle applicazioni web. Express supporta molti modi per leggere i parametri inviati via HTTP.
Script di Inizializzazione Server
Per dimostrare le caratteristiche di base di Express nel ricevere e gestire le richieste, simuliamo un’applicazione che richiede alcune informazioni dal server. In particolare, il server di esempio:
-
Fornisce una funzione
echo
, che restituisce semplicemente il messaggio inviato dal client. -
Comunica al client il suo indirizzo IP su richiesta.
-
Usa i cookie per identificare i client conosciuti.
Il primo passo è creare il file JavaScript che opererà come server. Usando npm
, create una directory chiamata myserver
con il file JavaScript:
$ mkdir myserver $ cd myserver/ $ npm init
Per il file di inizializzazione, può essere usato qualsiasi nome di file. Qui useremo il nome predefinito del file: index.js
. Il seguente elenco mostra un file di base index.js
che sarà usato come punto di ingresso per il nostro server:
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}`)
})
Alcune costanti importanti per la configurazione del server sono definite nelle prime righe dello script. Le prime due, express
e app
, corrispondono al modulo incluso express
e a un’istanza di questo modulo che esegue la nostra applicazione. All’oggetto app
aggiungeremo le azioni che devono essere eseguite dal server.
Le altre due costanti, host
e port
, definiscono l’host e la porta di comunicazione associati al server.
Se hai un host accessibile pubblicamente, usa il suo nome invece di myserver
come valore di host
. Se non fornisci il nome dell’host, Express utilizzerà di default localhost
, ovvero il computer su cui l’applicazione viene eseguita. In questo caso, nessun client esterno sarà in grado di raggiungere il programma, il che può andare bene per i test ma offre poca utilità in produzione.
La porta deve essere fornita, altrimenti il server non partirà.
Questo script allega solo due procedure all’oggetto app
: l’azione app.get()
che risponde alle richieste fatte dai client tramite HTTP GET
, e la chiamata app.listen()
, che è necessaria per attivare il server e gli assegna un host e una porta.
Per avviare il server, basta eseguire il comando node
, fornendo il nome dello script come argomento:
$ node index.js
Non appena appare il messaggio Server ready at http://myserver:8080
, il server è pronto a ricevere richieste da un client HTTP. Le richieste possono essere fatte da un browser sullo stesso computer su cui il server è in esecuzione, o da un’altra macchina che può accedere al server.
Tutti i dettagli della transazione che vedremo qui sono mostrati nel browser se si apre la console dello sviluppatore. In alternativa, il comando curl
può essere usato per la comunicazione HTTP e permette di ispezionare più facilmente i dettagli della connessione. Se non si ha familiarità con la riga di comando della shell, si può creare un modulo HTML per inviare richieste a un server.
L’esempio seguente mostra come usare il comando curl
sulla linea di comando per fare una richiesta HTTP al server appena attivato:
$ 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’opzione -v
del comando curl
visualizza tutte le intestazioni di richiesta e di risposta, così come altre informazioni di debug. Le linee che iniziano con >
indicano gli header di richiesta inviati dal client e le linee che iniziano con <
indicano gli header di risposta inviati dal server. Le linee che iniziano con *
sono informazioni generate da curl
stesso. Il contenuto della risposta viene mostrato solo alla fine, che in questo caso è la linea Request received
.
L’URL del servizio, che in questo caso contiene l’hostname e la porta del server (http://myserver:8080
), è stata data come argomento al comando curl
. Non essendo stata fornita alcuna directory o nome di file, la sessione inizierà fecendo riferimento alla directory principale /
. Lo slash compare come file di richiesta nella linea > GET / HTTP/1.1
, che è seguita nell’output dall’hostname e dalla porta.
Oltre a visualizzare le intestazioni di connessione HTTP, il comando curl
facilita lo sviluppo dell’applicazione permettendoti di inviare dati al server usando diversi metodi HTTP e in diversi formati. Questa flessibilità rende più facile il debug di qualsiasi problema e l’implementazione di nuove funzionalità sul server.
Rotte
Le richieste che il client può fare al server dipendono da quali route sono state definite nel file index.js
. Una rotta indica un metodo HTTP e definisce un percorso (più precisamente, una URI) che può essere richiesta dal client.
Finora, il server ha solo una rotta configurata:
app.get('/', (req, res) => {
res.send('Request received')
})
Anche se si tratta di una rotta molto semplice, che restituisce semplicemente un messaggio di testo al client, è sufficiente per identificare i componenti più importanti che vengono utilizzati per strutturare la maggior parte delle rotte:
-
Il metodo HTTP servito dalla rotta. Nell’esempio, il metodo HTTP
GET
è indicato dalla proprietàget
dell’oggettoapp
. -
Il percorso servito da una rotta. Quando il client non specifica un percorso per la richiesta, il server utilizza la directory principale, che è la directory di base riservata all’utilizzo da parte del server web. Un esempio successivo in questo capitolo usa il percorso
/echo
, che corrisponde a una richiesta fatta amyserver:8080/echo
. -
La funzione eseguita quando il server riceve una richiesta su questa rotta, di solito scritta utilizzando la forma abbreviata
=>
di una arrow function . Il parametroreq
(abbreviazione di “request”) e il parametrores
(abbreviazione di “response”) forniscono dettagli sulla connessione, passati alla funzione dall’istanza dell’app stessa.
Metodo POST
Per estendere la funzionalità del nostro server di prova, vediamo come definire una rotta per il metodo HTTP POST
. È usato dai client quando hanno bisogno di inviare dati extra al server oltre a quelli inclusi nell’intestazione della richiesta. L’opzione --data
del comando curl
invoca automaticamente il metodo POST
e include il contenuto che sarà inviato al server tramite POST
. La linea POST / HTTP/1.1
nel seguente output mostra che è stato usato il metodo POST
. Tuttavia, il nostro server ha definito solo un metodo GET
, quindi si verifica un errore quando usiamo curl
per inviare una richiesta 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
Nell’esempio precedente, eseguire curl
con il parametro --data message="This is the POST request body"
equivale a inviare un modulo contenente il campo di testo chiamato message
, riempito con This is the POST request body
.
Poichè il server è configurato con una sola rotta per il percorso /
, e quella rotta risponde solo al metodo HTTP GET
, l'header di risposta sarà HTTP/1.1 404 Not Found
. Inoltre, Express ha generato automaticamente una breve risposta HTML con l’avviso Cannot POST
.
Avendo visto come generare una richiesta POST
attraverso curl
, scriviamo un programma Express che possa gestire con successo la richiesta.
Per prima cosa, nota che il campo Content-Type
nell’intestazione della richiesta dice che i dati inviati dal client sono in formato application/x-www-form-urlencoded
. Express non riconosce questo formato per default, quindi dobbiamo usare il modulo express.urlencoded
. Quando includiamo questo modulo, l’oggetto req
— passato come parametro alla funzione handler — ha la proprietà req.body.message
impostata, che corrisponde al campo message
inviato dal client. Il modulo viene caricato con app.use
, che dovrebbe essere posto prima della dichiarazione delle rotte:
const express = require('express')
const app = express()
const host = "myserver"
const port = 8080
app.use(express.urlencoded({ extended: true }))
Una volta fatto questo sarebbe sufficiente cambiare app.get
in app.post
nella rotta esistente per soddisfare le richieste fatte tramite POST
e recuperare il corpo della richiesta:
app.post('/', (req, res) => {
res.send(req.body.message)
})
Invece di sostituire la rotta, un’altra possibilità sarebbe quella di aggiungere semplicemente questa nuova rotta, perché Express identifica il metodo HTTP nell’intestazione della richiesta e usa la rotta appropriata. Poiché siamo interessati ad aggiungere più di una funzionalità a questo server, è conveniente separare ciascuna di esse con il proprio percorso, come /echo
e /ip
.
Path e Function Handler
Avendo definito quale metodo HTTP risponderà alla richiesta, ora abbiamo bisogno di definire un percorso specifico per la risorsa e una funzione che elabori e generi una risposta al client.
Per espandere la funzionalità echo
del server possiamo definire una rotta usando il metodo POST
con il percorso /echo
:
app.post('/echo', (req, res) => {
res.send(req.body.message)
})
Il parametro req
della funzione handler contiene tutti i dettagli della richiesta memorizzati come proprietà. Il contenuto del campo message
nel corpo della richiesta è disponibile nella proprietà req.body.message
. L’esempio invia semplicemente questo campo al client attraverso la chiamata res.send(req.body.message)
.
Ricorda che i cambiamenti apportati hanno effetto solo dopo che il server è stato riavviato. Poiché stai eseguendo il server da una finestra di terminale durante gli esempi di questo capitolo, puoi spegnere il server premendo kbd:[Ctrl+C] sul terminale. Poi rilancia il server attraverso il comando node index.js
. La risposta ottenuta dal client alla richiesta curl
che abbiamo mostrato prima avrà ora successo:
$ curl http://myserver:8080/echo --data message="This is the POST request body" This is the POST request body
Altri Modi per Passare e Restituire Informazioni in una Richiesta GET
Potrebbe essere eccessivo usare il metodo HTTP POST
per inviare solo brevi messaggi di testo come quello usato nell’esempio. In questi casi, i dati possono essere inviati in una query string che inizia con un punto interrogativo. Così facendo, la stringa ?message=This+is+the+message
potrebbe essere inclusa nel percorso di richiesta del metodo HTTP GET
. I campi usati nella stringa di richiesta sono disponibili al server nella proprietà req.query
. Pertanto, un campo chiamato message
è disponibile nella proprietà req.query.message
.
Un altro modo per inviare dati tramite il metodo HTTP GET
è quello di usare i parametri di rotta di Express:
app.get('/echo/:message', (req, res) => {
res.send(req.params.message)
})
La rotta in questo esempio corrisponde alle richieste fatte con il metodo GET
usando il percorso /echo/:message
, dove :message
è un marcatore per qualsiasi termine inviato con quella etichetta dal client. Questi parametri sono accessibili nella proprietà req.params
. Con questo nuovo percorso, la funzione echo
del server può essere richiesta più succintamente dal client:
$ curl http://myserver:8080/echo/hello hello
In altre situazioni, le informazioni di cui il server ha bisogno per elaborare la richiesta non devono essere fornite esplicitamente dal client. Per esempio, il server ha un altro modo per recuperare l’indirizzo IP pubblico del client. Questa informazione è presente nell’oggetto req
per default, nella proprietà req.ip
:
app.get('/ip', (req, res) => {
res.send(req.ip)
})
Ora il client può richiedere il percorso /ip
con il metodo GET
per trovare il proprio indirizzo IP pubblico:
$ curl http://myserver:8080/ip 187.34.178.12
Altre proprietà dell’oggetto req
possono essere modificate dal client, specialmente le intestazioni della richiesta disponibili in req.headers
. La proprietà req.headers.user-agent
, per esempio, identifica quale programma stia eseguendo la richiesta. Anche se non è una pratica comune, il client può cambiare il contenuto di questo campo, quindi il server non dovrebbe usarlo per identificare in modo affidabile un particolare client. È ancora più importante validare i dati forniti esplicitamente dal client, per evitare incongruenze nei termini e nei formati che potrebbero influenzare negativamente l’applicazione.
Adeguamenti alla Risposta
Come abbiamo visto negli esempi precedenti, il parametro res
è responsabile della restituzione di una risposta al client. Inoltre, l’oggetto res
può cambiare altri aspetti della risposta. Avrai notato che, sebbene le risposte che abbiamo implementato finora siano solo brevi messaggi di testo semplice, l’intestazione Content-Type
delle risposte usa text/html; charset=utf-8
. Anche se questo non impedisce alla risposta in testo semplice di essere accettata, sarà più corretto se ridefiniamo questo campo nell’intestazione della risposta a text/plain
con l’impostazione res.type('text/plain')
.
Altri tipi di aggiustamenti della risposta implicano l’uso di cookie, che permettono al server di identificare un client che ha precedentemente fatto una richiesta. I cookie sono importanti per funzioni avanzate, come la creazione di sessioni private che associano le richieste a un utente specifico, ma qui tratteremo solo un semplice esempio di come usare un cookie per identificare un client che ha precedentemente eseguito l’accesso al server.
Dato il design modulare di Express, la gestione dei cookie deve essere installata con il comando npm
prima di essere usata nello script:
$ npm install cookie-parser
Dopo l’installazione, la gestione dei cookie deve essere inclusa nello script del server. La seguente definizione dovrebbe essere inclusa nelle prime righe del file:
const cookieParser = require('cookie-parser')
app.use(cookieParser())
Per illustrare l’uso dei cookie, modifichiamo la funzione handler della rotta con il percorso di root /
che esiste già nello script. L’oggetto req
ha una proprietà req.cookies
, dove vengono conservati i cookie inviati nell’intestazione della richiesta. L’oggetto res
, d’altra parte, ha un metodo res.cookie()
che crea un nuovo cookie da inviare al client. La funzione handler nell’esempio seguente controlla se nella richiesta esiste un cookie con il nome known
. Se tale cookie non esiste, il server assume che si tratti di un primo visitatore e gli invia un cookie con quel nome attraverso la chiamata res.cookie('known', '1')
. Noi assegniamo arbitrariamente il valore 1
al cookie perché si suppone che abbia qualche contenuto, ma il server non consulta quel valore. Questa applicazione assume semplicemente che la semplice presenza del cookie indichi che il client ha già richiesto questo percorso in precedenza:
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');
})
Per impostazione predefinita, curl
non usa i cookie nelle transazioni. Ma ha opzioni per memorizzare (-c cookies.txt
) e inviare i cookie memorizzati (-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!
Poiché questo comando era il primo con cui effettuavamo un accesso dall’implementazione dei cookie, il client non ne ha fornito nessuno nella richiesta. Come previsto, quindi, il server non trovando il cookie nella richiesta ne ha incluso uno nelle intestazioni di risposta, come indicato nella linea Set-Cookie: known=1; Path=/
dell’output. Poiché abbiamo abilitato i cookie in curl
, una nuova richiesta includerà il cookie known=1
nelle intestazioni della richiesta, permettendo al server di identificare la presenza del 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
Sicurezza dei Cookie
Lo sviluppatore dovrebbe essere consapevole delle potenziali vulnerabilità quando usa i cookie per identificare i client che fanno richieste. I criminali informatici possono usare tecniche come cross-site scripting (XSS) e cross-site request forgery (CSRF) per rubare i cookie da un client e quindi impersonarlo quando fa una richiesta al server. In generale, questi tipi di attacchi utilizzano campi di commento non convalidati o URL meticolosamente costruite per inserire codice JavaScript dannoso nella pagina. Quando viene eseguito da un client, questo codice può copiare i cookie validi e memorizzarli o inoltrarli a un’altra destinazione.
Perciò, specialmente nelle applicazioni professionali, è importante installare e usare funzioni Express più specializzate, conosciute come middleware. I moduli express-session
o cookie-session
forniscono un controllo più completo e sicuro sulla gestione della sessione e dei cookie. Questi componenti permettono controlli extra per evitare che i cookie vengano deviati dal loro emittente originale.
Esercizi Guidati
-
Come si può leggere il contenuto del campo
comment
, inviato all’interno di una stringa di query del metodo HTTPGET
, in una funzione handler? -
Scrivi una rotta che usi il metodo HTTP
GET
e il percorso/agent
per rimandare al client il contenuto dell’intestazioneuser-agent
. -
Express.js ha una caratteristica chiamata route parameters, dove un percorso come
/user/:name
può essere usato per ricevere il parametroname
inviato dal client. Come si può accedere al parametroname
all’interno della funzione handler della rotta?
Esercizi Esplorativi
-
Se il nome host di un server è
myserver
, quale rotta Express riceverebbe l’invio nel modulo sottostante?<form action="/contact/feedback" method="post"> ... </form>
-
Durante la fase di sviluppo del server, il programmatore non è in grado di leggere la proprietà
req.body
, anche dopo aver verificato che il client stia inviando correttamente il contenuto tramite il metodo HTTPPOST
. Qual è la probabile causa di questo problema? -
Cosa succede quando il server ha una rotta impostata sul percorso
/user/:name
e il client fa una richiesta a/user/
?
Sommario
Questa lezione spiega come scrivere script Express per ricevere e gestire richieste HTTP. Express usa il concetto di route per definire le risorse disponibili ai client, il che dà grande flessibilità per costruire server per qualsiasi tipo di applicazione web. Questa lezione passa in rassegna i seguenti concetti e procedure:
-
Percorsi che usano i metodi HTTP
GET
e HTTPPOST
. -
Come vengono memorizzati i dati del modulo nell’oggetto
request
. -
Come usare i parametri delle rotte.
-
Personalizzare le intestazioni di risposta.
-
Gestione di base dei cookie.
Risposte agli Esercizi Guidati
-
Come si può leggere il contenuto del campo
comment
, inviato all’interno di una stringa di query del metodo HTTPGET
, in una funzione handler?Il campo
comment
è disponibile nella proprietàreq.query.comment
. -
Scrivi una rotta che usi il metodo HTTP
GET
e il percorso/agent
per rimandare al client il contenuto dell’intestazioneuser-agent
.app.get('/agent', (req, res) => { res.send(req.headers.user-agent) })
-
Express.js ha una caratteristica chiamata route parameters, dove un percorso come
/user/:name
può essere usato per ricevere il parametroname
inviato dal client. Come si può accedere al parametroname
all’interno della funzione handler della rotta?Il parametro
name
è accessibile nella proprietàreq.params.name
.
Risposte agli Esercizi Esplorativi
-
Se il nome host di un server è
myserver
, quale rotta Express riceverebbe l’invio nel modulo sottostante?<form action="/contact/feedback" method="post"> ... </form>
app.post('/contact/feedback', (req, res) => { ... })
-
Durante la fase di sviluppo del server, il programmatore non è in grado di leggere la proprietà
req.body
, anche dopo aver verificato che il client stia inviando correttamente il contenuto tramite il metodo HTTPPOST
. Qual è la probabile causa di questo problema?Il programmatore non ha incluso il modulo
express.urlencoded
, che permette a Express di estrarre il corpo di una richiesta. -
Cosa succede quando il server ha una rotta impostata sul percorso
/user/:name
e il client fa una richiesta a/user/
?Il server genererà una risposta
404 Not Found
, perché la rotta richiede che il parametro:name
sia fornito dal client.