9. Servidores y mejoras de seguridad

En este post vamos a centrarnos en hacer mejoras en nuestra aplicacion con lo que a HTTP server respecta, veremos:

  • Como personalizar las opciones del servidor usando http.Server type.
  • Que es server error log y como configurarlo para que use la estructura logger.
  • Cuan rapido y facil es crar un certificado TLS auto firmado, usando solo Go.
  • Las configuraciones fundamentales de nuestra app para que todas las peticiones y respuestas sean servidas de manera segura a traves de HTTPS.
  • Algunos ajustes en la configuracion por default TLS para que nos ayude a mantener la informacion del usuario segura y nuestro servidor funcione rapido.
  • Como configurar timeouts en las conexiones para mitigar el impacto de ataques que relentizan nuestra app.

9.1 La estructura http.Server

En post anteriores usamos la funcion shortcut http.ListenAndServe() para inicar nuetro server.

A pesar de que http.ListenAndServe es muy util en ejemplos cortos y tutoriales, en el mundo real es mas comun crear y usar la estructura http.Server. Hacer esto nos abre la oportunidad de personalizar el comportamiento de nuestro server lo cual es exactamente lo que vamos a hacer en este post.

Lo primero es ir a nuestro archivo main.go y dejar de usar http.ListenAndServe para crear manualmente y usar la estructura http.Server.

  package main
  
  import (
  	"database/sql"
  	"flag"
  	"html/template"
  	"log"
  	"net/http"
  	"os"
  	"time"
  
  	"log/slog"
  
  	"github.com/alexedwards/scs/mysqlstore"
  	"github.com/alexedwards/scs/v2"
  	"github.com/go-playground/form/v4"
  	_ "github.com/go-sql-driver/mysql"
  	"github.com/nahueldev23/snippetbox/internal/models"
  )
  
  type application struct {
  	logger         *slog.Logger
  	snippets       *models.SnippetModel
  	templateCache  map[string]*template.Template
  	formDecoder    *form.Decoder
  	sessionManager *scs.SessionManager
  }
  
  func main() {
  	addr := flag.String("addr", ":4000", "HTTP network address")
+  //tu password
  	dsn := flag.String("dsn", "web:xxxxx/snippetbox?parseTime=true", "HTTP network address")
  
  	flag.Parse()
  
  	logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
  
  	db, err := openDB(*dsn)
  	if err != nil {
     	logger.Error(err.Error())
        os.Exit(1)
  	}
  
  	defer db.Close()
  	templateCache, err := newTemplateCache()
  	if err != nil {
  		logger.Error(err.Error())
                 os.Exit(1)
  	}
  
  	// Initialize a decoder instance...
  	formDecoder := form.NewDecoder()
  
  	sessionManager := scs.New()
  	sessionManager.Store = mysqlstore.New(db)
  	sessionManager.Lifetime = 12 * time.Hour
  
  	app := &application{
  		logger:         logger,
  		snippets:       &models.SnippetModel{DB: db},
  		templateCache:  templateCache,
  		formDecoder:    formDecoder,
  		sessionManager: sessionManager,
  	}

+    // Initialize a new http.Server struct. We set the Addr and Handler fields so
+	  // that the server uses the same network address and routes as before.
+	  srv := &http.Server{
+		  Addr:    *addr,
+		  Handler: app.routes(),
+	  }

  
- 	srv := &http.Server{
-  		Addr:     *addr,
-  		ErrorLog: errorLog,
-  		Handler:  app.routes(),
-  	}
  
  	logger.Info("Starting server", "addr", *addr)
  
  	err = srv.ListenAndServe()
  	logger.Error(err.Error())
  	os.Exit(1)
  }
  
 

Este cambio no afecta al comportamiento de nuesta app (aun) , pero nos sirve para el trabajo que se viene.

9.2 El registro de errores del servidor

Es importante ser consciente que http.Server tiene que escribir sus propias entradas de log relacionados con cosas como panicos que no pudieron ser recuperados o problemas aceptando o escribiendo conexiones HTTP.

Por default escribe sus entradas usando el standard logger lo cual significa que escribira con el flujo de error estandar en vez del estandar que usamos para nuestras entradas de registo , por lo que no se formateara de la misma manera que el resto de nuestros logs.

Vamos a demostrarlo creando un error deliberado,setando un header invalido en el helper render().

func (app *application) render(w http.ResponseWriter, r *http.Request, status int, page string, data *templateData) {
  
  	ts, ok := app.templateCache[page]
  
  	if !ok {
  		err := fmt.Errorf("the template %s does not exist", page)
  		app.serverError(w, r, err)
  		return
  	}
  
  	buff := new(bytes.Buffer)
  
  	err := ts.ExecuteTemplate(buff, "base", data)
  
  	if err != nil {
  		app.serverError(w, r, err)
  	}
  
        // Deliberate error: set a Content-Length header with an invalid (non-integer)
        // value.
        w.Header().Set("Content-Length", "this isn't an integer!")
  	w.WriteHeader(status)
  
  	buff.WriteTo(w)
  }

Reinicmoas el servidor y veremos el siguente log:

$ go run ./cmd/web/
time=2023-09-06T18:40:55.881+02:00 level=INFO msg="starting server" addr=:4000
time=2023-09-06T18:40:58.741+02:00 level=INFO msg="received request" ip=127.0.0.1:60824 proto=HTTP/1.1 method=GET uri=/
2023/09/06 18:40:58 http: invalid Content-Length of "this isn't an integer!"
time=2023-09-06T18:40:58.754+02:00 level=INFO msg="received request" ip=127.0.0.1:60824 proto=HTTP/1.1 method=GET uri=/static/css/main.css
time=2023-09-06T18:40:59.014+02:00 level=INFO msg="received request" ip=127.0.0.1:60830 proto=HTTP/1.1 method=GET uri=/static/img/logo.png

Podemos ver que el error en la linea 4 tiene un formato diferente a los demas. Esto no esta bueno, especialmente si vamos a usar filtros para ver los erores o usar un servicio externo para monitorizarlos y enviarnos alertas.

Desafortunadamente no podemos configurar http.Server para usar nuestra estructura logger directamente. En su lugar podemos convertir nuestra estructura logger en un *log_Logger el cual escribe entradas de log en un nivel especifico y luego registrarlo en http.Server. Podemos hacer esta conversion usando slog.NewLogLogger() asi:

func main() {
	addr := flag.String("addr", ":4000", "HTTP network address")
  //tu pass
	dsn := flag.String("dsn", "web:xxxx/snippetbox?parseTime=true", "HTTP network address")

	flag.Parse()

	logger := slog.New(slog.NewTextHandler(os.Stdout, nil))

	db, err := openDB(*dsn)
	if err != nil {
		logger.Error(err.Error())
		os.Exit(1)
	}

	defer db.Close()
	templateCache, err := newTemplateCache()
	if err != nil {
		logger.Error(err.Error())
		os.Exit(1)
	}


	formDecoder := form.NewDecoder()

	sessionManager := scs.New()
	sessionManager.Store = mysqlstore.New(db)
	sessionManager.Lifetime = 12 * time.Hour

	app := &application{
		logger:         logger,
		snippets:       &models.SnippetModel{DB: db},
		templateCache:  templateCache,
		formDecoder:    formDecoder,
		sessionManager: sessionManager,
	}

	srv := &http.Server{
		Addr:    *addr,
		Handler: app.routes(),
		// Create a *log.Logger from our structured logger handler, which writes
		// log entries at Error level, and assign it to the ErrorLog field. If
		// you would prefer to log the server errors at Warn level instead, you
		// could pass slog.LevelWarn as the final parameter.
		ErrorLog: slog.NewLogLogger(logger.Handler(), slog.LevelError),
	}

	logger.Info("Starting server", "addr", *addr)

	err = srv.ListenAndServe()
	logger.Error(err.Error())
	os.Exit(1)
}

Con eso en su lugar, cualqueir mensaje en http.Server automaticamente escribira usando nuestra estructura logger como Error level. Si reiniciamos la app y hacemos otra peticion a localhost:4000 deberiamos ver algo asi:

$ go run ./cmd/web/
time=2023-09-06T18:42:52.321+02:00 level=INFO msg="starting server" addr=:4000
time=2023-09-06T18:42:54.808+02:00 level=INFO msg="received request" ip=127.0.0.1:40854 proto=HTTP/1.1 method=GET uri=/
time=2023-09-06T18:42:54.809+02:00 level=ERROR msg="http: invalid Content-Length of \"this isn't an integer!\""

Antes de seguir recuerda quitar ese error en cmd/web/helpers.go render().

9.3 Generando certificados TLS auto firmados.

Cambiemos nuetra atencion para qeu ahora nuestra app use HTTPS (En vez de HTTP plano) para todas nuestras respuestas y peticiones.

HTTP es esencialmente HTTP enviado a traves de una conexion TLS (Transport Layer Security). La ventaja de hacer esto es que en HTTPS el trafico es encriptado y firmado , lo cual ayuda a asegurar la privacidad y la integridad durante los viajes de informacion.

Si no estas familiarizado con el termino, TLS es la version moderna de SSL (Secure Socket Layer). SSL es oficialmente deprecado por motivos de seguridad, pero el nombre aun vive en la conciencia publica y se usa a menudo interpolando con TLS , por claridad en mis post usare el termino TLS.

Antes de que nuestro server pueda usar HTTPS, necesitamos generar un certificado TLS.

Para servidores en produccion recomiendo usar Let’s Encrypt para crear nuestro certificados TLS, pero para propositios de desarrollo la manera simple de hacerlo es generar nuestro propio certificado auto firmado.

Un certificado autofirmado es lo mismo que un certificado TLS, excepto que no esta criptograficamente firmado por una autoridad certificada. Esto significa que nusetro navegador arrojara una advertencia la primera vez que lo use, pero de todos modos encriptara el trafico correctamente y esta bien para propositos de desarrollo y testing.

Convenientemente el paquete de Go crypto/tls incluye una herramienta generate_cert.go que podemos usar par crear facilmente un certificado autofirmado.

Creemos un directorio en el root de nuestra app llamado tls

$ cd $HOME/code/snippetbox
$ mkdir tls
$ cd tls

Para ejecutar la herramienta generate_cert.go, deberás saber dónde se encuentra en tu computadora el código fuente de la biblioteca estándar de Go. Si estás utilizando Linux, macOS o FreeBSD y has seguido las instrucciones de instalación oficiales, entonces el archivo generate_cert.go debería estar ubicado en /usr/local/go/src/crypto/tls.

Si estás utilizando macOS y has instalado Go utilizando Homebrew, es probable que el archivo se encuentre en /usr/local/Cellar/go/<versión>/libexec/src/crypto/tls/generate_cert.go o en una ruta similar.

Una vez sepas donde seta localizado usaremos la herramienta asi:

$ go run /usr/local/go/src/crypto/tls/generate_cert.go --rsa-bits=2048 --host=localhost
2023/09/06 18:53:09 wrote cert.pem
2023/09/06 18:53:09 wrote key.pem

generate_cert.go funciona haciendo dos cosas:

  1. Primero genera un par de keys RSA 2048-bit, el cual es una clave publica y privada criptograficamente segura.
  2. Almacena la key privada en key.pem y genera un certificado TLS auto firmado para el host localhost que contiene la clave publica (La cual se almacena en cert.pem). Tanto la key privada como la publica estan codificadas en PEM. el cual es un formato estandar pusado en la mayoria de las implementaciones TLS.

9.4 Corriendo el servidor con HTTPS.

Ahora que tenemos un certificado TLS auto firmado y la corresopondiente llave privada, arranquemos el web server con HTTPS. Solo tenemos que ir a main.go y cambiar srv.ListenAndServe() por srv.ListenAndServeTLS()

  func main() {
  	addr := flag.String("addr", ":4000", "HTTP network address")
      //tu pass
  	dsn := flag.String("dsn", "web:xxxx/snippetbox?parseTime=true", "HTTP network address")
  
  	flag.Parse()
  
  	logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
  
  	db, err := openDB(*dsn)
  	if err != nil {
  		logger.Error(err.Error())
  		os.Exit(1)
  	}
  
  	defer db.Close()
  	templateCache, err := newTemplateCache()
  	if err != nil {
  		logger.Error(err.Error())
  		os.Exit(1)
  	}
  
  	formDecoder := form.NewDecoder()
  
  	sessionManager := scs.New()
  	sessionManager.Store = mysqlstore.New(db)
  	sessionManager.Lifetime = 12 * time.Hour
+  	// Make sure that the Secure attribute is set on our session cookies.
+  	// Setting this means that the cookie will only be sent by a user's web
+  	// browser when a HTTPS connection is being used (and won't be sent over an
+  	// unsecure HTTP connection).
+  	sessionManager.Cookie.Secure = true
  
  	app := &application{
  		logger:         logger,
  		snippets:       &models.SnippetModel{DB: db},
  		templateCache:  templateCache,
  		formDecoder:    formDecoder,
  		sessionManager: sessionManager,
  	}
  
  	srv := &http.Server{
  		Addr:     *addr,
  		Handler:  app.routes(),
  		ErrorLog: slog.NewLogLogger(logger.Handler(), slog.LevelError),
  	}
  
  	logger.Info("Starting server", "addr", *addr)
  
+  	// Use the ListenAndServeTLS() method to start the HTTPS server. We
+  	// pass in the paths to the TLS certificate and corresponding private key as
+  	// the two parameters.
+  	err = srv.ListenAndServeTLS("./tls/cert.pem", "./tls/key.pem")
  	logger.Error(err.Error())
  	os.Exit(1)
  }

Cuando iniciemos el server ahora la diferencia es que estamos usando HTTPS en vez de HTTP.

$ cd $HOME/code/snippetbox
$ go run ./cmd/web
time=2023-09-06T18:57:54.860+02:00 level=INFO msg="starting server" addr=:4000

Abri el navegador en ingresa con el prefijo https https://localhost:4000/ va a ver un cartel de advertencia, pone que queres continuar de todos modos.

Si estas en firefox podemos hacer Ctrl+i para inspeccionar el certificado.

Si te estas preguntando quien es el verificador Acme Co es un nombre de relleno que usa generate_cert.go.

9.4 Informacion adicional

9.4 HTTP request

Es importante notar que nuestro server HTTPS solo soporta HTTPS!, si intentas haceer una peticion normal a HTTP el server te contestara con un 400 Bad Request y un mensaje “Client sent an HTTP request to an HTTPS server”. Esto no sera logeado.

$ curl -i http://localhost:4000/
HTTP/1.0 400 Bad Request
Client sent an HTTP request to an HTTPS server.
9.4 Conexiones HTTP/2

Una gran ventaja de usar HTTPS es que Go automaticamente actualizara la conexion a http/2 si el cliente lo soporta.

Esto es bueno porque significa que nuestras paginas cargaran mas rapido a nuestros usuarios. Si no estas familiarizado con HTTP/2 podes obtener un resumen de los conceptos basicos y una idea de como funciona lo que esta implementado detras de Go en este video de Brad Fitzpatrick.

Si estas usando una version actualizada de firefox presiona Ctrl+Shift+E para abrir las herramientas de desarrollo.Si hechamos un vistado a los headers en network veremos la version HTTP/2.

9.4 Permisos en los certificados

Es importante darse cuenta de que el usuario de sistema que eta corriendo tu app de Go tiene que tener permisos de lectura sobre ambos certificados cert.pem y key.pem , de otra manera ListenAndServeTLS() retornara el error permission denied.

Por default generate_cert.go otorga permisos de lectura para todos los usuarios en cert.pem pero solo de lectura para el creador en key.pem.

Generalmente es buena idea mantener los permisos de nuestras claves privadas lo mas acotada posible , permitiendo leer solo al crador o a un grupo especifico.

9.4 .gitignore

Si estas usando un controlador de versiones como Git tenes que agregar al .gitignnore el directorio tls para que no sea enviado al repositorio por accidente.

$ cd $HOME/code/snippetbox
$ echo 'tls/' >> .gitignore

9.5 Configurando las opciones HTTPS

Go tiene unas buenas configuraciones por default para el servidor HTTPS, pero es posible optimizarlo y personalizar como se comportara el servidor.

Uno de los cambios que simpre es buena idea hacer, es restringir las curvas elipticas (elliptic curves) que potencialmente pueden ser usados por el handShake del TLS. Go soporta algunos elliptic curves pero a partir de Go 1.21 solo tls.CurveP256 y tls.X25519 tienen implementaciones de assembly, las demas con muy costosas para el CPU, entonces las omitiremos para ayudar a estar seguros que nuestro server mantendra performance bajo cargas pesadas.

Para hacer estos ajustes, podemso crear una structura tls.Config que contenga todas las configuraciones personalizadas para el TLS. y agregarla en nuestra estructura http.Server antes de inicalizar el servidor.Asi:

  package main
  
  import (
+  	"crypto/tls"
  	"database/sql"
  	"flag"
  	"html/template"
  	"net/http"
  	"os"
  	"time"
  
  	"log/slog"
  
  	"github.com/alexedwards/scs/mysqlstore"
  	"github.com/alexedwards/scs/v2"
  	"github.com/go-playground/form/v4"
  	_ "github.com/go-sql-driver/mysql"
  	"github.com/nahueldev23/snippetbox/internal/models"
  )
  
  ...
  
  func main() {
  	addr := flag.String("addr", ":4000", "HTTP network address")
  
      //tu pass
  	dsn := flag.String("dsn", "web:xxxx/snippetbox?parseTime=true", "HTTP network address")
  
  	flag.Parse()
  
  	logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
  
  	db, err := openDB(*dsn)
  	if err != nil {
  		logger.Error(err.Error())
  		os.Exit(1)
  	}
  
  	defer db.Close()
  	templateCache, err := newTemplateCache()
  	if err != nil {
  		logger.Error(err.Error())
  		os.Exit(1)
  	}
  
  	formDecoder := form.NewDecoder()
  
  	sessionManager := scs.New()
  	sessionManager.Store = mysqlstore.New(db)
  	sessionManager.Lifetime = 12 * time.Hour
    
  	sessionManager.Cookie.Secure = true
  
  	app := &application{
  		logger:         logger,
  		snippets:       &models.SnippetModel{DB: db},
  		templateCache:  templateCache,
  		formDecoder:    formDecoder,
  		sessionManager: sessionManager,
  	}
  
+  	// Initialize a tls.Config struct to hold the non-default TLS settings we
+  	// want the server to use. In this case the only thing that we're changing
+  	// is the curve preferences value, so that only elliptic curves with
+  	// assembly implementations are used.
+  	tlsConfig := &tls.Config{
+  		CurvePreferences: []tls.CurveID{tls.X25519, tls.CurveP256},
+  	}

+  	// Set the server's TLSConfig field to use the tlsConfig variable we just
+  	// created.
  	srv := &http.Server{
  		Addr:      *addr,
  		Handler:   app.routes(),
  		ErrorLog:  slog.NewLogLogger(logger.Handler(), slog.LevelError),
+  		TLSConfig: tlsConfig,
  	}
  
  	logger.Info("Starting server", "addr", *addr)
  
  	err = srv.ListenAndServeTLS("tls/cert.pem", "tls/key.pem")
  	logger.Error(err.Error())
  	os.Exit(1)
  }
  
  ...

9.5 Informacion adicional

9.5 Versiones TLS

Las versiones TLS tambien definidas como constantes en el paquete crypto/tls y el servidor de Go HTTPS soporta las versiones TLS desde 1.0 a 1.3.

Podes configurar el minimoy el maximo de la version TLS via tls.Config.MinVersion y MaxVersion. Por ejemplo si sabes que todas las computadores de tus usuarios adminten TLS 1.2 pero no TLS 1.3 es posible que quieras usar esta configuracion:

tlsConfig := &tls.Config{
  MinVersion: tls.VersionTLS12,
  MaxVersion: tls.VersionTLS12,
  }
9.5 Restriccion de conjuntos de cifrado.

La suite de cifrano que Go soporta tambien esta definido en las constantes del paquete crypto/tls.

Para algunas aplicaciones , puede ser conveniente limitar el servidor HTTPS para que solo admita un subconjunto de suites de cifrado. Por ejemplo, podes querer solo dar soporte de cifrado a suites que usen ECDHE (forward secrecy) y no soportar suites de cifrados debiles que usen RC4,3DES o CBC. Podes hacer esto via tls.Config.CipherSuites asi:

tlsConfig := &tls.Config{
    CipherSuites: []uint16{
    tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
    tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
    tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305,
    tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305,
    tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
    tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
  },
}

Go automaticamente elegira cual de esos suit de cifrados es usado en tiempo de ejecucion cuandno se establece una conexion segura. La desicion esta basada en la seguridad,el rendimiento y el soporte del hardware del cliente y del servidor.

Restringirr el soporte de suit de cifrado para que solo incluya las mas fuertes y modernas puede significar que los usuarios con ciertos navegadores viejos no puedan usar tu web.Tiene que haber un balance entre seguridad y retrocompatibilidad y la desicion correcta dependera de la tecnologia que los usuarios usen.Mozilla tiene recomendaciones de configuracion para aplicaciones modernas y navegadores medio antiguos, esto puede ayudarte en la toma de decision.

Es importante e interesante notar que si la conexion esta negociando CON tls 1.3 cualquier CipherSuites en neustro tls.Config es ignorado.La razon es que todas las cipher suites que soportan TLS 1.3 son consideradas seguras , asi que no tiene mucho sentido preever un mecanismo para configurarlas.

Basicamente usamos tls.Config para setear una lista personalizada de cipher suites que afectara solo a las conexiones TLS 1.0-1.2.

9.6 Tiempos de espera de conexion

Vamos a tomar un momento para mejorar la resistencia de nuestro server agregando algunas configuraciones a los tiempos de espera.

package main

...

  func main() {
  	addr := flag.String("addr", ":4000", "HTTP network address")
    // tu pass
  	dsn := flag.String("dsn", "web:xxxx/snippetbox?parseTime=true", "HTTP network address")
  
  	flag.Parse()
  
  	logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
  
  	db, err := openDB(*dsn)
  	if err != nil {
  		logger.Error(err.Error())
  		os.Exit(1)
  	}
  
  	defer db.Close()
  	templateCache, err := newTemplateCache()
  	if err != nil {
  		logger.Error(err.Error())
  		os.Exit(1)
  	}
  
  	formDecoder := form.NewDecoder()
  
  	sessionManager := scs.New()
  	sessionManager.Store = mysqlstore.New(db)
  	sessionManager.Lifetime = 12 * time.Hour
  	sessionManager.Cookie.Secure = true
  
  	app := &application{
  		logger:         logger,
  		snippets:       &models.SnippetModel{DB: db},
  		templateCache:  templateCache,
  		formDecoder:    formDecoder,
  		sessionManager: sessionManager,
  	}
  
  
  	tlsConfig := &tls.Config{
  		CurvePreferences: []tls.CurveID{tls.X25519, tls.CurveP256},
  	}
  
  	srv := &http.Server{
  		Addr:      *addr,
  		Handler:   app.routes(),
  		ErrorLog:  slog.NewLogLogger(logger.Handler(), slog.LevelError),
  		TLSConfig: tlsConfig,
+		// Add Idle, Read and Write timeouts to the server.
+		IdleTimeout:  time.Minute,
+		ReadTimeout:  5 * time.Second,
+		WriteTimeout: 10 * time.Second,
  	}
  
  	logger.Info("Starting server", "addr", *addr)
  
  	err = srv.ListenAndServeTLS("tls/cert.pem", "tls/key.pem")
  	logger.Error(err.Error())
  	os.Exit(1)
  }
...

Los tres timeouts IdleTimeout , ReadTimeout y WriteTimeout son configuraciones de servidor amplias que actuan sobre las conexiones subyacentes y se aplican a todas las peticiones independientemente del controlador o URL.

9.6 La configuracion IdleTimeout

Por default , Go activa keep-alives (conexiones persistenetes) en todas las conexiones. Esto ayuda a reducir la latencia (especialmente para conexiones HTTPS ) porque el cliente puede reusar la misma conexion para multiples peticiones sin tener que vover a repetir el handShake TLS.

Por default las conexiones keep-alives se cierran automaticamente despues de un par de minutos ( el tiempo exacto depende de tu sistema operativo). Esto ayuda a limpiar conexiones donde el usuario ha desaparecido de manera inesperada, por ejemplo en un corte de luz.

No hay manera de aumentar este comportamiento predeterminado (al menos que utilices tu propio net.Listener), pero podes reducirlo via IdleTimeout . En nuestro caso lo seteamos a 1 minuto, lo cual significa que todas las conexiones keep-alives seran automaticamente cerradas luego de 1 minuto de inactividad.

9.6 La configuracion ReadTimeout

En nuestro codigo seteamos ReadTimeout a 5 segundos . Esto significa que si los headers de la peticion o el body todavia se estan leyendo pasados esos 5 segunods despues de que la peticion es aceptada, entonces Go cerrara la conexion subyacente, como este es una cierre forzado de la conexion, el usuario no recibira ninguna respuesta HTTP(S).

Configurar un ReadTimeout corto ayuda a mitigar el riesgo de slow-client attacks como lo es Slowloris El cual podria mantener una conexion abierta indefinidamente enviando peticiones parciales o completas.

importante: Si seteas ReadTimeout pero no seteas IdleTimeout, entonces IdleTimeout usara por default la misma configuracion que ReadTimeout . Por ejemplo Si seteaste ReadTimeout en 3 segundos, entonces el efecto secundario sera que el keep-alives de las conexiones tambien seran de 3 segundos.Por lo general es mejor evitar ambiguedades y setaer IdleTimeout en el server.

9.6 La configuracion WriteTimeout

La configuracion WriteTimeout cerrara la conexion subyacente si nuestro servidor intenta escribir en la conexion despues de tiempo dado ( en nustro caso 10 segundos ). Pero esto se comporta ligeramente diferente dependiendo del protocolo que se esta usando.

  • HTTP Connections (Conexiones HTTP): En el caso de conexiones HTTP (no seguras), si se intenta escribir datos en la conexión después de que haya pasado más de 10 segundos desde que se completó la lectura de los encabezados de la solicitud HTTP, Go cerrará la conexión subyacente en lugar de escribir los datos.*
  • HTTPS Connections (Conexiones HTTPS): En el caso de conexiones HTTPS (seguras), si se intenta escribir datos en la conexión después de que haya pasado más de 10 segundos desde que se aceptó la solicitud inicial, Go también cerrará la conexión subyacente en lugar de escribir los datos.

Esto significa que si usamos HTTPS (como en nuestro caso) es sensato setear WriteTimeout con un valor mayor que ReadTimeout. Esto se debe a que, en HTTPS, la lectura de la solicitud y la escritura de la respuesta pueden llevar más tiempo debido a la necesidad de cifrar y descifrar datos en conexiones seguras. Configurar WriteTimeout más alto que ReadTimeout garantiza que el servidor tenga suficiente tiempo para enviar respuestas en conexiones seguras sin riesgo de cierre inmediato debido a inactividad.

En resumen, el WriteTimeout en un servidor HTTP en Go es un mecanismo para definir cuánto tiempo se permite que el servidor espere para escribir datos en una conexión después de aceptar una solicitud. Si el tiempo de espera se agota y el servidor aún no ha escrito todos los datos de respuesta, la conexión se cierra de manera abrupta. Esto se hace para evitar esperas indefinidas y liberar recursos del servidor en caso de problemas de comunicación con el cliente.

9.6 Informacion adicional

9.6 La configuracion ReadHeaderTimeout

http.Server tambien provee un ReadHeaderTimeout el cual no usamos en nuesta app. Este funciona de manera similar a ReadTimeout excepto que se aplica a la lectura de los haders HTTPS unicamente. Asi que si seteas ReadHeaderTimeout en 3 segundos , la conexion se cerrara si los headers de la peticion se siguien leyendo despues de 3 segundos despues de que la peticion fue aceptadaSin embargo, incluso si la lectura de las cabeceras se cierra después de 3 segundos, la lectura del cuerpo de la solicitud (por ejemplo, los datos de formulario o el contenido de una solicitud POST) puede continuar después de que hayan pasado 3 segundos sin que la conexión se cierre. En otras palabras, el cierre de la conexión no afecta la lectura del cuerpo de la solicitud.

Si una cabecera contiene información crítica necesaria para el procesamiento del cuerpo de la solicitud, entonces un cierre temprano debido al ReadHeaderTimeout podría afectar negativamente al procesamiento de los datos del cuerpo. Esto es una consideración importante al configurar ReadHeaderTimeout. La idea detrás de ReadHeaderTimeout es proporcionar una protección contra solicitudes que podrían ser maliciosas o diseñadas para consumir recursos del servidor, por ejemplo, enviando cabeceras excesivamente grandes. Sin embargo, en situaciones donde las cabeceras son esenciales para la comprensión y el procesamiento del cuerpo de la solicitud, se debe configurar el ReadHeaderTimeout de manera que no cause interrupciones en el procesamiento legítimo. La configuración de ReadHeaderTimeout debe considerar cuidadosamente el tiempo necesario para procesar las cabeceras de manera efectiva sin interrupciones y debe ser suficiente para acomodar situaciones normales. Si tienes cabeceras críticas en tus solicitudes que deben procesarse junto con el cuerpo, debes establecer un valor apropiado para ReadHeaderTimeout que permita que esto ocurra sin problemas.

Esto puede ser util si se desea aplicar un limite a todo el servidor al leer los encabezado, pero a su vez se quiere implementar diferentes tiempos de espera para cada ruta en cuanto a lectura del body respecta.(posiblemente usando el middleware http.TimeoutHandler)

9.6 La configuracion MaxHeaderBytes

http.Server incluye un campo MaxHeaderBytes , el cual podemos usar para controlar el maximo numero de bytes que el server leera cuando parsee los headers de la peticion por default Go permite un maximo en el largo de 1MB.

Si queremos limitar el maximo de los headers a 0.5MB, podemos hacerlo asi:

srv := &http.Server{
  Addr: *addr,
  MaxHeaderBytes: 524288,
  ...
  }

Si MaxHeaderBytes es exedido , entonces el usuario recibira la respuesta 431 Request Header Fields Too Large

Hay un problema que señalar, Go siempre agrega 4096 bytes adicionales a la cifra que establezcamos. Si necesitas ser mas preciso o quereas setear un numero mas bajo que esto , necesitaras tenerlo en cuenta.