Un WEB server più dinamico

>>> LINK A PAGINA AGGIORNATA <<<

  • .
  • .
  • .
  • .
  • .
  • .
  • .
  • .
  • .
  • .
  • .
  • .
  • .
  • .
  • .
  • .
  • .
  • .
  • .
  • .
  • .
  • .

Creazione di contenuti dinamici

Dinamicità significa creazione di contenuti “al volo” su richiesta, magari in base a parametri scelti di volta in volta. Oppure richiedere al server operazioni più complesse della semplice preparazione di pagine, e poter in qualche modo interagire con i contenuti stessi, come si farebbe con una comune applicazione.

Un primo grado di dinamicità si può ottenere richiamando periodicamente un URL specifico, che invece di essere un file su disco, avvia la creazione “sul momento” di un nuovo contenuto aggiornato.

Per quanto riguarda l’aggiornamento automatico, il modo più semplice (anche se un po’ limitato) è inserire nella sezione head di una pagina HTML un tag meta che indica ogni quanti secondi la pagina deve “ricaricarsi”:


<meta http-equiv="refresh" content="5">

Il WEB server naturalmente deve poter distinguere tra la richiesta di un contenuto statico (file su disco) e uno dinamico (creato al momento).

Tipicamente questo riconoscimento avviene grazie all’estensione della risorsa richiesta: .php .asp .cgi .jsp ecc.

Se il server è scritto da noi, abbiamo libertà totale. Ad esempio possiamo decidere che tutte le risorse terminanti con .dyn vengano gestite da codice invece che essere cercate su disco.

Per fare questo basta una piccolissima modifica all’handler del server statico dell’articolo precedente. In questo modo ogni “pagina” terminante con .dyn attiva una apposita funzione di gestione. Tutti gli altri file invece vengono trattati normalmente.


    def do_GET(self):
        richiesta = unquote_plus(self.path)
        risorsa = richiesta.split('?', 1)[0]
        if risorsa.endswith('.dyn'):
            dinamica(self, risorsa)
        elif os.path.isfile(BASEDIR + risorsa):
            invia_risorsa(self, risorsa)
        else:  
            self.send_error(
                404, '{} Not found'.format(risorsa))

Nella funzione di gestione dinamica si può eventualmente scegliere tra diverse richieste/funzioni:


def dinamica(hdl, risorsa):
    if risorsa.endswith('funzione1.dyn'):
        .....
        return 

    if risorsa.endswith('funzione2.dyn'):
        .....
        return 

    if risorsa.endswith('funzione3.dyn'):
        .....
        return

    hdl.send_error(
        404, '{} Not found'.format(risorsa))

Di seguito un esempio di grafica creata sul momento (il programma è completo, ma ridotto alla sola parte di gestione dinamica). Viene visualizzata una pagina che si ricarica ogni cinque secondi. Si usa la libreria grafica Pillow (fork di PIL), che permette la creazione di qualsiasi grafica bitmap. L’immagine png ottenuta non viene salvata su disco, ma in memoria grazie all’ oggetto BytesIO che si comporta come un file. I dati contenuti nel file vengono codificati in base64, per trasformarli nei caratteri testuali da trasmettere nel formato HTML previsto per le immagini incorporate. Questi dati vengono inseriti nella stringa immagine_dinamica, e questa stringa viene a sua volta inserita nella stringa pagina_refresh, ottenendo la pagina completa da trasmettere al browser.


from urllib.parse import unquote_plus
from http.server  import HTTPServer
from http.server  import BaseHTTPRequestHandler
import base64
import random
import io
from PIL import Image      #PIL / Pillow
from PIL import ImageDraw  #PIL / Pillow
#https://www.pythonforbeginners.com/gui/how-to-use-pillow
#https://pillow.readthedocs.io/en/3.0.x/reference/ImageDraw.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)
#___________________________________________________________

pagina_refresh = u'''<!DOCTYPE html>
<html lang="it">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="refresh" content="5">
</head>
<body style="background-color:gray;">
{}
</body>
</html>'''

immagine_dinamica = u'''
<div style="text-align:center;">
<img style="border:1px solid white;"
     src="data:image/png;base64,{}">
</div>'''
#___________________________________________________________

def dinamica(hdl, risorsa):
    if risorsa.endswith('fn0.dyn'):
        immagine = Image.new('RGB', (300, 200), 'green')
        tracciatore = ImageDraw.Draw(immagine)
        x = 20
        while x < 270:
            y = random.randrange(180)
            tracciatore.rectangle(
                (x, 190, x+20, 190-y),
                 outline='green', fill='yellow'
            )
            x += 20
        mem_file = io.BytesIO()
        immagine.save(mem_file, format='png')
        mem_file.seek(0)
        contenuto = base64.b64encode(mem_file.read())
        contenuto = contenuto.decode('utf-8')
        mem_file.close()
        contenuto = immagine_dinamica.format(contenuto)
        contenuto = pagina_refresh.format(contenuto)
        contenuto = contenuto.encode('utf-8')
        trasmetti(hdl, 200, 'text/html', contenuto)
        return        

    hdl.send_error(
        404, '{} Not found'.format(risorsa))
#___________________________________________________________

class Gestore(BaseHTTPRequestHandler):

    def do_GET(self):
        richiesta = unquote_plus(self.path)
        risorsa = richiesta.split('?', 1)[0]
        if risorsa.endswith('.dyn'):
            dinamica(self, risorsa)
        else:  
            self.send_error(
                404, '{} Not found'.format(risorsa))

    def do_POST(self):
        pass
#___________________________________________________________

indirizzo = '', 8000
server = HTTPServer(indirizzo, Gestore)
try:
    server.serve_forever()
except KeyboardInterrupt:
    server.socket.close()

Immagine dinamicaPoter creare delle pagine “al volo”, grafica compresa, ed effettuare anche il refresh automatico, ha già una certa utilità, ad esempio permette la lettura remota aggiornata di sensori o allarmi.

 

 

Le variabili nelle richieste GET

Le variabili sono informazioni di controllo aggiuntive che il browser invia al server con le richieste. La parte di richiesta contenente le variabili si chiama query string, ed è separata dall’URL dal simbolo punto interrogativo.

Ad esempio una richiesta come la seguente non solo richiede al nostro server una pagina dinamica, ma include le due variabili ‘p’ e ‘z’, che possono essere lette dal server stesso per preparare la pagina in modi diversi.

fn1.dyn?p=10&z=SERVI DELLA PLEBE

Queste variabili possono essere inserite a mano nel campo indirizzo del browser, oppure possono essere già contenute in un link. Sono trasmesse assieme all’URL, quindi con richiesta HTTP di tipo GET.

Possono essere facilmente estratte dalla query string  e inserite in un dizionario.


def estrai_variabili(query):
    variabili = {}
    for elemento in query.split('&'):
        parti = elemento.split('=', 1)
        if len(parti) > 1:
            variabili[parti[0]] = parti[1]
        else:
            variabili[parti[0]] = ''
    return variabili

Questa la modifica all’handler:


    def do_GET(self):
        richiesta = unquote_plus(self.path)
        parti = richiesta.split('?', 1)
        risorsa = parti[0]
        query = '' if len(parti) < 2 else parti[1]
        variabili = estrai_variabili(query)
        if risorsa.endswith('.dyn'):
            dinamica(self, risorsa, variabili)
        elif os.path.isfile(BASEDIR + risorsa):
            invia_risorsa(self, risorsa)
        else:  
            self.send_error(
                404, '{} Not found'.format(risorsa))

La funzione dinamica seguente legge il dizionario delle variabili e produce una pagina contente ‘p’ volte il messaggio ‘z’… provate a farlo senza le variabili 😉


def numero_intero_positivo(x):
    try:
        return int(x) >= 0
    except ValueError:
        return False
#___________________________________________________________

pagina1 = u'''<!DOCTYPE html>
<html lang="it">
<head>
<meta charset="UTF-8">
</head>
<body>
{}
</body></html>'''

def dinamica(hdl, risorsa, variabili):
    if risorsa.endswith('fn1.dyn'):
        if not 'p' in variabili \
        or not 'z' in variabili \
        or not numero_intero_positivo(variabili['p']):
            hdl.send_error(400, 'Bad request')
            return
        contenuto = u'{}<br />\n'.format(variabili['z'])
        contenuto *= int(variabili['p'])
        contenuto = pagina1.format(contenuto)
        contenuto = contenuto.encode('utf-8')
        trasmetti(hdl, 200, 'text/html', contenuto)        
        return

    hdl.send_error(
        404, '{} Not found'.format(risorsa))

Riassumendo: con le variabili si può specificare di volta in volta cosa si vuole ottenere e come.

 

 

I form

Scrivere le variabili a mano nella barra dell’indirizzo può essere molto scomodo, e in più è una comunicazione unidirezionale.

Fino dalle prime versioni di HTML è stata prevista di base la possibilità di compilare dei moduli, chiamati form, da inviare al server per essere elaborati e compiere specifiche operazioni.

I form sono formati da campi di input di diversi tipi, al momento della richiesta HTTP i nomi dei campi e i loro valori vengono usati per comporre la query string da inviare al server.

Alcune informazioni contenute nei form possono essere prodotte  dal server stesso, e possono essere rilette nella successiva richiesta HTTP. Questo permette un flusso di informazioni bidirezionale o circolare, e quindi la realizzazione di “pagine” dotate di “stato” che si comportano come vere e proprie applicazioni (anche multiutente).

L’esempio seguente mostra come si potrebbero comandare quattro luci attraverso una richiesta dinamica.

La presenza o assenza delle variabili L1 L2 L3 L4 nella richiesta indica quali luci devono essere accese e quali spente.

Schermata form di controllo luciÈ indifferente scrivere le variabili a mano nel campo indirizzo  dare Invio, oppure selezionare le checkbox e poi premere il pulsante ‘Aggiorna’.

Se si scrivono le variabili a mano, allora nella pagina di risposta si ritroveranno le corrispondenti checkbox selezionate (informazioni di stato da server a pagina).

Se si selezionano le checkbox e si preme ‘Aggiorna’, allora all’URL vengono automaticamente aggiunte le variabili corrispondenti (informazioni da pagina a server).

Il tutto si comporta quindi come un programma con interfaccia WEB che mantiene uno stato coerente e aggiornato della situazione.


pag_luci = u'''<!DOCTYPE html>
<html lang="it">
<head>
  <meta charset="UTF-8">
</head>
<body>
<div style="text-align:center;font-weight:bold;">
  <h1>STATO LUCI</h1>
  1={}&nbsp;&nbsp;2={}&nbsp;&nbsp;3={}&nbsp;&nbsp;4={}
</div>
<br /><br />
<form action="/fn2.dyn"
      method="get"
      style="border:1px solid black;
             padding:3em; text-align:center;">
  L1:<input type="checkbox" name="L1" value="" {}>
  &nbsp;&nbsp;&nbsp;
  L2:<input type="checkbox" name="L2" value="" {}>
  &nbsp;&nbsp;&nbsp;
  L3:<input type="checkbox" name="L3" value="" {}>
  &nbsp;&nbsp;&nbsp;
  L4:<input type="checkbox" name="L4" value="" {}>
  &nbsp;&nbsp;&nbsp;
  <input type="submit" value="Aggiorna">
</form> 
</body>
</html>'''

def dinamica(hdl, risorsa, variabili):
    if risorsa.endswith('fn2.dyn'):
        comanda_luce(1, 'L1' in variabili)
        comanda_luce(2, 'L2' in variabili)
        comanda_luce(3, 'L3' in variabili)
        comanda_luce(4, 'L4' in variabili)
        contenuto = pag_luci.format(
            'ON' if 'L1' in variabili else 'OFF',
            'ON' if 'L2' in variabili else 'OFF',
            'ON' if 'L3' in variabili else 'OFF',
            'ON' if 'L4' in variabili else 'OFF',
            'checked' if 'L1' in variabili else '',
            'checked' if 'L2' in variabili else '',
            'checked' if 'L3' in variabili else '',
            'checked' if 'L4' in variabili else '',
        )
        contenuto = contenuto.encode('utf-8')
        trasmetti(hdl, 200, 'text/html', contenuto)        
        return

In questo codice l’informazione di “stato” che il server invia con la pagina è semplicemente il parametro ‘checked’ delle checkbox. Per contenuti più generici si possono usare i campi hidden. Ad esempio ogni pagina potrebbe contenere un campo ‘control’ con un valore univoco, che ad ogni richiesta permette al server di identificare l’utente in un sistema multiutente, oppure lo stato della presentazione.


 <input type="hidden" name="control" value="0xF0A9EBC3">

È chiaro a questo punto che il concetto di WEB server, originariamente inteso come programma che fornisce semplicemente file su richiesta, è già ampiamente superato. Di fatto già con questi strumenti di base si può effettuare qualsiasi tipo di elaborazione remota.

Nota: Qui si sta parlando solo di principi. Per una applicazione reale ci sono grossi problemi di sicurezza da valutare. Ad esempio usare https al posto di http, consentire operazioni remote solo ad utenti correttamente loggati, riconoscere e respingere tentativi di accesso non autorizzati.

 

Codifica dei dati trasmessi con i form

I form permettono diversi tipi di input (password, text, radio, submit, reset, checkbox, hidden e altri). Nel caso di input testuali (input di tipo text) i testi per essere trasmessi vengono codificati, ad esempio la seguente stringa scritta in un campo input text:

a b+c

viene trasmessa come:

 a+b%2Bc

Per decodificare correttamente le informazoni, nell’handler è necessario effettuare un’operazione unquote_plus. Quindi all’inizio del programma andrà scritto:


from urllib.parse import unquote_plus

 

 

Le variabili nelle richieste POST

Un form può generare anche una richiesta di tipo POST.

In questo caso le variabili non sono inviate come query string assieme all’URL, ma sono contenute nel corpo della richiesta (non sono visibili nella barra dell’indirizzo come avviene per le richieste GET).

Per gestire una richiesta POST, si deve implementare anche il metodo do_POST dell’handler.

Le variabili vanno lette dallo stream in arrivo (self.rfile), ed è indispensabile conoscere la quantità di byte da leggere, che si ottiene da self.headers.

Trattandosi di uno stream di byte, come sempre per ottenere i caratteri unicode occorre un decode utf-8 (l’operazione opposta rispetto a quando inviamo).

Con POST un form può inviare al server una quantità illimitata di dati, mentre con GET ci si limita a poche centinaia di caratteri (massimo 256 per vecchie versioni di browser).


    def do_POST(self):
        richiesta = unquote_plus(self.path)
        risorsa = richiesta.split('?', 1)[0]
        lunghezza = int(self.headers.get('Content-Length'))
        query = self.rfile.read(lunghezza).decode('utf-8')
        variabili = estrai_variabili(unquote_plus(query))
        if risorsa.endswith('.dyn'):
            dinamica(self, risorsa, variabili)
        elif os.path.isfile(BASEDIR + risorsa):
            invia_risorsa(self, risorsa)
        else:  
            self.send_error(
                404, '{} Not found'.format(risorsa))

 

 

(15/7/2018)

 

Lascia un commento

Il tuo indirizzo email non sarà pubblicato. I campi obbligatori sono contrassegnati *