WEB server / Application server

Sommario

Struttura base

Il server e' un'istanza della classe HTTPServer. Si passa l'indirizzo+porta e la classe di gestione delle richieste derivata da BaseHTTPRequestHandler, infine si chiama il metodo serve_forever che rende funzionante e ricettivo il server.
I due metodi do_GET e do_POST del gestore rispondono alle request HTTP di tipo GET (le variabili sono fornite assieme all'indirizzo) e POST (le variabili sono fornite tramite un form HTML).

import BaseHTTPServer

#---------------------------------------------------------------------

class Gest(BaseHTTPServer.BaseHTTPRequestHandler):

    def do_GET(self):
        pass


    def do_POST(self):
        pass

#---------------------------------------------------------------------

ADDR = ("localhost", 80)     # Indirizzo locale server
BaseHTTPServer.HTTPServer(ADDR, Gest).serve_forever() 


Hello world!

Un po' di codice per ottenere una risposta minimale, l'hello world del web server, qualunque richiesta di tipo GET ottiene di ritorno sempre la stessa pagina contenuta nella stringa p.

import BaseHTTPServer

#---------------------------------------------------------------------

class Gest(BaseHTTPServer.BaseHTTPRequestHandler):

    def do_GET(self):
        p = """
            <html>
            <head></head>
            <body>
            <div style="text-align:center">
            <h1>Hello world!<br>WEB Server Python here</h1>
            </div>
            </body>
            </html>
            """
        self.send_response(200)
        self.send_header("Content-Type", "text/html")
        self.send_header("Content-length", str(len(p)))
        self.end_headers()
        self.wfile.write(p)
        self.wfile.flush()
        self.connection.shutdown(1)


    def do_POST(self):
        pass

#---------------------------------------------------------------------

ADDR = ("localhost", 80)     # Indirizzo locale server
BaseHTTPServer.HTTPServer(ADDR, Gest).serve_forever() 


Pagine caricate da file

Con la richiesta HTTP arriva una stringa (riportata in self.path) che contiene il percorso del file desiderato. Se non si specifica alcun percorso/file viene ritornato comunque almeno uno slash "/". Il percorso si intende relativo alla directory in cui si trova il server e tutte le sottodirectory.
Una pagina puo' essere composta da piu' elementi (come le immagini) e pertanto il browser puo' effettuare piu' richieste in sequenza per recuperare una alla volta tutte le informazioni necessarie.
Per ogni tipo di file richiesto il server deve inviare nella risposta il Content-Type e la Content-length relativi.


import BaseHTTPServer, urllib

#---------------------------------------------------------------------

class Gest(BaseHTTPServer.BaseHTTPRequestHandler):

    tipi = {  ".css": "text/css",    ".gif": "image/gif",
              ".jpg": "image/jpg",   ".png": "image/png",
              "html": "text/html",   ".htm": "text/html"  }


    def tipo(self, nomeFile):
        a = nomeFile[-4:]
        return (a in self.tipi) and self.tipi[a] or 'text/plain'


    def scomponi_url(self):
        a = urllib.unquote_plus(self.path.replace("&amp;", "&"))
        a = a.split("?", 1)
        return a[0][1:].lower()


    def do_GET(self):
        nomeFile = self.scomponi_url()
        p = file(nomeFile, "rb").read()
        self.send_response(200)
        self.send_header("Content-Type", self.tipo(nomeFile))
        self.send_header("Content-length", str(len(p)))
        self.end_headers()
        self.wfile.write(p)
        self.wfile.flush()
        self.connection.shutdown(1)


    def do_POST(self):
        pass

#---------------------------------------------------------------------

ADDR = ("localhost", 80)     # Indirizzo locale server
BaseHTTPServer.HTTPServer(ADDR, Gest).serve_forever() 




Errori e sicurezza

Il server precedente e' in grado di fornire pagine complete tramite richieste GET ma presenta diversi problemi. Tutte queste cose vengono risolte con le modifiche seguenti, con la costante WDIR si puo' anche specificare una diversa unita' disco. Tutti i file si intendono relativi ad essa e alle eventuali sottodirectory.
In caso di file non recuperabile il server fornisce correttamente l'errore 404.
Se non si specifica alcun file vengono cercati in sequenza index.htm e index.html.

import BaseHTTPServer, urllib, os

WDIR = "C:/www/"             # Directory ricerca file
ADDR = ("localhost", 80)     # Indirizzo locale server

#---------------------------------------------------------------------

class Gest(BaseHTTPServer.BaseHTTPRequestHandler):

    tipi = {  ".css": "text/css",    ".gif": "image/gif",
              ".jpg": "image/jpg",   ".png": "image/png",
              "html": "text/html",   ".htm": "text/html"  }


    def tipo(self, nomeFile):
        a = nomeFile[-4:]
        return (a in self.tipi) and self.tipi[a] or 'text/plain'



    def norm_path(self, p):
        if not p:
            if os.path.exists(WDIR + "index.htm"):
                p = "index.htm"
            else:
                p = "index.html"
        p = WDIR + p
        if not os.path.exists(p):
            if p[-4:] in [ ".gif", ".jpg", ".png" ]:
                p = WDIR + "placeholder.jpg"
        return p        



    def scomponi_url(self):
        a = urllib.unquote_plus(self.path.replace("&amp;", "&"))
        a = a.split("?", 1)
        p = a[0][1:].lower()
        return self.norm_path(p)



    def do_GET(self):
        nomeFile = self.scomponi_url()
        try:
            p = file(nomeFile, "rb").read()
            self.send_response(200)
            self.send_header("Content-Type", self.tipo(nomeFile))
            self.send_header("Content-length", str(len(p)))
            self.end_headers()
            self.wfile.write(p)
            self.wfile.flush()
            self.connection.shutdown(1)
        except IOError:
            self.send_error(404, "File '%s' non trovato" % nomeFile)



    def do_POST(self):
        nomeFile = self.scomponi_url()
        try:        
            p = file(nomeFile, "rb").read()
            self.send_response(301)
            self.send_header("Content-Type", self.tipo(nomeFile))
            self.send_header("Content-length", str(len(p)))
            self.end_headers()
            self.wfile.write(p)
            self.wfile.flush()
            self.connection.shutdown(1)
        except IOError:
            self.send_error(404, "File '%s' non trovato" % nomeFile)

#---------------------------------------------------------------------

BaseHTTPServer.HTTPServer(ADDR, Gest).serve_forever() 




Accesso alle variabili fornite dal browser

Per la visualizzazione di semplici pagine statiche non e' necessario altro, tuttavia per permettere al server di svolgere funzioni di elaborazione piu' complesse e' necessario ricavare dalla request HTTP le variabili fornite tramite il metodo GET o POST.
Il server seguente ricava (anche se ancora non le usa) le variabili in entrambi i casi e le mette in un dizionario chiamato reqData.

import BaseHTTPServer, urllib, os

WDIR = "C:/www/"             # Directory ricerca file
ADDR = ("localhost", 80)     # Indirizzo locale server

#---------------------------------------------------------------------

class Gest(BaseHTTPServer.BaseHTTPRequestHandler):

    tipi = {  ".css": "text/css",    ".gif": "image/gif",
              ".jpg": "image/jpg",   ".png": "image/png",
              "html": "text/html",   ".htm": "text/html"  }


    def tipo(self, nomeFile):
        a = nomeFile[-4:]
        return (a in self.tipi) and self.tipi[a] or 'text/plain'



    def scomponi_var(self, dati):
        v = {}
        for a in dati.split("&"):
            b = a.split("=", 1)
            v[b[0]] = (len(b) > 1) and b[1] or ""
        return v



    def leggi_postvars(self):
        len_dati = int(self.headers.getheader('content-length'))
        dati = urllib.unquote_plus(self.rfile.read(len_dati))
        return self.scomponi_var(dati)



    def norm_path(self, p):
        if not p:
            if os.path.exists(WDIR + "index.htm"):
                p = "index.htm"
            else:
                p = "index.html"
        p = WDIR + p
        if not os.path.exists(p):
            if p[-4:] in [ ".gif", ".jpg", ".png" ]:
                p = WDIR + "placeholder.jpg"
        return p        



    def scomponi_url(self):
        a = urllib.unquote_plus(self.path.replace("&amp;", "&"))
        a = a.split("?", 1)
        p = a[0][1:].lower()
        getVars = (len(a) > 1) and self.scomponi_var(a[1]) or {}
        return self.norm_path(p), getVars


        
    def do_GET(self):
        nomeFile, reqData = self.scomponi_url()
        try:
            p = file(nomeFile, "rb").read()
            self.send_response(200)
            self.send_header("Content-Type", self.tipo(nomeFile))
            self.send_header("Content-length", str(len(p)))
            self.end_headers()
            self.wfile.write(p)
            self.wfile.flush()
            self.connection.shutdown(1)
        except IOError:
            self.send_error(404, "File '%s' non trovato" % nomeFile)



    def do_POST(self):
        nomeFile, _ = self.scomponi_url()
        reqData = self.leggi_postvars()
        try:        
            p = file(nomeFile, "rb").read()
            self.send_response(301)
            self.send_header("Content-Type", self.tipo(nomeFile))
            self.send_header("Content-length", str(len(p)))
            self.end_headers()
            self.wfile.write(p)
            self.wfile.flush()
            self.connection.shutdown(1)
        except IOError:
            self.send_error(404, "File '%s' non trovato" % nomeFile)

#---------------------------------------------------------------------

BaseHTTPServer.HTTPServer(ADDR, Gest).serve_forever() 




Pagine dinamiche e funzioni arbitrarie

Il server seguente e' un esempio "hello world" dinamico, tutti gli url che terminano con .py causano la chiamata della funzione elabora a cui vengono passati il path, le variabili della request e un dizionario con le eventuali variabili dell'applicazione.
Questa funzione, se strutturata come macchina a stati finiti che viene chiamata ad ogni richiesta del browser, puo' servire per realizzare applicazioni web based che accedono ad ogni risorsa del computer.
La funzione deve resituire come risultato una pagina HTML.

#---------------------------------------------------------------------
#               ESEMPIO RICHIAMO FUNZIONE DA WEB SERVER
#
#                      Di: Claudio Fin 2010
#                  Ultimo aggiornamento: 2/9/2010
#---------------------------------------------------------------------

import BaseHTTPServer, urllib, os, time

appData = {}                 # Dati applicazione
WDIR = "C:/www/"             # Directory ricerca file
ADDR = ("localhost", 80)     # Indirizzo locale web server

#---------------------------------------------------------------------
#                  FUNZIONE / APPLICAZIONE WEB
#---------------------------------------------------------------------

def elabora(nomeFile, reqData):
    p =  '''
         <html>
         <body style="background-color:white;  margin:64px;
               text-align:center; font-size:24px">
         <h1>Hello world!<br>Dynamic contents here...</h1>
         '''
    p += '<h1>' + time.strftime("%H:%M",time.localtime()) + '</h1>\n'
    p += '<h1>' + time.strftime("%d-%m-%Y",time.localtime()) +'</h1>\n'
    p += '''
         <a href="index.html"><b>INDEX</b></a>
         <br><br><br>
         '''
    p += '<b>reqData = ' + repr(reqData) + '</b>\n'
    p += '''
         <br><br><br>
         <form action="dinamico.py" method="POST">
         <input type="text" value="" size=50 name="campone">
         <input type="submit" value="Invio">
         </form>
         </body>
         </html>
         '''
    return p

#---------------------------------------------------------------------
#                           WEB SERVER
#---------------------------------------------------------------------

class Gest(BaseHTTPServer.BaseHTTPRequestHandler):


    tipi = {  ".css": "text/css",    ".gif": "image/gif",
              ".jpg": "image/jpg",   ".png": "image/png",
              "html": "text/html",   ".htm": "text/html"  }


    def tipo(self, nomeFile):
        a = nomeFile[-4:]
        return (a in self.tipi) and self.tipi[a] or "text/plain"


    def scomponi_var(self, dati):
        v = {}
        for a in dati.split("&"):
            b = a.split("=", 1)
            v[b[0]] = (len(b) > 1) and b[1] or ""
        return v


    def leggi_postvars(self):
        len_dati = int(self.headers.getheader("content-length"))
        dati = urllib.unquote_plus(self.rfile.read(len_dati))
        return self.scomponi_var(dati)


    def norm_path(self, p):
        if not p:
            if os.path.exists(WDIR + "index.htm"):
                p = "index.htm"
            else:
                p = "index.html"
        p = WDIR + p
        if not os.path.exists(p):
            if p[-4:] in [ ".gif", ".jpg", ".png" ]:
                p = WDIR + "placeholder.jpg"
        return p        


    def scomponi_url(self):
        a = urllib.unquote_plus(self.path.replace("&", "&"))
        a = a.split("?", 1)
        p = a[0][1:].lower()
        getVars = (len(a) > 1) and self.scomponi_var(a[1]) or {}
        return self.norm_path(p), getVars


    def do_GET(self):
        nomeFile, reqData = self.scomponi_url()
        try:
            if nomeFile.endswith(".py"):
                p = elabora(nomeFile, reqData)
                self.send_response(200)
                self.send_header("Content-Type", "text/html")
            else:
                p = file(nomeFile, "rb").read()
                self.send_response(200)
                self.send_header("Content-Type", self.tipo(nomeFile))
            self.send_header("Content-length", str(len(p)))
            self.end_headers()
            self.wfile.write(p)
            self.wfile.flush()
            self.connection.shutdown(1)
        except IOError:
            self.send_error(404, "File %s non trovato" % nomeFile)


    def do_POST(self):
        nomeFile, _ = self.scomponi_url()
        try:        
            if nomeFile.endswith(".py"):
                p = elabora(nomeFile, self.leggi_postvars())
                self.send_response(200)
                self.send_header("Content-Type", "text/html")
            else:
                p = file(nomeFile, "rb").read()
                self.send_response(200)
                self.send_header("Content-Type", self.tipo(nomeFile))
            self.send_header("Content-length", str(len(p)))
            self.end_headers()
            self.wfile.write(p)
            self.wfile.flush()
            self.connection.shutdown(1)
        except IOError:
            self.send_error(404, "File %s non trovato" % nomeFile)

#---------------------------------------------------------------------
#                               MAIN
#---------------------------------------------------------------------

BaseHTTPServer.HTTPServer(ADDR, Gest).serve_forever()