5. Middleware

Cuando estamos creando una app web, seguramente tenemos alguna funcionalidad que queremos usar para algunas (o todas) las peticiones HTTP. Por ejemplo, si quisieras tener un log con cada peticion,comprimir cada respuesta,o revisar el cache antes de pasar una peticion hacia tus handlers.

La manera coun de organizar esta funcionalidad compartida es creadno un Middleware. Basicamente es un codigo el cual actua independientemente antes o despues de que una peticion interactue con tus handlers.

En este post veremos:

  • Una manera facil de construir y usar middleware, el cual es compatible con net/http muchas otras librerias de terceros.
  • Como crear un middleware el cual establesca seguridad util en cada respuesta HTTP
  • Como crear un middleware el cual almacene los logs de las peticiones que lleguen a nuestra app
  • Como crear un middleware el cual se recupere de un panic de una manera amena
  • Como crear y componer cadenas de middleware para ayudarnos a manejar y organizar mejor nuestras funciones middleware

5.1 Como funciona un middleware

Actualmente en nuestra app, cuando nuestro server recibe una peticion HTTP , llama al servemux ServeHTTP(), este busca el handler apropiado dependiendo de la url y a su vez llama al metodo ServeHTTP de ese handler.

La idea basica de un middleware es agregar otro handler a esa cadena. El middleware ejecuta una logica , como una peticion de login y luego llama a ServeHTTP del siguitente handler en la cadena.

Nosotros ya usamos un middleware (http.StripPrefix()) el cual remueve un prefijo de una url antes de pasar la peticion al servidor.

5.1 El patron

El patron estandar para crear un middleware se ve asi:

func myMiddleware(next http.Handler) http.Handler {
  fn := func(w http.ResponseWriter, r *http.Request) {
  // TODO: Execute our middleware logic here...
  next.ServeHTTP(w, r)
  }
  return http.HandlerFunc(fn)
}
  • myMiddleware() es escencialmente un wrapper que encierra al siguiente controlador next que recibimos como parametro.
  • Se establece una funcion fn la cual encierra el handler next para formar un closure. Cuando fn corre, ejecuta la logica de nuestro middleware y luego transfiere el control al handler next para llamar al metodo ServeHTTP()
  • Independientemente de o que hagas en la closure siempre tendras acceo a las variables creadas en tu local scope en donde fue creada, lo cual significa que fn siempre tendra acceso a la variable next
  • En la ultima linea convertimos esta closure en un http.Handler usando el adaptador http.HandlerFunc

Podes pensarlo de la siguiente manera: myMiddleware() es una funcion que acepta el handler next en una cadena como parametro. Este retorna un handler que ejecuta alguna logica y luego llama al handler next .

5.1 Simplificando el middleware

Un retoque que le podemos hacer usar una funcion anonima dentro de myMiddleware() asi:

func myMiddleware(next http.Handler) http.Handler {
   return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
     // TODO: Execute our middleware logic here...
     next.ServeHTTP(w, r)
  })
}

Este patron es el mas comun que veras en codigo de terceros, mas aun en paquetes de terceros.

5.1 Posicionamiento del middleware

Es importante saber que dependiendo de donde pongamos el middleware en la cadena de handlers afectara el comportamiento de nuestra aplicacion.

Si ponemos el middleware antes del servemux en la cadena , estonces actuara en cada peticion que tu aplicacion reciba.

myMiddleware → servemux → application handler

Un buen ejemplo donde esto puede ser util es tener un middleware que haga un log de todas las peticiones.

Otra alternativa es poner el middleware despues del servemux en la cadena para que solo actue en ese handler especificamente.

servemux → myMiddleware → application handler

Un ejemplo, donde querramos este comportamiento seria un middleware de autorizacion, el cual solo funcione en ciertas rutas.

En otros post veremos como hacer estas cosas.

5.2 Configurando headers de seguridad.

Vamos a usar el patron que aprendimos antes y haremos nuestro propio middleware el cual automaticamente agregara seguridad a nuestras cabeceras HTTP en cada respuesta, segun lo dicta current OWASP guidance.

Content-Security-Policy: default-src 'self'; style-src 'self' fonts.googleapis.com; font-src fonts.gstatic.com
Referrer-Policy: origin-when-cross-origin
X-Content-Type-Options: nosniff
X-Frame-Options: deny
X-XSS-Protection: 0

Si no estas familiarizado con esos headers, voy a explicar rapidamiente que hacen:

  • Content-Security-Policy (a menudo abreviado CSP) Estos headers son usados para especificar claramente que fuentes de contenido externas son permitidas y cuales no. Configurar los CSP de manera estricta ayuda a prevenir ataques de cross-site scripting , clickjacking y otros tipos de ataques de inyeccion de codigo.

Recomiendo leer esto si no sabes nada sobre este tema. Pero en nuestro caso el header le dice al navegador qe esta bien que cargue la fuentes de fonts.gstatic.com , stylesheet de fonts.googleapis.com y self (nuestro propio origen).Inline javascript es bloqueado por default.

  • Referrer-Policy es usado para controlar la informacion que se incluye en el header Referer cuando un usuario navega en nuestra web. En nuestro caso lo setearemos con origin-when-cross-origin , lo cual significa que la url completa sera incluida para peticiones same-origin , pero todas las otras peticiones de informacion como el path de la URL y cualquier query string sera removida si se trata de un sitio diferente.

  • X-Content-Type-Options: nosniff le dice a los navegadores que no intente adivinar el tipo de dato de los content-type de una respuesta , lo cual ayuda a prevenir content-sniffing attacks

  • X-Frame-Options: deny es usado para prevenir clickjacking atacks en navegadores viejos que no sorportan CSP headers.

  • X-XSS-Protection: 0 Es usado para desactivar el bloqueo de ataques de comandos entre sitios. Anteriormente era buena practica setear el header con : X-XSS-Protection: 1; mode=block, pero cuando estas usando cabeceras CSP como en la recomendacion es desactivarlo.

Vamos a nuestro codigo y empecemos creando un nuevo middleware.go lo usaremos para contener todos los middleware personalizados qu vayamos creando a traves de estos post.

$ touch cmd/web/middleware.go

Dentro de el agregamos la funcion secureHeaders() usando el patron que vimos antes.

package main
import (
"net/http"
)
func secureHeaders(next http.Handler) http.Handler {
   return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
      // Note: This is split across multiple lines for readability. You don't
      // need to do this in your own code.
      w.Header().Set("Content-Security-Policy",
      "default-src 'self'; style-src 'self' fonts.googleapis.com; font-src fonts.gstatic.com")
      w.Header().Set("Referrer-Policy", "origin-when-cross-origin")
      w.Header().Set("X-Content-Type-Options", "nosniff")
      w.Header().Set("X-Frame-Options", "deny")
      w.Header().Set("X-XSS-Protection", "0")
      next.ServeHTTP(w, r)
   })
}

Como nosotros queremos que este middleware en cada peticion que recibamos, tenemos que ejecutarlo antes de que esta peticion llegue a nuestro servemux , queremos que el flujo de nuestra app se vea asi:

secureHeaders → servemux → application handler

Para lograrlo tenemos que envolver nuestro servemux con secureHeaders. Vamos a las routes.go y lo dejamos de la siguiente manera.

   package main
   import "net/http"
-   func (app *application) routes() *http.ServeMux {
   // Update the signature for the routes() method so that it returns a
   // http.Handler instead of *http.ServeMux.
+   func (app *application) routes() http.Handler {
      mux := http.NewServeMux()
      fileServer := http.FileServer(http.Dir("./ui/static/"))
      mux.Handle("/static/", http.StripPrefix("/static", fileServer))
      mux.HandleFunc("/", app.home)
      mux.HandleFunc("/snippet/view", app.snippetView)
      mux.HandleFunc("/snippet/create", app.snippetCreate)
      // Pass the servemux as the 'next' parameter to the secureHeaders middleware.
      // Because secureHeaders is just a function, and the function returns a
      // http.Handler we don't need to do anything else.
+      return secureHeaders(mux)

-      return mux
}

Corramos nuestra app y veamos los nuevos resultados que nos arroja la terminal.

$ curl -I http://localhost:4000/
HTTP/1.1 200 OK
Content-Security-Policy: default-src 'self'; style-src 'self' fonts.googleapis.com; font-src fonts.gstatic.com
Referrer-Policy: origin-when-cross-origin
X-Content-Type-Options: nosniff
X-Frame-Options: deny
X-Xss-Protection: 0
Date: Wed, 06 Sep 2023 14:16:39 GMT
Content-Length: 1700
Content-Type: text/html; charset=utf-8

5.2 Informacion adicional

5.2 Flow of control

Es importante saber que cuanod el ultimo handler en la cadena hace el return , el
control es pasado de manera inversa en la cadena.

Cualquier middleware handler que este despues de next.ServeHTTP() sera ejecutado en la direccion contraria a como viene la cadena normalmente.

func myMiddleware(next http.Handler) http.Handler {
   return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
      // Any code here will execute on the way down the chain.
      next.ServeHTTP(w, r)
      // Any code here will execute on the way back up the chain.
   })
}
5.2 Early returns

Otra cosa a mencionar es que si llamas a un return antes de llamar a next.ServeHTTP() entonces la cadena de ejecucion se detendra y el control volversa a fluir en sentido contrario.

Un ejemplo comun de un early return es cuando corremos un middleware con autenticacion, el cual permitira ejecutar el resto de la cadena si una condicion en particular es lograda ser pasada.

func myMiddleware(next http.Handler) http.Handler {
   return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   // If the user isn't authorized, send a 403 Forbidden status and
   // return to stop executing the chain.
   if !isAuthorized(r) {
     w.WriteHeader(http.StatusForbidden)
     return
   }
   // Otherwise, call the next handler in the chain.
   next.ServeHTTP(w, r)
  })
}

Usaremos este patron en otros post para restringir el acceso a ciertas partes de nuesta app.

5.2 Debugging CSP issues

Los headers CSP son importantes y definitivamente deberias usarlos. En ocacioes perdi mucho tiempo intentando buscar un problema en mis app , solo para darme cuenta que un recurso critico en mi app estaba siendo bloquead por mis propias reglas CSP.

Te recomiendo tener a vista la consola del navegador para darte cuenta temprano si es que tenes algun error.

5.3 Request logging

Crearemos otro middleware para tener un log de todas las consultas que nos vayan llegando. Usaremos una estrucura logger para capurar la direccion ip del usuario, el metodo , la URI y la version HTTP para la peticion.

Abramos middleware.go y creemos el metodo logRequest() usando el patron estandar para la creacion de los middleware.

func (app *application) logRequest(next http.Handler) http.Handler {
  return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  var (
    ip = r.RemoteAddr 
    proto = r.Proto
    method = r.Method
    uri= r.URL.RequestURI()
  )
  app.logger.Info("received request", "ip", ip, "proto", proto, "method", method, "uri", uri)
  next.ServeHTTP(w, r)
  })
}

Te distecuenta que estamos implementando al middleware como metodo en application ?

Esto es perfectamente valido, nuestro middleware tiene la misma firma que venimos usando hasta ahora peor domo es un metodo perteneciente a application tambien tiene acceso a las dependencias, incluyendo a la estrucura logger.

Ahora actualicemos nuestro routes.go par que use el middleware logRequest y este sea ejecutado primero y para todas las peticiones , de manera que el flujo de control se vea asi ( de izquierda a derecha).

logRequest ↔ secureHeaders ↔ servemux ↔ application handler
package main
import "net/http"


func (app *application) routes() http.Handler {
    mux := http.NewServeMux()
    fileServer := http.FileServer(http.Dir("./ui/static/"))
    mux.Handle("/static/", http.StripPrefix("/static", fileServer))
    mux.HandleFunc("/", app.home)
    mux.HandleFunc("/snippet/view", app.snippetView)
    mux.HandleFunc("/snippet/create", app.snippetCreate)
    // Wrap the existing chain with the logRequest middleware.
    return app.logRequest(secureHeaders(mux))
}

Listo! Reinicia el servidor y deberias ver algocomo esto:

$ go run ./cmd/web
time=2023-09-06T16:23:46.794+02:00 level=INFO msg="starting server" addr=:4000
time=2023-09-06T16:23:48.946+02:00 level=INFO msg="received request" ip=127.0.0.1:56536 proto=HTTP/1.1 method=GET uri=/
time=2023-09-06T16:23:48.965+02:00 level=INFO msg="received request" ip=127.0.0.1:56536 proto=HTTP/1.1 method=GET uri=/static/css/main.css
time=2023-09-06T16:23:48.966+02:00 level=INFO msg="received request" ip=127.0.0.1:56546 proto=HTTP/1.1 method=GET uri=/static/js/main.js
time=2023-09-06T16:23:49.233+02:00 level=INFO msg="received request" ip=127.0.0.1:56536 proto=HTTP/1.1 method=GET uri=/static/img/logo.png
time=2023-09-06T16:23:49.235+02:00 level=INFO msg="received request" ip=127.0.0.1:56536 proto=HTTP/1.1 method=GET uri=/static/img/favicon.ico
time=2023-09-06T16:23:51.687+02:00 level=INFO msg="received request" ip=127.0.0.1:56536 proto=HTTP/1.1 method=GET uri="/snippet/view?id=2"

Dependiendo de como el navegador haya cacheado los archivos, puede que tengas que hacer un hard refresh o abrir la ventana en incognito para ver las request a los archivos staticos.

5.4 Panic Recovery

En un aplicaion simple de Go, cuando nuestro codigo arroja un panic, resultara en que nuestra app terminara de una manera extrania.

Pero nuestra web app es un poco mas sofisticada, el server HTTP de Go asume que el efecto de cada panic esta aislado en la goroutine de la peticion HTTP activa ( recuerda que cada peticion es manejada en su propia goroutine).

Despues de un panico nuestro servidor registrara el erro en el log con todo el trakeo hasta el momento del suceso y dentro de la goroutine llamara a las funciones defer y cerrara las conexiones subyacentes, pero no finalizara nuestra app, Lo importante aca es que los panic en nuestros handlers no detendran el servidor.

Pero que vera el usuario si un un panic se dispara en uno de nuestros handler?

Por default recibira una respuseta vacia y se cerrara la conexion HTTP subyacente luego del panic.

No es un buena experiencia para el usuario , lo mas apropiado seria retornar la respuesta HTTP correspondiente con un 500 Internal Server Error .

Una manera limpia de hacerlo seria creando un middleware el cual haga un recover el panic y llame a app.serverError() .Para hacer esto podemos aprovechar que las funcione defer siempre se llaman luego de que haya un panic.

Vamos a middleware.go y agregamos el siguiente codigo.

package main 

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


func (app *application) recoverPanic(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		// Create a deferred function (which will always be run in the event
		// of a panic as Go unwinds the stack).
		defer func() {
			// Use the builtin recover function to check if there has been a
			// panic or not. If there has...
			if err := recover(); err != nil {
				// Set a "Connection: close" header on the response.
				w.Header().Set("Connection", "close")
				// Call the app.serverError helper method to return a 500
				// Internal Server response.
				app.serverError(w, r, fmt.Errorf("%s", err))
			}
		}()
		next.ServeHTTP(w, r)
	})
}

Hay dos cosas las cuales tenemos que explicar.

  • Configurar el header con Connection: Close en la respuesta actua como disparador que hace que el HTTP de Go automaticamente cierre la conexion actual despues de que la respuesta fue enviada. Esto tambien informa al usuario que la coneccion sera cerrada. Nota: Si el protocolo usado para la peticion es HTTP/2, Go automaticamente eliminara el header Connection: Close de la respuesta para que no este mal formado y enviara un GOAWAY frame.

  • El valor devuelto por recover() tiene el tipo any, lo que significa que su tipo exacto puede variar según lo que se haya pasado como argumento a la función panic(). En otras palabras, puede ser un string, un error u otro tipo de dato, dependiendo de lo que se haya pasado a panic().

Vamos a usarlo en routes.go , lo agregaremos al principio de la cadena asi puede hacer el recover sobre todos los sub middleware y handlers.

package main

import "net/http"

func (app *application) routes() http.Handler {
	mux := http.NewServeMux()
	fileServer := http.FileServer(http.Dir("./ui/static/"))
	mux.Handle("/static/", http.StripPrefix("/static", fileServer))
	mux.HandleFunc("/", app.home)
	mux.HandleFunc("/snippet/view", app.snippetView)
	mux.HandleFunc("/snippet/create", app.snippetCreate)
-	return app.logRequest(sercureHeaders(mux))
+ return app.recoverPanic(app.logRequest(secureHeaders(mux))
}

Si reniciamos el servidor ahora cuando se dispare un panic veremos un error como este :

$ curl -i http://localhost:4000
HTTP/1.1 500 Internal Server Error
Connection: close
Content-Security-Policy: default-src 'self'; style-src 'self' fonts.googleapis.com; font-src fonts.gstatic.com
Content-Type: text/plain; charset=utf-8
Referrer-Policy: origin-when-cross-origin
X-Content-Type-Options: nosniff
X-Frame-Options: deny
X-Xss-Protection: 0
Date: Wed, 06 Sep 2023 14:29:33 GMT
Content-Length: 22
Internal Server Error

Podes forzar este error yendo a handler home y pone panic("ooops un error")

Antes de seguir recuerda quitar este panic forzado.

5.4 Informacion adicional

5.4 Panic recovery in background goroutines.

Es importante darse cuenta que nuestro middleware solo recuperara los panicos que sucedan en la misma goroutine que haya ejecutado el middleware recoverPanic().

Si por ejemplo tenes un handler el cual tiene otra goroutine ( hace algo en background ), esos panic que sucedan en el la segunda goroutine no pasaran por recoverPanic() lo que causara que nuestro servidor se cierre.

Por lo que si tenemos goroutine adicionales dentro de nuestro web server , y hay alguna chance de que haya un panico . debemos estar serguro de hacer el recover dentro de ellos. Por ejemplo:

func (app *application) myHandler(w http.ResponseWriter, r *http.Request) {

	// Spin up a new goroutine to do some background processing.
	go func() {
		defer func() {
			if err := recover(); err != nil {
				app.logger.Error(fmt.Sprint(err))
			}
		}()
		doSomeBackgroundProcessing()
	}()
	w.Write([]byte("OK"))
}

5.5 Composable middleware chains

Vamos a introducir ahora el paquete justinas/alice que nos ayudara a manejar nuestras cadenas de middleware/handlers .run/

No es necesario que uses este paquete, pero la razon por la cual es recomendable usarlo es que hace mas facil crear composiciones entre las cadenas de middleware y nos permite crear variables y poderlas reutilizar. Donde veremos que es de mayor utilidad es cuando nuestra app crezca y se vuelva mas compleja, otra cosa positiva es que es un paquete muy liviano.

Actualmente nuestras cadenas de middleware se ve asi:

return myMiddleware1(myMiddleware2(myMiddleware3(myHandler)))

Con el paquete, lograremos que se vea asi:

return alice.New(myMiddleware1, myMiddleware2, myMiddleware3).Then(myHandler)

Pero el real poder reside en el hecho de que podemos crear una cadena de middleware y asignarlas a variables y hacer appends y reusarlas, asi:

myChain := alice.New(myMiddlewareOne, myMiddlewareTwo)
myOtherChain := myChain.Append(myMiddleware3)
return myOtherChain.Then(myHandler)

Si venis siguiendo los post , instala el paquete justina/alice usando _ go get_:

$ go get github.com/justinas/alice@v1
go: downloading github.com/justinas/alice v1.2.0

Vamos a routes.go y usemoslo asi:

package main

import (
	"github.com/justinas/alice" // New import
	"net/http"
)

func (app *application) routes() http.Handler {
	mux := http.NewServeMux()
	fileServer := http.FileServer(http.Dir("./ui/static/"))
	mux.Handle("/static/", http.StripPrefix("/static", fileServer))
	mux.HandleFunc("/", app.home)
	mux.HandleFunc("/snippet/view", app.snippetView)
	mux.HandleFunc("/snippet/create", app.snippetCreate)
+	// Create a middleware chain containing our 'standard' middleware
+	// which will be used for every request our application receives.
+	standard := alice.New(app.recoverPanic, app.logRequest, sercureHeaders)
+	// Return the 'standard' middleware chain followed by the servemux.
+	return standard.Then(mux)
}

Podes reiniciar el servidor y ver que todo sigue funcionando.