From ff9da4ebc404201becf9d0e33cdd3471d2458e5c Mon Sep 17 00:00:00 2001 From: Drew Bednar Date: Sun, 26 Jan 2025 13:44:37 -0500 Subject: [PATCH] Middleware --- README.md | 15 +++++ cmd/ratchetd/main.go | 2 +- internal/logging/logging.go | 4 +- internal/server/handlers.go | 3 - internal/server/middleware.go | 103 ++++++++++++++++++++++++++++++++++ internal/server/server.go | 17 ++---- 6 files changed, 125 insertions(+), 19 deletions(-) create mode 100644 internal/server/middleware.go diff --git a/README.md b/README.md index 8fd3129..f3dcb3f 100644 --- a/README.md +++ b/README.md @@ -163,3 +163,18 @@ The [official template docs](https://pkg.go.dev/text/template#hdr-Functions) can | `{{ printf "%s-%s" .Foo .Bar }}`| Yields a formatted string containing the `.Foo` and `.Bar` values. Works in the same way as `fmt.Sprintf()`. | | `{{ len .Foo }}` | Yields the length of `.Foo` as an integer. | | `{{$bar := len .Foo}}` | Assigns the length of `.Foo` to the template variable `$bar`. | + + +## Middleware + +- Function that forms a closure over the `next.ServerHTTP` function in a call chain +- `myMiddleware → servemux → application handler` applies to all requests +- `servemux → myMiddleware → application handler` wraps specific handlers. Example Auth middleware. +- The control flow actually looks like `commonHeaders → servemux → application handler → servemux → commonHeaders` + - This means defered blocks or code after `next.ServeHTTP()` will execute on the way back through. +- Early returns before `next.ServeHTTP()` will hand controlflow back upsteam. + - Auth middle ware is a good example. `w.WriteHeader(http.StatusForbidden); return` + +### Headers + +- [Primer on Content Security Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP) \ No newline at end of file diff --git a/cmd/ratchetd/main.go b/cmd/ratchetd/main.go index 524e2d9..9ba5e52 100644 --- a/cmd/ratchetd/main.go +++ b/cmd/ratchetd/main.go @@ -31,7 +31,7 @@ func main() { // DEPENDENCY INJECTION FOR HANDLERS // Setup Logging - logger := logging.InitLogging(*logLevel) + logger := logging.InitLogging(*logLevel, false) // Setup DB Connection Pool db, err := rdb.OpenSqlite3DB(*dbPath) diff --git a/internal/logging/logging.go b/internal/logging/logging.go index 18c38c6..fbacd13 100644 --- a/internal/logging/logging.go +++ b/internal/logging/logging.go @@ -22,14 +22,14 @@ func parseLogLevel(levelStr string) slog.Level { } // InitLogggin initializes global structured logging for the entire application -func InitLogging(level string) *slog.Logger { +func InitLogging(level string, addSource bool) *slog.Logger { // Use os.Stderr // // Stderr is used for diagnostics and logging. Stdout is used for program // output. Stderr also have greater likely hood of being seen if a programs // output is being redirected. parsedLogLevel := parseLogLevel(level) - loggerHandler := slog.NewJSONHandler(os.Stderr, &slog.HandlerOptions{Level: parsedLogLevel, AddSource: true}) + loggerHandler := slog.NewJSONHandler(os.Stderr, &slog.HandlerOptions{Level: parsedLogLevel, AddSource: addSource}) logger := slog.New(loggerHandler) slog.SetDefault(logger) return logger diff --git a/internal/server/handlers.go b/internal/server/handlers.go index cceeda2..2d4f90f 100644 --- a/internal/server/handlers.go +++ b/internal/server/handlers.go @@ -14,9 +14,6 @@ import ( func handleHome(logger *slog.Logger, tc *TemplateCache, snippetService *model.SnippetService) http.Handler { return http.HandlerFunc( func(w http.ResponseWriter, r *http.Request) { - logger.Info("request received", "method", "GET", "path", "/") - // Just and example of adding a header - w.Header().Add("Server", "Go") // Retrieve Snippets from DB snippets, err := snippetService.Lastest() if err != err { diff --git a/internal/server/middleware.go b/internal/server/middleware.go new file mode 100644 index 0000000..814cbed --- /dev/null +++ b/internal/server/middleware.go @@ -0,0 +1,103 @@ +package server + +import ( + "fmt" + "log/slog" + "net/http" +) + +// 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) + }) +} diff --git a/internal/server/server.go b/internal/server/server.go index 9f1ac50..f8b3291 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -31,20 +31,11 @@ func NewRatchetServer(logger *slog.Logger, tc *TemplateCache, db *sql.DB) *Ratch // Subtree pattern for static assets router.Handle("GET /static/", http.StripPrefix("/static/", fileServer)) - // /{$} is used to prevent subtree path patterns from acting like a wildcard - // resulting in this route requiring an exact match on "/" only - // You can only include one HTTP method in a route pattern if you choose - // GET will match GET & HEAD http request methods - // router.HandleFunc("GET /{$}", rs.home) - // router.HandleFunc("GET /snippet/view/{id}", rs.snippetView) - // router.HandleFunc("GET /snippet/create", rs.snippetCreate) - - // FYI The http.HandlerFunc() adapter works by automatically adding a ServeHTTP() method to - // the passed function - // router.HandleFunc("POST /snippet/create", rs.snippetCreatePost) - // Mux Router implements the Handler interface. AKA it has a ServeHTTP receiver. // SEE we can really clean things up by moving this into routes.go and handlers.go - rs.Handler = addRoutes(router, rs.logger, rs.templateCache, db, rs.snippetService) + wrappedMux := addRoutes(router, rs.logger, rs.templateCache, db, rs.snippetService) + rs.Handler = CommonHeaderMiddleware(wrappedMux) + rs.Handler = RequestLoggingMiddleware(rs.Handler, logger) + rs.Handler = RecoveryMiddleware(rs.Handler) return rs }