Middleware

drew/lets-go
Drew Bednar 3 months ago
parent 13445115da
commit ff9da4ebc4

@ -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)

@ -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)

@ -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

@ -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 {

@ -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
// 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)
})
}

@ -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
}

Loading…
Cancel
Save