PYTHON
HTTP server
Importiamo le classi necessarie
Per realizzare un web server minimale sono necessarie le
classi
HTTPServer e
BaseHTTPRequestHandler contenute nel modulo
http.server
(
BaseHTTPServer
in Python2).
from http.server import HTTPServer
from http.server import BaseHTTPRequestHandler
Creiamo un handler vuoto e un server in ascolto
L’handler è la parte di programma che deve gestire la richiesta.
È una classe che eredita da
BaseHTTPRequestHandler e ne ridefinisce
i metodi
do_GET e
do_POST.
Il server invece è un’istanza di HTTPServer, creata passando
l’indirizzo locale (IP + porta in ascolto) e la classe handler.
La stringa dell’IP può essere impostata uguale all’IP LAN
della macchina su cui gira il server, oppure può essere lasciata vuota
(l’IP viene preso automaticamente).
La porta di default su cui un WEB server attende richieste è
la 80, ma per applicazioni particolari se ne possono usare
altre, casi comuni sono 8000, 8080, 8800.
Il server normalmente può essere fermato solo con un’interruzione da tastiera (CTRL + C).
class Gestore(BaseHTTPRequestHandler):
def do_GET(self):
pass
def do_POST(self):
pass
#___________________________________________________________
indirizzo = '', 80
server = HTTPServer(indirizzo, Gestore)
try:
server.serve_forever()
except KeyboardInterrupt:
server.socket.close()
Oggetti disponibili nell’handler
Ad ogni request ricevuta dal server, viene chiamato il metodo
do_GET
oppure
do_POST dell’handler. Questi sono gli oggetti principali con cui lavorare:
OGGETTO | DESCRIZIONE |
self.client_address | Tupla (“ip”, porta) |
self.path | Stringa, es: “/pagine/file1.html”. Solo “/” se non si specifica niente nella richiesta. Da qui si leggono i dati ricevuti tramite una richiesta di tipo GET. |
self.headers | Tutti gli header inviati nella richiesta |
self.server | Istanza del server |
self.rfile | Stream in arrivo, da qui si leggono i dati ricevuti tramite una richiesta di tipo POST. |
self.wfile | Stream in uscita, qui si scrive il corpo della risposta (il testo HTML, altri file richiesti ecc) |
Il server (istanza di
HTTPServer è costruito partendo
dalla classe
TCPServer del modulo
socketserver
(
SocketServer in Python2).
Quindi dispone di tutti gli attributi/metodi di
TCPServer, ad
esempio il metodo
serve_forever per avviarlo.
Nota:se tra due applicazioni si vuole solo comunicare tramite TCP/IP
scambiando messaggi con protocollo proprietario e non HTTP, allora va usato direttamente
il modulo di livello più basso
socketserver (istanziando un
TCPServer).
Il modulo
http.server
si trova infatti ad un livello di astrazione superiore, creato
per gestire in modo semplificato le transazioni HTTP da/verso un browser.
Una risposta minimale
Prima l’esempio completo, poi la spiegazione...
from http.server import HTTPServer
from http.server import BaseHTTPRequestHandler
#___________________________________________________________
pagina = u'''<!DOCTYPE html>
<html lang="it">
<head>
<meta charset="UTF-8">
</head>
<body>
OK
</body>
</html>'''
#___________________________________________________________
def trasmetti(hdl, codice, tipo, contenuto):
hdl.send_response(codice)
hdl.send_header('Content-Type', tipo)
hdl.send_header('Content-Length', len(contenuto))
hdl.end_headers()
hdl.wfile.write(contenuto)
hdl.wfile.flush()
hdl.connection.shutdown(1)
#___________________________________________________________
class Gestore(BaseHTTPRequestHandler):
def do_GET(self):
contenuto = pagina.encode('utf-8')
tipo = 'text/html'
trasmetti(self, 200, tipo, contenuto)
def do_POST(self):
pass
#___________________________________________________________
indirizzo = '', 80
server = HTTPServer(indirizzo, Gestore)
try:
server.serve_forever()
except KeyboardInterrupt:
server.socket.close()
Per dare al browser una risposta minimale (quella che comunemente
viene chiamata “pagina WEB”) prepariamo una “pagina” minima ma
completa in
HTML5 sotto forma di stringa di testo.
È importante trattare sempre il testo in forma
unicode in modo da poter rappresentare qualsiasi carattere. Questo è il default in Python3, mentre Python2 richiede di specificare esplicitamente il tipo di stringa anteponendo il carattere ‘u’ alle stringhe unicode.
Il prefisso ‘u’ è comunque accettato anche da Python3, quindi una stringa iniziante con ‘u’ va sempre bene ed evita ambiguità.
Il
protocollo HTTP prevede che la risposta inizi con un codice di risposta, seguito da uno o più header che specificano informazioni sul contenuto, da una riga vuota che separa header da contenuto, ed infine dal contenuto vero e proprio (la pagina o qualsiasi altro dato richiesto).
L’invio del codice di risposta, degli header e della riga vuota, si ottengono con i metodi
send_response,
send_header,
end_headers. Il contenuto vero e proprio invece si scrive sullo stream di uscita.
Per comodità si può creare una funzione ‘trasmetti’ con tutto il necessario per trasmettere la risposta. Il primo parametro è l’istanza dell’handler, il secondo il codice di risposta, il terzo il mimetype, il quarto i dati veri e propri.
Ogni contenuto va trasmesso sotto forma di byte, quindi il testo della pagina va codificato
(
encode).
Va usato lo stesso
encoding
dichiarato nella sezione header della pagina HTML. In questo modo il browser che riceve i byte dei caratteri, li interpreta e visualizza sempre correttamente.
...e con questo abbiamo creato un WEB server che ad ogni richiesta di tipo GET risponde sempre con il testo “OK”.
Forse poco utile, ma necessario per vedere le basi e verificare il corretto funzionamento. Su qualche sistema può essere necessario avviare il server con i permessi di amministratore, ad esempio scrivendo a terminale:
sudo python3 webserver.py
Servire i file statici
Più interessante è un WEB server in grado di fornire i contenuti presenti in una specifica directory (e sottodirectory), pagine HTML, fogli di stile CSS, script Javascript, immagini ecc:
import os
from urllib.parse import unquote_plus
from http.server import HTTPServer
from http.server import BaseHTTPRequestHandler
BASEDIR = r'/home/a/Scrivania'
#___________________________________________________________
def trasmetti(hdl, codice, tipo, contenuto):
hdl.send_response(codice)
hdl.send_header('Content-Type', tipo)
hdl.send_header('Content-Length', len(contenuto))
hdl.end_headers()
hdl.wfile.write(contenuto)
hdl.wfile.flush()
hdl.connection.shutdown(1)
#___________________________________________________________
TIPI = {
'.css': 'text/css',
'.gif': 'image/gif',
'.jpg': 'image/jpg',
'.png': 'image/png',
'.ico': 'image/x-icon',
'.html': 'text/html',
'.htm': 'text/html',
'.txt': 'text/plain',
'.js': 'application/javascript'
}
def determina_tipo(risorsa):
for estensione in TIPI:
if risorsa.lower().endswith(estensione):
return TIPI[estensione]
return 'text/plain'
#___________________________________________________________
def invia_risorsa(hdl, risorsa):
tipo = determina_tipo(risorsa)
try:
with open(BASEDIR + risorsa, 'rb') as fi:
trasmetti(hdl, 200, tipo, fi.read())
except IOError:
hdl.send_error(500, 'Internal server error')
#___________________________________________________________
class Gestore(BaseHTTPRequestHandler):
def do_GET(self):
richiesta = unquote_plus(self.path)
risorsa = richiesta.split('?', 1)[0]
if os.path.isfile(BASEDIR + risorsa):
invia_risorsa(self, risorsa)
else:
self.send_error(
404, '{} Not found'.format(risorsa))
def do_POST(self):
pass
#___________________________________________________________
indirizzo = '', 80
server = HTTPServer(indirizzo, Gestore)
try:
server.serve_forever()
except KeyboardInterrupt:
server.socket.close()
La stringa costante BASEDIR rappresenta la directory in cui il server deve cercare i file.
Nota: Sui sistemi Windows si usano i backslash ‘\’ per comporre i percorsi, e questi potrebbero essere interpretati da Python come sequenze di escape. Per scongiurare del tutto questa possibilità basta definire come “raw” tutte le stringhe che contengono percorsi, anteponendo il simbolo ‘r’.
La stringa
self.path potrebbe presentarsi “quotata” con alcuni caratteri sostituiti da altri caratteri, inizianti con il simbolo ‘%’, ad esempio:
/archivio/1995/pag2.html?p=126&r=%22ab%20cd%22
Va quindi preventivamente “unquotata” con la funzione
unquote_plus
del modulo
urllib.parse
(
urllib in Python2).
Poi dobbiamo separare il percorso della risorsa da tutto quello che eventualmente si trova dopo il simbolo ‘?’ (che serve per la creazione dinamica dei contenuti, vedi articolo: Un WEB server più dinamico).
Se la risorsa corrisponde ad un nome di file presente, viene chiamata la funzione
invia_risorsa, altrimenti viene trasmessa una pagina “non trovato” con codice errore 404.
Con la funzione
determina_tipo, si determina il mimetype della risorsa grazie all’estensione del nome del file. Un tipo sconosciuto viene considerato text/plain.