|
|
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
|
|
|
// haven’t come across them before. But, in our case, the header tells the browser that
|
|
|
//it’s 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, we’ll 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
|
|
|
// don’t 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 you’re 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)
|
|
|
})
|
|
|
}
|