http.server - Classe Base per Implementare Server Web

Scopo: Include classi che possono formare le basi un server web.

Il modulo http.server usa classi dal modulo socketserver per creare classi base per costruire server HTTP. HTTPServer può essere usato direttamente, ma la classe BaseHTTPRequestHandler è concepita per essere estesa per gestire ciascun metodo del protocollo (GET, POST, ecc.).

HTTP GET

Per aggiungere supporto ad un metodo HTTP in una classe per la gestione di richieste, si implementi il metodo do_METODO) sostituendo METODO con il nome del metodo HTTP (es. do_GET(), do_POST(), ecc.). Per consistenza, i metodi per la gestione della richiesta non richiedono argomenti. Tutti i parametri per la richiesta sono elaborati da BaseHTTPRequestHandler e conservati come attributi di istanza nell'istanza della richiesta.

Questo esempio di gestore di richiesta illustra come ritornare una risposta al client, ed alcuni degli attributi locali che possono essere utili nella costruzione della risposta.

# http_server_GET.py

from http.server import BaseHTTPRequestHandler
from urllib import parse


class GetHandler(BaseHTTPRequestHandler):

    def do_GET(self):
        parsed_path = parse.urlparse(self.path)
        message_parts = [
            'VALORI DEL CLIENT:',
            'indirizzi client={} ({})'.format(
                self.client_address,
                self.address_string()),
            'comando={}'.format(self.command),
            'percorso={}'.format(self.path),
            'percorso reale={}'.format(parsed_path.path),
            'query={}'.format(parsed_path.query),
            'versione richiesta={}'.format(self.request_version),
            '',
            'VALORI DEL SERVER:',
            'versione server={}'.format(self.server_version),
            'versione sys={}'.format(self.sys_version),
            'versione protocollo={}'.format(self.protocol_version),
            '',
            'INTESTAZIONI RICEVUTE:',
        ]
        for name, value in sorted(self.headers.items()):
            message_parts.append(
                '{}={}'.format(name, value.rstrip())
            )
        message_parts.append('')
        message = '\r\n'.join(message_parts)
        self.send_response(200)
        self.send_header('Content-Type',
                         'text/plain; charset=utf-8')
        self.end_headers()
        self.wfile.write(message.encode('utf-8'))


if __name__ == '__main__':
    from http.server import HTTPServer
    server = HTTPServer(('localhost', 8080), GetHandler)
    print('Avvio del server, usare <Ctrl-C> per interrompere')
    server.serve_forever()

Il testo del messaggio viene assemblato, quindi scritto a wfile, l'handle che incapsula il socket di risposta. Ogni risposta deve avere un codice di risposta, impostato tramite send_response(). Se viene usato un codice di errore (404, 501, ecc.), viene incluso nell'intestazione un appropriato messaggio di errore predefinito, oppure un messaggio può essere passato con il codice di errore.

Per eseguire il gestore di richiesta nel server, lo si passi al costruttore di HTTPServer, come indicato nella parte __main__ del codice di esempio qui sopra.

Per far partire il server:

$ python3 http_server_GET.py

Avvio del server, usare <Ctrl-C> per interrompere

In un altro terminale usare il programma curl per accedere:

$ curl -v -i http://127.0.0.1:8080/?foo=bar

* Trying 127.0.0.1...
* Connected to 127.0.0.1 (127.0.0.1) port 8080 (#0)
> GET /?foo=bar HTTP/1.1
> Host: 127.0.0.1:8080
> User-Agent: curl/7.50.1
> Accept: */*
>
* HTTP 1.0, assume close after body
< HTTP/1.0 200 OK
HTTP/1.0 200 OK
< Server: BaseHTTP/0.6 Python/3.5.2+
Server: BaseHTTP/0.6 Python/3.5.2+
< Date: Sun, 04 Jun 2017 08:44:41 GMT
Date: Sun, 04 Jun 2017 08:44:41 GMT
< Content-Type: text/plain; charset=utf-8
Content-Type: text/plain; charset=utf-8

<
VALORI DEL CLIENT:
indirizzi client=('127.0.0.1', 55180) (127.0.0.1)
comando=GET
percorso=/?foo=bar
percorso reale=/
query=foo=bar
versione richiesta=HTTP/1.1

VALORI DEL SERVER:
versione server=BaseHTTP/0.6
versione sys=Python/3.5.2+
versione protocollo=HTTP/1.0

INTESTAZIONI RICEVUTE:
Accept=*/*
Host=127.0.0.1:8080
User-Agent=curl/7.50.1
* Closing connection 0
L'output potrebbe variare a seconda delle versioni di curl. Se l'esecuzione degli esempi produce output diversi, verificare il numero di versione riportato da curl.

HTTP POST

Per supportare richieste POST è necessario un poco più di lavoro, visto che la classe base non elabora automaticamente i dati del form. Il modulo cgi fornisce la classe FieldStorage che è in grado di elaborare il form, se fornita dei corretti input.

# http_server_POST.py

import cgi
from http.server import BaseHTTPRequestHandler
import io


class PostHandler(BaseHTTPRequestHandler):

    def do_POST(self):
        # Parse the form data posted
        form = cgi.FieldStorage(
            fp=self.rfile,
            headers=self.headers,
            environ={
                'REQUEST_METHOD': 'POST',
                'CONTENT_TYPE': self.headers['Content-Type'],
            }
        )

        # Inizio della risposta
        self.send_response(200)
        self.send_header('Content-Type',
                         'text/plain; charset=utf-8')
        self.end_headers()

        out = io.TextIOWrapper(
            self.wfile,
            encoding='utf-8',
            line_buffering=False,
            write_through=True,
        )

        out.write('Client: {}\n'.format(self.client_address))
        out.write('User-agent: {}\n'.format(
            self.headers['user-agent']))
        out.write('Percorso: {}\n'.format(self.path))
        out.write('Dati del form:\n')

        # Ritorna le informazioni di ciò che era stato inviato nel form
        for field in form.keys():
            field_item = form[field]
            if field_item.filename:
                # Il campo contiene un file inviato
                file_data = field_item.file.read()
                file_len = len(file_data)
                del file_data
                out.write(
                    '\tUploaded {} as {!r} ({} bytes)\n'.format(
                        field, field_item.filename, file_len)
                )
            else:
                # Valori del normali
                out.write('\t{}={}\n'.format(
                    field, form[field].value))

        # Disconnette il wrapper di codifica dal buffer sottostante
        # in modo che l'eliminazione del wrapper non chiuda
        # il socket, che è ancora in uso dal server.
        out.detach()


if __name__ == '__main__':
    from http.server import HTTPServer
    server = HTTPServer(('localhost', 8080), PostHandler)
    print('Avvio del server, usare <Ctrl-C> per interrompere')

    server.serve_forever()

Eseguire il server in un terminale:

$ python3 http_server_POST.py

Server in esecuzione, usare <Ctrl-C> per terminare

Gli argomenti per curl possono includere dati di un form da inviare al server tramite l'opzione -F. L'ultimo argomento, -F datafile=@http_server_GET.py, invia il contenuto del file http_server_GET.py per illustrare la lettura di un file di dati dal form.

$ curl -v http://127.0.0.1:8080/ -F name=dhellmann -F foo=bar -F datafile=@http_server_GET.py
*   Trying 127.0.0.1...
* Connected to 127.0.0.1 (127.0.0.1) port 8080 (#0)
> POST / HTTP/1.1
> Host: 127.0.0.1:8080
> User-Agent: curl/7.50.1
> Accept: */*
> Content-Length: 2025
> Expect: 100-continue
> Content-Type: multipart/form-data; boundary=------------------------bd06a03b251d489f
>
* Done waiting for 100-continue
* HTTP 1.0, assume close after body
< HTTP/1.0 200 OK
< Server: BaseHTTP/0.6 Python/3.5.2+
< Date: Sun, 04 Jun 2017 08:55:23 GMT
< Content-Type: text/plain; charset=utf-8
<
Client: ('127.0.0.1', 34358)
User-agent: curl/7.50.1
Percorso: /
Dati del form:
    foo=bar
    Uploaded datafile as 'http_server_GET.py' (1614 bytes)
    name=dhellmann
* Closing connection 0

Threading e Forking

Per aggiungere caratteristiche di threading e forking, occorre creare una nuova classe usando il mix-in appropriato da socketserver, visto che HTTPServer è una semplice sottoclasse di socketserver.TCPServer e non usa thread multipli o processi per gestire le richieste.

# http_server_threads.py

from http.server import HTTPServer, BaseHTTPRequestHandler
from socketserver import ThreadingMixIn
import threading


class Handler(BaseHTTPRequestHandler):

    def do_GET(self):
        self.send_response(200)
        self.send_header('Content-Type',
                         'text/plain; charset=utf-8')
        self.end_headers()
        message = threading.currentThread().getName()
        self.wfile.write(message.encode('utf-8'))
        self.wfile.write(b'\n')


class ThreadedHTTPServer(ThreadingMixIn, HTTPServer):
    """Gestisce le richieste in un thread separato."""


if __name__ == '__main__':
    server = ThreadedHTTPServer(('localhost', 8080), Handler)
    print('Avvio del server, usare <Ctrl-C> per interrompere')

    server.serve_forever()

Eseguire il server allo stesso modo degli altri esempi.

$ python3 http_server_threads.py

Server in esecuzione, usare <Ctrl-C> per terminare

Ogni volta che il server riceve una richiesta, inizia un nuovo thread o processo per gestirla.

$ curl  http://127.0.0.1:8080/

Thread-1

$ curl  http://127.0.0.1:8080/

Thread-2

$ curl  http://127.0.0.1:8080/

Thread-3

Scambiando ForkingMixIn con ThreadingMixIn si sarebbero ottenuti gli stessi risultati, usando processi separati in luogo dei thread.

Gestione degli Errori

Si gestiscano gli errori chiamando send_error(), passando il codice di errore appropriato ed il messaggio di errore opzionale. L'intera risposta (con intestazioni, codice di stato e corpo) viene generata automaticamente.

# http_server_errors.py

from http.server import BaseHTTPRequestHandler


class ErrorHandler(BaseHTTPRequestHandler):

    def do_GET(self):
        self.send_error(404)


if __name__ == '__main__':
    from http.server import HTTPServer
    server = HTTPServer(('localhost', 8080), ErrorHandler)
    print('Avvio del server, usare <Ctrl-C> per interrompere')

    server.serve_forever()

In questo caso, viene sempre ritornato un errore 404.

$ python3 http_server_errors.py

Server in esecuzione, usare <Ctrl-C> per terminare

Il messaggio di errore viene riportato al client usando un documento HTML assieme all'intestazione per indicare un codice di errore.

$ curl -i  http://127.0.0.1:8080/

HTTP/1.0 404 Not Found
Server: BaseHTTP/0.6 Python/3.5.2+
Date: Sun, 04 Jun 2017 09:15:25 GMT
Connection: close
Content-Type: text/html;charset=utf-8
Content-Length: 447

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"
        "http://www.w3.org/TR/html4/strict.dtd">
<html>
    <head>
        <meta http-equiv="Content-Type" content="text/html;charset=utf-8">
        <title>Error response</title>
    </head>
    <body>
        <h1>Error response</h1>
        <p>Error code: 404</p>
        <p>Message: Not Found.</p>
        <p>Error code explanation: 404 - Nothing matches the given URI.</p>
    </body>
</html>

Impostare Intestazioni

Il metodo send_header() aggiunge dati di intestazione alla risposta HTTP. Richiede due argomenti: il nome dell'intestazione ed il valore.

# http_server_send_header.py

from http.server import BaseHTTPRequestHandler
import time


class GetHandler(BaseHTTPRequestHandler):

    def do_GET(self):
        self.send_response(200)
        self.send_header(
            'Content-Type',
            'text/plain; charset=utf-8',
        )
        self.send_header(
            'Last-Modified',
            self.date_time_string(time.time())
        )
        self.end_headers()
        self.wfile.write('Response body\n'.encode('utf-8'))


if __name__ == '__main__':
    from http.server import HTTPServer
    server = HTTPServer(('localhost', 8080), GetHandler)
    print('Avvio del server, usare <Ctrl-C> per interrompere')

    server.serve_forever()

Questo esempio imposta l'intestazione Last-Modified all'orario corrente, formattato secondo le specifiche RFC 7231.

$ curl -i  http://127.0.0.1:8080/

HTTP/1.0 200 OK
Server: BaseHTTP/0.6 Python/3.5.2+
Date: Sun, 04 Jun 2017 09:20:47 GMT
Content-Type: text/plain; charset=utf-8
Last-Modified: Sun, 04 Jun 2017 09:20:47 GMT

Response body

Il server registra la richiesta nel terminale, come negli altri esempi.

$ python3 http_server_send_header.py

Server in esecuzione, usare <Ctrl-C> per terminare
127.0.0.1 - - [04/Jun/2017 11:20:47] "GET / HTTP/1.1" 200 -

Utilizzo da Riga di Comando

http.server include un server built-in per servire file dal file system locale. Per farlo partire dalla riga di comando usare l'opzione -m per l'interprete Python.

$ python3 -m http.server 8080

Serving HTTP on 0.0.0.0 port 8080 ...
127.0.0.1 - - [04/Jun/2017 11:26:32] "HEAD /http_server_GET.py HTTP/1.1" 200 -

La directory radice del server è la directory di lavoro dalla quale il server è stato fatto partire.

$ curl -I http://127.0.0.1:8080/http_server_GET.py

HTTP/1.0 200 OK
Server: SimpleHTTP/0.6 Python/3.5.2+
Date: Sun, 04 Jun 2017 09:26:32 GMT
Content-type: text/plain
Content-Length: 1614
Last-Modified: Sun, 04 Jun 2017 08:32:47 GMT

Vedere anche:

http.server
La documentazione della libreria standard per questo modulo
socketserver
Creare server di rete
RFC 7231
"Hypertext Transfer Protocol (HTTP/1.1): Semantics and Content" include specifiche per il formato delle intestazioni HTTP e delle date.