Premier Training & Business Partner Red Hat

Creare middleware HTTP in Go

EXTRAORDY
Ti piacerebbe diventare anche tu uno di noi e
pubblicare i tuoi articoli nel blog degli RHCE italiani?

Il linguaggio Go è in costante sviluppo ed è sempre più apprezzato per lo sviluppo server-side, pur essendo (è importante ricordarlo) un linguaggio general-purpose. Il suo successo sta nella semplicità e nella chiarezza semantica, oltra alla sua elevata portabilità.

In questo blogpost ci occuperemo di approfondire meglio alcuni concetti relativo allo sviluppo web con Go. Nello specifico ci occuperemeo di gestire dei middleware attraverso un web server minimale di nostra creazione.
Talvolta si da per scontato il concetto di middleware ma se ci chiedessero una definizione semplice e intuitiva cosa risponderemmo?

Potremmo dire che un middleware è una entità che viene posta nel mezzo di un ciclo richiesta/risposta di un server.

Il middleware dunque “sta in mezzo”, e in questo senso il nome è una metafora molto efficace: dalla richiesta di un client (un browser web, ad esempio) fino all’arrivo al servizio target che contiene la logica di business possono essere posti tanti layer aggiuntivi che, in modo più o meno trasparente, andranno ad avere un impatto sul processamento della richiesta. Stesso dicasi per la restituzione al client della risposta.

 

Prendiamo come modello questa immagine:

 

In questo schema si possono notare quattro middleware diversi che si trovano tra la richiesta HTTP e un servizio API. Essi si occuperanno di funzioni differenti, e non tutti avranno necessariamente un impatto sulla user experience: logging, gestione sessione, autenticazione, funzioni custom.
Il logging ad esempio potrebbe registrare tutte le chiamate e le risposte HTTP in un handler dedicato, mentre il middleware di autenticazione potrebbe utilizzare un backend con Oauth2 per autenticare gli utenti tramite un token. Il CustomMiddelware potrebbe, ad esempio, alterare la risponsta del servizio API in modo da restituire a determinati User-Agent una risposta formattata in modo diverso.
Il concetto alla base, quello più importante è che non devo preoccuparmi di questi aspetti nello sviluppo del servizio API, dove invece potrò centrarmi soltanto sulla logica di business.

Questo è molto importante, ed è anche uno dei princpali motivi che hanno decretato il successo degli application server J2EE negli ultimi 20 anni.
Si pensi ad esempio al layer di autenticazione: se dovessimo implementare l’autenticazione nel nostro servizio o svilupparne uno dedicato solo a questo scopo avremmo un effort (anche in termini di costi e ore/uomo) notevolmente maggiore. Invece possiamo demandare questo aspetto ad un layer creato appositamente. Nel caso in cui l’autenticazione fallisca, la richiesta non verrà nemmeno inoltrata al servizio API.
Detto ciò, nulla ci vieta di sviluppare il nostro custom middleware e di riutilizzarlo su più progetti, cosa che faremo in questo blogpost.

 

Preparazione dell’environment

Per i seguenti esercizi sarà sufficiente un’installazione standard di Go. E la definizione di alcune variabili di ambiente standard.
Per installare Go su RHEL/CentOS:

$ sudo yum install golang

Su Fedora

$ sudo dnf install golang

Su Debian e derivate

$ sudo apt-get install golang

Una volta installato Go è necessario definire la variabile d’ambiente GOPATH che andremo subito a popolare con le directory standard.

$ export GOPATH=/home/user1
$ mkdir -p $GOPATH/{src,bin,pkg}

E’ buona pratica scolpire la variabile GOPATH nel file .bashrc o nel .bash_profile della propria home.

 

Tutti gli esempi citati in questo blogpost sono disponibili su GitHub al seguente link: https://github.com/giannisalinetti/blog-examples/tree/master/go-middleware.

Possiamo clonare tutto il repository utilizzando il tool git per lavorare in locale:

$ mkdir -p $GOPATH/src/github.com/giannisalinetti
$ cd $GOPATH/src/github.com/giannisalinetti
$ git clone https://github.com/giannisalinetti/blog-examples.git
$ cd blog-examples/go-middleware

Se non abbiamo git installarlo è facile:

Su RHEL/CentOS

$ sudo yum install git

Su Fedora

$ sudo dnf install git

Su Debian e derivate

$ sudo apt-get install git

 

Background: un web server minimale

Uno dei primi esempi che viene riportato nel capitolo introduttivo dell’ormai famoso testo “The Go Programming Language” di Alan Donovan e Brian Kernighan (si, proprio quel Kernighan) è proprio la creazione di un semplice web server HTTP che sia in grado restituire una stringa di testo a fronte di un metodo GET.
Riproponiamo questo esempio e commentiamolo brevemente al fine propedeutico di introdurre gradualmente il topic principale di questo blogpost.

 

httpServer.go

package main

import (
    "fmt"
    "log"
    "net/http"
)

func handler(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "Hello World!\n")
}

func main() {
    http.HandleFunc("/", handler)
    log.Fatal(http.ListenAndServe(":8080", nil))
}

Questo 16 righe di codice sono sufficienti ad implementeare un web server minimale che sia in grado di restuire la stringa “Hello World!” ad ogni richiesta.

Poiché è un singolo main non dobbiamo necessariamente compilare per eseguire questo web server, pertanto:

$ go run httpserver.go

 

Il webserver si avvierà e restarà in esecuzione. Apriamo un’altra finestra e testiamone le funzionalità:

$ curl http://localhost:8080
Hello World!

Funziona! Ma ora cerchiamo di capire meglio cosa è accaduto con un breve walkthrough.

 

package main

import (
    "fmt"
    "log"
    "net/http"
)

Oltre alla dichiarazione del package main vengono importati i package fmt, per la formattazoine del testo, log per il logging e il più importante per il nostro articolo net/http. Questo package include una serie di tipi, funzioni, metodi e interfacce che vengono usati abbondantemente nell’ambito dello sviluppo di applicazioni RESTful.

 

func handler(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "Hello World!\n")
}

Definiamo una funzione handler che dovrà soddisfare un’interfaccia definita nel package net/http, ovvero http.Handler. Questo tipo di funzioni prende sempre due argomenti: http.ResponseWriter, interfaccia che sua volta implementa l’interfaccia io.Writer e che ci permette di scrivere una risposta, e *http.Request, che definisce una struttura con il contenuto della richiesta (header, body, Cookie, ecc).
All’interno della funzione definiamo la logica del nostro server web minimale, ovvero stampare a schermo un “Hello World!”. Lo facciamo tramite la funzione fmt.Fprintf che scrive un testo formattato su una variabile che soddisfi io.Writer (e la nostra http.ResponseWriter lo fa).

Veniamo ora al main:

func main() {
    http.HandleFunc("/", handler)
    log.Fatal(http.ListenAndServe(":8080", nil))
}

La funzione main() è davvero piccola in questo caso.
Il primo statement associa ad un pattern “/” la funzione handler dichiarata prima. In pratica stiamo registrando la funzione su uno specifico path, in questo caso la root della nostra URL.

Successivamente mettiamo in ascolto il web server con la funzione http.ListenAndServe che accetta come argomenti l’indirizzo su cui mettersi in ascolto (qui abbiamo passato solo la porta 8080) e un eventuale handler.
La funzione restituisce solo un error che andremo a passare come argomento a log.Fatal che, nel caso di errore diverso da nil ne stamperà il contenuto su stderr e uscirà con exit code 1.

 

Il seguente link documenta il package net/http e mostra ulteriori esempi di implementazione.
Dalla nostra workstation possiamo utilizzare il comando godoc, presente nel pacchetto golang-godoc in RHEL/CentOS/Fedora.

$ godoc net/http

Consiglio profondamente di installare questo pacchetto e di utilizzarlo costantemente quando in dubbio sulle caratteristiche di una funzione o di un tipo.

Se ad esempio avessimo bisogno di leggere velocemente la documentazione relativa alla funzione  http.HandleFunc basterebbe digitare:

$ godoc net/http HandleFunc

Dopo questo rapido ripasso del package net/http possiamo finalmente inoltralci nello sviluppo del nostro middleware.

 

Multiplexer HTTP

Il web server minimale che abbiamo creato permette di definire in modo semplice un singolo handler, ma lascia molte domande in sospeso. Ad esempio, in che modo si potrebbero “smistare” le chiamate su diversi path nel caso in cui ci siano più di un endpoint, a cui rispondono diverse funzioni?
Per rispondere a questa domanda è necessario spiegare cosa è un multiplexer HTTP: è un componente che smista le richieste HTTP in arrivo, ed associa la URL di ogn richiesta ad una lista di pattern registrati richiamando poi l’handler associato. Lo possiamo immaginare come un router che gira a questa o quella funzione una richiesta in base al pattern che viene dato nella URL.
Nell’esempio precedente viene creato un default multiplexer in modo del tutto trasparente. Nel prossimo esempio si creerà un multiplexer custom, che chiameremo r.

Su questo nuovo multiplexer definiremo due metodi HandleFunc per registreare due path, “/date” e “/hello“, associati rispettivamente alle funzioni printDateprintHello.
La funzione printDate restituirà in output la data corrente (formattata secondo lo standard RFC3339) utilizzando time.Now().Format(time.RFC3339), mentre la funzione printHello restituirà il classicissimo Hello World!.

 

httpServerMux.go

package main

import (
    "fmt"
    "log"
    "net/http"
    "time"
)

func printHello(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "Hello World!\n")
}

func printDate(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "Current date: %s\n", time.Now().Format(time.RFC3339))
}

func main() {
    // Define a custom multiplexer
    r := http.NewServeMux()

    // Register printHello function
    r.HandleFunc("/hello", printHello)
    // Register printDate function
    r.HandleFunc("/date", printDate)

    // Use r as the default handler
    log.Fatal(http.ListenAndServe(":8080", r))
}

 

Da notare la fondamentale differenza nella funzione http.ListenAndServe. Alla suddetta funzione viene passata la stringa ip:port come primo argomento e come secondo l’http.Handler da servire che in questo caso è proprio il multiplexer definito, il quale soddisfa l’interfaccia suddetta.

 

Esistono diveri altri tipi di multiplexer in Go che offrono anche maggiore possibiltà di maggiore controllo sul routing. Tra questi il mio preferito è di sicuro gorilla/mux che invito a valutare per i propri progetti futuri. Per la dimostrazione di questo blogpost si è preferito non aumentare il livello di complessità e utilizzare lo il multiplexer standard del package net/http.

 

La Terra di Mezzo

E’ dunque giunto il momento di avventurarci nel fantastico mondo del middleware.
Partiamo subito da un presupposto fondamentale: si possono implementare middleware anche con il solo package net/http. Di seguito una semplice dimostrazione:

 

basicMiddleware.go

package main

import (
    "fmt"
    "net/http"
)

func middleware(handler http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        fmt.Println("Executing middleware before request phase!")
        handler.ServeHTTP(w, r)
        fmt.Println("Executing middleware after request phase!")
    })
}

func mainLogic(w http.ResponseWriter, r *http.Request) {
    // Business logic goes here
    fmt.Println("Executing mainLogic")
    w.Write([]byte("May the Force be with you!"))
}

func main() {
    // HandlerFunc returns a HTTP Handler
    mainLogicHandler := http.HandlerFunc(mainLogic)
    http.Handle("/", middleware(mainLogicHandler))
    http.ListenAndServe(":8000", nil)
}

 

Questo listato merita un walkthrough più approfondito. Il codice inizia con la dichiarazione del main package e l’import di fmtnet/http. Non ci serve davvero altro per questo esempio.

package main

import (
   "fmt"
   "net/http"
)

Successivamente passiamo alla dichiarazione delle funzione middleware. Questa funzione non prenderà, come fatto finora http.ResponseWriter e *http.Request come argomenti ma soltanto un’interfaccia di tipo http.Handler per restituire, di nuovo, http.Handler. Questo ci permetterò di incpasulare al suo interno una funzione facendo precedere e seguire altre operazioni.
L’interfaccia http.Handler definisce un solo metodo, ServeHTTP(http.ResponseWriter, *http.Request). Questo metodo richiama la funzione sottostante passando come argomenti li stessi ricevuti.

Una nota: il fatto di ragionare in termini di interfacce permette di implementare in Go una grande flessibilità e di adottare pattern come la dependency injection. Se non abbiamo finora lavorato molto con le interfacce è il arrivato il momento di approfondirle!
La funzione middleware passa una funzione anonima come argomento a http.HandlerFunc (da NON confondersi con http.HandleFunc) che è un tipo custom costruito su func(ResponseWriter, *Request).

Se si richiama http.HandlerFunc(f), dove f è una funzione passatagli come in questo esempio, viene restituita un’interfaccia http.Handler, proprio quello che vogliamo avere noi come output della funzione middleware!.
Ok, ma la funzione anonima cosa fa? Essa stampa due output dimostrativi (nello stdout del nostro server web) prima e dopo l’esecuzione di handler.ServeHTTP(w, r). Questo metodo invocherà la funzione soggiacente all’handler che si è passato all’inizio come argomento.

func middleware(handler http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        fmt.Println("Executing middleware before mainLogic")
        handler.ServeHTTP(w, r)
        fmt.Println("Executing middleware after mainLogic")
    })
}

 

Si passa successivamente alla definizione di una classica funzione con la logica principale:

func mainLogic(w http.ResponseWriter, r *http.Request) {
    // Business logic goes here
    fmt.Println("Executing mainLogic")
    w.Write([]byte("May the Force be with you!\n"))
}

Questa piccola funzione utilzza, per la stampa sul terminale dell’utente, il metodo w.Write, che prende come argomento non una stringa ma un byte slice. Per farlo usiamo una conversione di tipo su una stringa: []byte(“May the Force be with you!\n”).

 

Si giunge infine al main del programma:

func main() {
 // HandlerFunc returns a HTTP Handler
 mainLogicHandler := http.HandlerFunc(mainLogic)
 http.Handle("/", middleware(mainLogicHandler))
 http.ListenAndServe(":8000", nil)
}

Per prima cosa dobbiamo far si che la nostra funzione mainLogic soddisfi l’interfaccia http.Handler. Usiamo di nuovo http.HandlerFunc(mainLogic) per ottenere un’interfaccia mainLogicHandler.
Abbiamo parlato di middleware come enttà che interagiscono tra la richiesta del client e la risposta del server: in questo esempio possiamo immaginare il nostro middleware, in senso metaforico, come una scatola cinese la quale, appunto, “inscatola” altri  handler.
Ed è proprio quello che accade nella riga di codice http.Handle(“/”, middleware(mainLogicHandler)) in cui vediamo il wrapping dell’handler mainLogicHandler nella funzione middleware che viene registrata sulla rotta “/”.

Segue infine il classico http.ListenAndServe che mette in ascolto in nostro web server.

 

Osserviamo ora il comportamento di questo servizio. Avviamo il programma da un terminale:

$ go run basicMiddleware.go

E da un altro terminale eseguiamo una curl:

$ curl localhost:8080
May the Force be with you!

Abbiamo l’output atteso dalla funzione mainLogic. Tuttavia, se torniamo sul terminale da cui è stato lanciato il servizio possiamo leggere delle informazioni di logging aggiuntive scritte dal nostro middleware:

$ go run basicMiddleware.go 
Executing middleware before mainLogic
Executing mainLogic
Executing middleware after mainLogic

Negroni con ghiaccio

Fino ad ora abbiamo giocato con semplici esempi che avevano la finalità di dimostrare alcuni aspetti fondamentali dell’implementazione di servizi web in Go e di introdurre il concetto di middeware in modo semplificato. Lavorare con middleware complessi con il solo package net/http, pur non essendo impossibile, può dimostrarsi abbastanza complesso e difficile da manutenere. Per questo la maggior parte degli sviluppatori preferisce utilizzare package dedicati allo sviluppo del middleware. Uno di questi, e di certo uno dei più usati, è urfave/negroni, che vuole essere uno strumeto semplice per la creazione e la gestione di middleware in Go.

Negroni non è un framework web ma semplicemente una libreria orientata al middleware basata sul package net/http.
Nel prossimo e ultimo esempio, il vero obiettivo di questo blogpost, andremo a sviluppare un’applicazione che implementa due custom middleware. Questa volta non ci limiteremo a far stampare loro degli output di log sullo stdout del nostro web server, ma daremo loro un ruolo attivo che impatterà sul funzionamenti del servizio.
Si costruiranno due due middleware:

  • il primo, che chiameremo userAgentCheck verificherà se lo User-Agent da cui arriva la richiesta HTTP non sia uno tra quelli definiti in una regular expression. Questo ci può permettere di escludere alcuni bot, curl e Wget (anche se di solito i bot ciclano gli User-Agent ad ogni richiesta). Lo scopo, lo ribadiamo, è puramente propedeutico è non è una soluzione di security enforment praticabile.
  • il secondo middleware, denominato methodCheck, verifica che il metodo della richiesta sia GET e respinge qualsiasi altro metodo utilizzato.

In seguito al “filtraggio attivo” dei nostri middleware potremo infine processare le richieste con i nostri handler.
Sia i middleware che gli handler produrranno inoltre dei messaggi di logging sufficienti a raccogliere informazioni sull’esecuzione.

 

Per utilizzare Negroni dobbiamo prima importare il package sulla nostra macchina. Se si sono svolte correttamente le configurazioni preliminari dell’ambiente di sviluppo questa operazione sarà semplicissima:

$ go get github.com/urfave/negroni

I sorgenti del package verranno copiati così nel path $GOPATH/src/github.com/urfave/negroni e saranno facilmente ispezionabili.

 

Iniziamo con l’analisi del codice. Come al solito si parte dal listato completo del programma:

negroniMiddleware.go

package main

import (
    "flag"
    "fmt"
    "log"
    "net/http"
    "os/user"
    "regexp"
    "strconv"

    "github.com/urfave/negroni"
)

const (
    rePattern = "^.*(curl|bot|[Pp]ython|[Ww]get).*"
)

// printWelcome prints a simple welcome message
func printWelcome(w http.ResponseWriter, r *http.Request) {
    log.Println("printWelcome INFO - Entering welcome funcion")
    fmt.Fprintf(w, "Hello visitor! You are connecting from IP/Port %s with User-Agent %s\n", r.RemoteAddr, r.UserAgent())
    log.Println("printWelcome INFO - Leaving welcome function")
}

// printHelp prints out a simple help to demonstrate different handlers
func printHelp(w http.ResponseWriter, r *http.Request) {
    log.Println("printHelp INFO - Entering help funcion")
    fmt.Fprintf(w, "Usage: http://<host>:8000/welcome\n")
    log.Println("printHelp INFO - Leaving help function")
}

// userAgentCheck avoids requests from curl User-Agent
func userAgentCheck(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) {
    log.Println("userAgentCheck INFO - Begin User-Agent check")
    userAgent := r.UserAgent() // The User-Agent of the HTTP Request
    re, err := regexp.Compile(rePattern)
    if err != nil {
        log.Println("userAgentCheck ERR - failed to compile regexp pattern")
        panic(1)
    }
    if re.MatchString(userAgent) {
        w.WriteHeader(http.StatusBadRequest)
        fmt.Fprintf(w, "Error: cannot accept connections from %s User-Agent.\n", userAgent)
        log.Printf("userAgentCheck ERR - Refused connection to client with User-Agent %s", userAgent)
        return
    }
    log.Println("printAgentCheck INFO - Completed User-Agent check")
    next(w, r)
}

// methodCheck verifies that the HTTP Request method is GET
func methodCheck(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) {
    log.Println("methodCheck INFO - Begin HTTP Request Method check")
    if r.Method != "GET" {
        w.WriteHeader(http.StatusBadRequest)
        fmt.Fprintf(w, "Error: %s method is forbidden in this context\n", r.Method)
        log.Printf("methodCheck ERR - Forbidden %s method", r.Method)
        return // We don't need to go through middleware2
    }
    log.Println("methodCheck INFO - Completed HTTP Request method check")
    next(w, r)
}

func main() {
    // Define port binding flag
    portFlag := flag.Int("port", 8000, "Port number")
    flag.Parse()

    // Load current username
    currentUser, err := user.Current()
    if err != nil {
        log.Fatal("Fatal: cannot evaluate current username")
    }

    // Test if current user is root to open ports below 1024
    if *portFlag < 1024 && currentUser.Username != "root" {
        log.Fatalf("Fatal: %s user cannot open ports under 1024\n", currentUser.Username)
    }

    // Create the port binding string
    portBinding := fmt.Sprintf(":%s", strconv.Itoa(*portFlag))

    // Create new mux router
    r := http.NewServeMux()

    // Register new routes and associated handler functions
    r.HandleFunc("/welcome", printWelcome)
    r.HandleFunc("/help", printHelp)

    // Define a classic Negroni environment with a standard middleware stack:
    // Recovery - Panic Recovery Middleware
    // Logger - Request/Response Logging
    // Static - Static File Serving
    n := negroni.Classic()

    // Append middleware in the stack.
    // Functions are processed as Negroni Handlers in the same order they are passed.
    n.Use(negroni.HandlerFunc(userAgentCheck))
    n.Use(negroni.HandlerFunc(methodCheck))

    // Append a standard http.Handler
    n.UseHandler(r)

    // Negroni.Run is a wrapper to http.ListenAndServe
    n.Run(portBinding)
}

 

Procediamo con il solito walkthrough del codice. Come usuale, si comincia dalla definizione del package main e dagli import dei package utilizzati.

package main

import (
    "flag"
    "fmt"
    "log"
    "net/http"
    "os/user"
    "regexp"
    "strconv"

    "github.com/urfave/negroni"
)

 

Notiamo subito che viene importato un package che non è nella libreria standard di Go, ovvero github.com/urfave/negroni. E’ buona convenzione importare i package community e in generale non presenti nella standard library in un blocco separato come fatto qui.
Altri package interessanti che useremo:

  • flag per gestire flag da command line
  • log per il logging del nostro web server
  • os/user per gestire informazioni sugli utenti di sistma
  • regexp per processare regular expressions
  • strconv per la conversione di stringhe

 

Poiché si userà una regular expression contenente dei pattern da identificare per lo User-Agent, si è preferito inserire la stringa all’interno di una constante:

const (
 rePattern = "^.*(curl|bot|[Pp]ython|[Ww]get).*"
)

L’esempio è abbastanza rozzo ma sufficiente per la dimostrazione. Questa regexp può matchare usera agent curl, tutti quelli che hanno la string bot, [Pp]ython e [Ww]get.

 

Definiamo ora le due funzioni che conterrano la logica principale:

// printWelcome prints a simple welcome message
func printWelcome(w http.ResponseWriter, r *http.Request) {
    log.Println("printWelcome INFO - Entering welcome funcion")
    fmt.Fprintf(w, "Hello visitor! You are connecting from IP/Port %s with User-Agent %s\n", r.RemoteAddr, r.UserAgent())
    log.Println("printWelcome INFO - Leaving welcome function")
}

// printHelp prints out a simple help to demonstrate different handlers
func printHelp(w http.ResponseWriter, r *http.Request) {
    log.Println("printHelp INFO - Entering help funcion")
    fmt.Fprintf(w, "Usage: http://<host>:8000/welcome\n")
    log.Println("printHelp INFO - Leaving help function")
}

La prima funzione, printWelcome, oltre a loggare le operazioni che svoge, restituisce come Response una stringa di benvenuto contenente ip/porta della macchina remota connessa e lo User-Agent.

La seconda funzione, printHelp, stampa un semplice help di uso della nostra applicazione. Minimale e rozzo ma sufficiente per fare alcuni test di multiplexing.

 

Passiamo ora alla definizione dei nostri middleware. Il primo, denominato userAgentCheck, si occupa di verificare lo User-Agent da cui arriva la richiesta. Si notino subito gli argomenti passati alla funzione:

  • w http.ResponseWriter
  • r *http.Request
  • next http.HandlerFunc

Qui abbiamo non più i due soliti ResponseWriter e *Request ma un terzo argomento next di tipo http.HandlerFunc. A cosa serve? E’ qui che entra in gioco Negroni.

Quello che fa Negroni nel nostro programma è creare una middleware chain che permette di concatenare più elementi in modo trasparente. Alla fine dell’esecuzione dei task all’interno della funzione viene infatti richiamato l’handler passato come argomento con next(w, r). Questo approccio permette di creare middleware in modo semplice lasciando a Negroni l’onere del concatenamento e dando a noi il solo compito di decidere l’ordine di esecuzione, cosa che faremo poi nel main.

La funzione inizia con un log sullo stdout del server. Successivamente carichiamo la stringa dello User-Agent tramite il metodo r.UserAgent(). Compiliamo la regexp con il pattern definito nella costante rePattern e lo testiamo con re.MatchString(UserAgent).
Questo metodo restituisce un boolean: se vero abbiamo un match della regular expression e pertanto lo User-Agent è indesiderato.
Si noti la funzione w.WriteHeader che imposta lo status della risposta a 400 BadRequest prima di produrre un messaggio d’errore all’utente e un log simile. A questo punto non vogliamo andare avanti con la chain e soprattutto non vogliamo far arrivare la richiesta al servizio, pertanto usciamo con un semplice statement return. In caso contario si procede a passare l’esecuzione al prossimo middleware.

// userAgentCheck avoids requests from curl User-Agent
func userAgentCheck(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) {
    log.Println("userAgentCheck INFO - Begin User-Agent check")
    userAgent := r.UserAgent() // The User-Agent of the HTTP Request
    re, err := regexp.Compile(rePattern)
    if err != nil {
        log.Println("userAgentCheck ERR - failed to compile regexp pattern")
        panic(1)
    }
    if re.MatchString(userAgent) {
        w.WriteHeader(http.StatusBadRequest)
        fmt.Fprintf(w, "Error: cannot accept connections from %s User-Agent.\n", userAgent)
        log.Printf("userAgentCheck ERR - Refused connection to client with User-Agent %s", userAgent) 
        return
    }
    log.Println("printAgentCheck INFO - Completed User-Agent check")
    next(w, r)
}

 

Passiamo al secondo middleware:

// methodCheck verifies that the HTTP Request method is GET
func methodCheck(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) {
    log.Println("methodCheck INFO - Begin HTTP Request Method check")
    if r.Method != "GET" {
        
        fmt.Fprintf(w, "Error: %s method is forbidden in this context\n", r.Method)
        log.Printf("methodCheck ERR - Forbidden %s method", r.Method)
        return // We don't need to go through middleware2
    }
    log.Println("methodCheck INFO - Completed HTTP Request method check")
    next(w, r)
}

In questo caso la logica è molto più semplice. Testiamo se il campo r.Method è uguale a “GET”. Se la richiesta contiene un altro metodo blocchiamo immediatamente l’esecuzione loggando l’errore.

 

Giungiamo ora al main del web server. Nella prima parte ci occupiamo di definire un flag per la customizzazione del socket binding.

func main() {
    // Define port binding flag
    portFlag := flag.Int("port", 8000, "Port number")
    flag.Parse()

Questa è l’occasione per familiarizzare con l’utilissimo package flag. Con lo statement portFlag := flag.Int(“port”, 8000, “Port number”) definiamo un flag assegnabile di tipo into con un default value 8000. Il secondo comando invece fa il parsing di tutti gli argomenti passati alla linea di comando.

 

    // Load current username
    currentUser, err := user.Current()
    if err != nil {
        log.Fatal("Fatal: cannot evaluate current username")
    }

    // Test if current user is root to open ports below 1024
    if *portFlag < 1024 && currentUser.Username != "root" {
        log.Fatalf("Fatal: %s user cannot open ports under 1024\n", currentUser.Username)
    }

    // Create the port binding string
    portBinding := fmt.Sprintf(":%s", strconv.Itoa(*portFlag))

Vogliamo permettere la customizzazione della porta ma prevenire errori di sistema nel caso di tentativi, da parte di utenti non privilegiati, di aprire porte inferiori a 1024. Per evitare ciò estrarremo info sull’utente corrente con la funzione user.Current() che restituisce una struttura User, di cui useremo il campo Username per testare se l’utente non è root nel caso in cui si tenti un binding con una porta privilegiata. In tal caso usciremo dall’esecuizione con log.Fatal che restituisce un exitcode 1 e logga il messaggio su stderr.
Nel caso in cui la porta sia superiore o uguale a 1024 o il processo sia lanciato dall’utente root contineremo con l’esecuzione creando la stringa che utilizzeremo alla fine per il port binding.

 

Arriviamo ora alla creazione del multiplexer:

    // Create new mux router
    r := http.NewServeMux()

    // Register new routes and associated handler functions
    r.HandleFunc("/welcome", printWelcome)
    r.HandleFunc("/help", printHelp)

Come fatto prima si definisce un multiplexer HTTP e si registrano due HandleFunc per due rotte diverse, associando “/welcome” alla funzione printWelcome e “/help” a printHelp.

 

E’ il momento di creare un’istanza di Negroni utilizzando un costruttore speciale, Classic, che prealloca nello stack un default middleware composto da:

  • negroni.Recovery
  • negroni.Logger
  • negroni.Static
 // Define a classic Negroni environment with a standard middleware stack:
 // Recovery - Panic Recovery Middleware
 // Logger - Request/Response Logging
 // Static - Static File Serving
 n := negroni.Classic()

In alternativa si potrebbe anche utilizzare il costruttore negroni.New() per creare un’istanza semplice di Negroni senza default middleware precaricati.

 

Arriviamo al punto cruciale, dove viene creato lo stack del custom middleware:

    // Append middleware in the stack.
    // Functions are processed as Negroni Handlers in the same order they are passed.
    n.Use(negroni.HandlerFunc(userAgentCheck))
    n.Use(negroni.HandlerFunc(methodCheck))

Per appendere middleware allo stack, molto semplicemente, è sufficiente utilizzare il metodo *Negroni.Use che aggiunge un Handler alla sequenza. Gli Handler verranno processati nell’ordine di inserimento. E’ bene ricordarsi che in questo caso non stiamo lavorando con http.Handler ma con negroni.Handler che implementano i primi e ne estendono, come si è visto, gli argomenti passati.

 

Giunti a questo punto passiamo il nostro multiplexer HTTP  all’istanza Negroni.

    // Append a standard http.Handler
    n.UseHandler(r)

Il metodo UseHandler è dichiarato come func (n *Negroni) UseHandler(handler http.Handler) e pertanto accetta come argomento un http.Hanlder.

 

Si può infine eseguire il web server. Per farlo si userà il metodo *Negroni.Run a cui passeremo la porta su cui fare il binding. Questo metodo eseguirà a sua volta la funzione http.ListenAndServe.

 // Negroni.Run is a wrapper to http.ListenAndServe
 n.Run(portBinding)

 

Siamo giunti alla fine del walkthough del server. Non resta che eseguirlo e vedere come si comporta nei vari possibili scenari.

$ go run negroniMiddleware.go
[negroni] listening on :8000

Poiché si è utilizzato il costruttore negroni.Classic() si godrà subito del beneficio di un logging esteso che produrrà informazioni sulla porta in ascolto e tutti i tentativi di connessione con annessi status e tempi di risposta.

 

Sappiamo che gli handler sono in questo caso due, “/welcome” e “/help”. Proviamo dunque a invocare il primo da curl:

$ curl http://localhost:8000/welcome
Error: cannot accept connections from curl/7.55.1 User-Agent.

 

Bene, sembra che il nostro middleware che testa lo User-Agent stia lavorando correttamente. Andando a vedere i log del server troveremo nuovi messaggi:

2018/07/18 01:20:13 userAgentCheck INFO - Begin User-Agent check
2018/07/18 01:20:13 userAgentCheck ERR - Refused connection to client with User-Agent curl/7.55.1
[negroni] 2018-07-18T01:20:13+02:00 | 400 | 438.616µs | localhost:8000 | GET /welcome

Si noti che, dopo i messaggi INFO e ERR prodotti dal middleware, a questi si aggiunge un generico logging negroni che ci riporta uno Status 400 BadRequest per la richiesta, così come si era impostato nell’header della risposta durante la definizione dei middleware.

 

Proviamo ora a simulare un altro User-Agent. Con l’opzione -A di curl possiamo modificare la stringa che viene presentata nell’header della richiesta.

$ curl -A 'Mozilla/5.0' http://localhost:8000/welcome
Hello visitor! You are connecting from IP/Port [::1]:50322 with User-Agent Mozilla/5.0

Questa volta la nostra richiesta verrà soddisfatta poiché lo User-Agent fittizzio che abbiamo passato non fa match con nessuna delle sottostringhe nel pattern della regexp. E’ stato abbastanza facile prendere in giro il nostro blocco!

 

Nei log del server troveremo qualcosa di simile a questo:

2018/07/18 01:37:13 userAgentCheck INFO - Begin User-Agent check
2018/07/18 01:37:13 printAgentCheck INFO - Completed User-Agent check
2018/07/18 01:37:13 methodCheck INFO - Begin HTTP Request Method check
2018/07/18 01:37:13 methodCheck INFO - Completed HTTP Request method check
2018/07/18 01:37:13 printWelcome INFO - Entering welcome funcion
2018/07/18 01:37:13 printWelcome INFO - Leaving welcome function
[negroni] 2018-07-18T01:37:13+02:00 | 200 | 391.969µs | localhost:8000 | GET /welcome

 

Notiamo che siamo entrati anche nel middleware methodCheck che ha verificato il metodo della richiesta. Proviamo ora a passare un metodo diverso:

$ curl -X POST -A 'Mozilla/5.0' http://localhost:8000/welcome
Error: POST method is forbidden in this context

 

La funzione methodCheck ha bloccato l’accesso ad un metodo diverso da GET, in questo caso un metodo POST. Nei log del server troveremo questo output:

2018/07/18 01:42:37 userAgentCheck INFO - Begin User-Agent check
2018/07/18 01:42:37 printAgentCheck INFO - Completed User-Agent check
2018/07/18 01:42:37 methodCheck INFO - Begin HTTP Request Method check
2018/07/18 01:42:37 methodCheck ERR - Forbidden POST method
[negroni] 2018-07-18T01:42:37+02:00 | 400 | 213.186µs | localhost:8000 | POST /welcome

 

Andiamo a combinare User-Agent curl* (non accettato) e metodo POST:

curl -X POST  http://localhost:8000/welcome
Error: cannot accept connections from curl/7.55.1 User-Agent.

 

Dai log si evince che il methodCheck in questo caso non viene neanche eseguito. I log possono confermarcelo:

2018/07/18 01:45:33 userAgentCheck INFO - Begin User-Agent check
2018/07/18 01:45:33 userAgentCheck ERR - Refused connection to client with User-Agent curl/7.55.1
[negroni] 2018-07-18T01:45:33+02:00 | 400 | 246.118µs | localhost:8000 | POST /welcome

Questo conferma quanto abbiamo osservato prima durante il walkthrough delle funzioni userAgentCheck e methodCheck. Se la condizione testata non viene soddisfatta ambedue le funzioni escono e non richiamano la funzione successiva dello stack astratta da next(w, r).

 

Conclusioni

Siamo giunti alla fine del nostro blogpost. Abbiamo dimostratoto numerose feature interessanti della creazione di servizi web in Go. Siamo partiti dalla creazione di un server web minimale per poi aggiungere un multiplexer custom. Successivamente abbiamo implementato, senza l’ausilio di alcun package aggiuntivo un middleware minimale. Infine abbiamo adottato Negroni per lavorare con in middleware in modo più elegante e pulito e lo abbiamo utilizzato per creare un filtering dinamico per la nostra web application.
Il README GitHub di Negroni riporta un utilissimo elenco di middleware di terze parti che possono essere adottati e che sono già di uso frequente nella community Go. Ne citiamo alcuni interessanti:

  • cors, per supportare il Cross Origin Resource Sharing
  • gzip, per la compressione gzip
  • logrus, middleware Negroni basato sul logger logrus
  • oauth2, middleware per oauth2
  • prometheus, per creare metrics endpoint
  • sessions, per il Session Management

 

Un buon esercizio potrebbe essere quello di partire dall’esempio dimostrato ed estenderne le funzionalità, ad esempio tramite l’implementazione di nuovi middleware custom o l’uso di quelli suggeriti sopra.

 

La pratica, come al solito, fa la differenza e aiuta a comprendere e fare propri anche i concetti più astratti.

Buon divertimento!

Info about author

EXTRAORDY