package main import ( "fmt" "net" "net/http" "sync" "time" "strings" "errors" "expvar" "strconv" "github.com/felixge/httpsnoop" "golang.org/x/time/rate" "party.at/party/internal/data" "party.at/party/internal/validator" ) func (app *application) recoverPanic(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // Create a deferred function (which will always be run in the event of a panic // as Go unwinds the stack). defer func() { // Use the builtin recover function to check if there has been a panic or // not. if err := recover(); err != nil { // If there was a panic, set a "Connection: close" header on the // response. This acts as a trigger to make Go's HTTP server // automatically close the current connection after a response has been // sent. w.Header().Set("Connection", "close") // The value returned by recover() has the type interface{}, so we use // fmt.Errorf() to normalize it into an error and call our // serverErrorResponse() helper. In turn, this will log the error using // our custom Logger type at the ERROR level and send the client a 500 // Internal Server Error response. app.serverErrorResponse(w, r, fmt.Errorf("%s", err)) } }() next.ServeHTTP(w, r) }) } func (app *application) rateLimit(next http.Handler) http.Handler { type client struct { limiter *rate.Limiter lastSeen time.Time } var mu sync.Mutex var clients = make(map[string]*client) go func() { for { time.Sleep(time.Minute) // Lock the mutex to prevent any rate limiter checks from happening while // the cleanup is taking place. mu.Lock() // Loop through all clients. If they haven't been seen within the last three // minutes, delete the corresponding entry from the map. for ip, client := range clients { if time.Since(client.lastSeen) > 3*time.Minute { delete(clients, ip) } } // Importantly, unlock the mutex when the cleanup is complete. mu.Unlock() } }() return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if app.config.limiter.enabled { ip, _, err := net.SplitHostPort(r.RemoteAddr) if err != nil { app.serverErrorResponse(w, r, err) return } mu.Lock() if _, found := clients[ip]; !found { clients[ip] = &client{limiter: rate.NewLimiter(rate.Limit(app.config.limiter.rps), app.config.limiter.burst)} } clients[ip].lastSeen = time.Now() if !clients[ip].limiter.Allow() { mu.Unlock() app.rateLimitExceededResponse(w, r) return } mu.Unlock() } next.ServeHTTP(w, r) }) } func (app *application) authenticate(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Add("Vary", "Authorization") authorizationHeader := r.Header.Get("Authorization") if authorizationHeader == "" { r = app.contextSetUser(r, data.AnonymousUser) next.ServeHTTP(w, r) return } headerParts := strings.Split(authorizationHeader, " ") if len(headerParts) != 2 || headerParts[0] != "Bearer" { app.invalidAuthenticationTokenResponse(w, r) return } token := headerParts[1] v := validator.New() if data.ValidateTokenPlaintext(v, token); !v.Valid() { app.invalidAuthenticationTokenResponse(w, r) return } userIdentity, err := app.models.UserIdentities.GetForToken(data.ScopeAuthentication, token) if err != nil { switch { case errors.Is(err, data.ErrRecordNotFound): app.invalidAuthenticationTokenResponse(w, r) default: app.serverErrorResponse(w, r, err) } return } user, err := app.models.Users.Get(userIdentity.UserID) if err != nil { switch { case errors.Is(err, data.ErrRecordNotFound): app.invalidCredentialsResponse(w, r) default: app.serverErrorResponse(w, r, err) } return } // Call the contextSetUser() helper to add the user information to the request // context. r = app.contextSetUser(r, user) // Call the next handler in the chain. next.ServeHTTP(w, r) }) } func (app *application) requireAuthenticatedUser(next http.HandlerFunc) http.HandlerFunc { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { user := app.contextGetUser(r) if user.IsAnonymous() { app.authenticationRequiredResponse(w, r) return } next.ServeHTTP(w, r) }) } func (app *application) requireActivatedUser(next http.HandlerFunc) http.HandlerFunc { // Rather than returning this http.HandlerFunc we assign it to the variable fn. fn := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { user := app.contextGetUser(r) // Check that a user is activated. if !user.Activated { app.inactiveAccountResponse(w, r) return } next.ServeHTTP(w, r) }) // Wrap fn with the requireAuthenticatedUser() middleware before returning it. return app.requireAuthenticatedUser(fn) } func (app *application) requirePermission(code string, next http.HandlerFunc) http.HandlerFunc { fn := func(w http.ResponseWriter, r *http.Request) { // Retrieve the user from the request context. user := app.contextGetUser(r) // Get the slice of permissions for the user. permissions, err := app.models.Permissions.GetAllForUser(user.ID) if err != nil { app.serverErrorResponse(w, r, err) return } // Check if the slice includes the required permission. If it doesn't, then // return a 403 Forbidden response. if !permissions.Include(code) { app.notPermittedResponse(w, r) return } // Otherwise they have the required permission so we call the next handler in // the chain. next.ServeHTTP(w, r) } // Wrap this with the requireActivatedUser() middleware before returning it. return app.requireActivatedUser(fn) } func (app *application) enableCORS(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Add("Vary", "Origin") w.Header().Add("Vary", "Access-Control-Request-Method") origin := r.Header.Get("Origin") if origin != "" && len(app.config.cors.trustedOrigins) != 0 { // Loop through the list of trusted origins, checking to see if the request // origin exactly matches one of them. for i := range app.config.cors.trustedOrigins { if origin == app.config.cors.trustedOrigins[i] { // If there is a match, then set a "Access-Control-Allow-Origin" // response header with the request origin as the value. w.Header().Set("Access-Control-Allow-Origin", origin) if r.Method == http.MethodOptions && r.Header.Get("Access-Control-Request-Method") != "" { // Set the necessary preflight response headers, as discussed // previously. w.Header().Set("Access-Control-Allow-Methods", "OPTIONS, PUT, PATCH, DELETE") w.Header().Set("Access-Control-Allow-Headers","Authorization, Content-Type") // Write the headers along with a 200 OK status and return from // the middleware with no further action. w.WriteHeader(http.StatusOK) return } } } } next.ServeHTTP(w, r) }) } func (app *application) metrics(next http.Handler) http.Handler { // Initialize the new expvar variables when the middleware chain is first built. totalRequestsReceived := expvar.NewInt("total_requests_received") totalResponsesSent := expvar.NewInt("total_responses_sent") totalProcessingTimeMicroseconds := expvar.NewInt("total_processing_time_μs") totalResponsesSentByStatus := expvar.NewMap("total_responses_sent_by_status") return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { totalRequestsReceived.Add(1) // Call the next handler in the chain. metrics := httpsnoop.CaptureMetrics(next, w, r) // On the way back up the middleware chain, increment the number of responses // sent by 1. totalResponsesSent.Add(1) totalProcessingTimeMicroseconds.Add(metrics.Duration.Microseconds()) totalResponsesSentByStatus.Add(strconv.Itoa(metrics.Code), 1) }) }