You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

180 lines
6.8 KiB
Go

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

package server
import (
"context"
"fmt"
"log/slog"
"net/http"
"git.runcible.io/learning/ratchet/internal/model"
"github.com/alexedwards/scs/v2"
"github.com/justinas/nosurf"
)
// https://owasp.org/www-project-secure-headers/ guidance
// - Headers to Add: https://owasp.org/www-project-secure-headers/ci/headers_add.json
// - Headers to Remove: https://owasp.org/www-project-secure-headers/ci/headers_remove.json
// - See also testing tools like https://github.com/ovh/venom for security testing
// Content-Security-Policy (often abbreviated to CSP) headers are used to restrict where the
// resources for your web page (e.g. JavaScript, images, fonts etc) can be loaded from. Setting
// a strict CSP policy helps prevent a variety of cross-site scripting, clickjacking, and
// other code-injection attacks.
// CSP headers and how they work is a big topic, and I recommend reading this primer if you
// havent come across them before. But, in our case, the header tells the browser that
//its OK to load fonts from fonts.gstatic.com, stylesheets from fonts.googleapis.com and
// self (our own origin), and then everything else only from self. Inline JavaScript is blocked
// by default.
// Referrer-Policy is used to control what information is included in a Referer header when
// a user navigates away from your web page. In our case, well set the value to
// origin-when-cross-origin, which means that the full URL will be included for same-origin
// requests, but for all other requests information like the URL path and any query string
// values will be stripped out.
// X-Content-Type-Options: nosniff instructs browsers to not MIME-type sniff the content-type
// of the response, which in turn helps to prevent content-sniffing attacks.
// X-Frame-Options: deny is used to help prevent clickjacking attacks in older browsers that
// dont support CSP headers.
// X-XSS-Protection: 0 is used to disable the blocking of cross-site scripting attacks.
// Previously it was good practice to set this header to X-XSS-Protection: 1; mode=block,
// but when youre using CSP headers like we are the recommendation is to disable this feature
// altogether.
// CommonHeaderMiddleware adds common headers and secure headers following OWASP guidance.
func CommonHeaderMiddleware(next http.Handler) http.Handler {
// Technically the guidance is to remove "Server" header
headers := map[string]string{"Server": "Go"}
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
for k, v := range headers {
// know the diff between Add and Set
w.Header().Set(k, v)
}
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)
})
}
func RequestLoggingMiddleware(next http.Handler, logger *slog.Logger) 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()
)
logger.Info("received request", "ip", ip, "proto", proto, "method", method, "uri", uri)
next.ServeHTTP(w, r)
})
}
// RecoveryMiddleware recovers from panics that occur within http handler functions
//
// Go's HTTP server assumes that any panic is isolated to the goroutine serving the
// active http request. Following a panic the server will log a stack trace to the
// server error log, unwind the stack of the affected goroutine, calling defered functions
// along the way, and closing the underlying http connection. This doesn't terminate the application.
//
// Important this only will recover panics raised within the same goroutine that was
// throwing the panic. This means that if the handler is spinning off it's own goroutine
// you need to add a defer recover function like this into that goroutine or your server
// will crash since the http Server will not be handling the panic in the request go routine
func RecoveryMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer func() {
if err := recover(); err != nil {
// Go's HTTP server will close the connection for us after the response with
// this header has been sent. http/2 it will strip it and send a GOAWAY frame
// instead
w.Header().Set("Connection", "close")
serverError(w, r, fmt.Errorf("%s", err))
}
}()
next.ServeHTTP(w, r)
})
}
func RequireAuthenticationMiddleware(next http.Handler, sm *scs.SessionManager) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// If the user is not authenticated, redirect them to the login page and
// return from the middleware chain so that no subsequent handlers in
// the chain are executed.
if !isAuthenticated(r) {
http.Redirect(w, r, "/user/login", http.StatusSeeOther)
return
}
// Otherwise set the "Cache-Control: no-store" header so that pages
// require authentication are not stored in the users browser cache (or
// other intermediary cache).
w.Header().Add("Cache-Control", "no-store")
next.ServeHTTP(w, r)
})
}
// NoSurfMiddleware uses the noSurf package to create a customized CSRF cookie
// with the Secure, Path and HttpOnly attributes set
func NoSurfMiddleware(next http.Handler) http.Handler {
csrfHandler := nosurf.New(next)
csrfHandler.SetBaseCookie(http.Cookie{
HttpOnly: true,
Path: "/",
Secure: true,
})
return csrfHandler
}
func AuthenticateMiddleware(next http.Handler, sm *scs.SessionManager, userService model.UserServiceInterface) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
id := sm.GetInt(r.Context(), "authenticatedUserID")
if id == 0 {
// no authenticated user
next.ServeHTTP(w, r)
return
}
exists, err := userService.Exists(id)
if err != nil {
serverError(w, r, err)
return
}
// If a matching user is found, we know that the request is
// coming from an authenticated user who exists in our database. We
// create a new copy of the request (with an isAuthenticatedContextKey
// value of true in the request context) and assign it to r.
if exists {
ctx := context.WithValue(r.Context(), isAuthenticatedKey, true)
r = r.WithContext(ctx)
}
next.ServeHTTP(w, r)
})
}
// CacheHeaders is a middleware that provides cache-control headers to be used
// for static assests.
func CacheHeaders(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// TODO parameterize cache control via config
w.Header().Set("Cache-Control", "private, max-age=21600")
next.ServeHTTP(w, r)
})
}