package api import ( "errors" "expvar" "fmt" "net/http" "strconv" "strings" "sync" "time" "gardomatic.kleiax.de/internal/auth" "gardomatic.kleiax.de/internal/platform/validate" "gardomatic.kleiax.de/internal/storage" "github.com/tomasen/realip" "golang.org/x/time/rate" ) func (app *application) recoverPanic(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { defer func() { pv := recover() if pv != nil { w.Header().Set("Connection", "close") app.serverErrorResponse(w, r, fmt.Errorf("%v", pv)) } }() next.ServeHTTP(w, r) }) } func (app *application) rateLimit(next http.Handler) http.Handler { if !app.config.Limiter.Enabled { return next } type client struct { limiter *rate.Limiter lastSeen time.Time } var ( mu sync.Mutex clients = make(map[string]*client) ) go func() { for { time.Sleep(time.Minute) mu.Lock() for ip, client := range clients { if time.Since(client.lastSeen) > 3*time.Minute { delete(clients, ip) } } mu.Unlock() } }() return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { ip := realip.FromRequest(r) 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 != "" { headerParts := strings.Split(authorizationHeader, " ") if len(headerParts) != 2 || headerParts[0] != "Bearer" { app.invalidAuthenticationTokenResponse(w, r) return } token := headerParts[1] v := validate.New() if auth.ValidateTokenPlaintext(v, token); !v.Valid() { app.invalidAuthenticationTokenResponse(w, r) return } user, err := app.models.Users.GetForToken(auth.ScopeAuthentication, token) if err != nil { switch { case errors.Is(err, storage.ErrRecordNotFound): app.invalidAuthenticationTokenResponse(w, r) default: app.serverErrorResponse(w, r, err) } return } r = app.contextSetAuthenticatedUser(r, user) next.ServeHTTP(w, r) return } userID := app.sessions.GetInt(r.Context(), authenticatedUserIDSessionKey) if userID != 0 { user, err := app.models.Users.GetByID(userID) if err != nil { switch { case errors.Is(err, storage.ErrRecordNotFound): if err := app.sessions.Destroy(r.Context()); err != nil { app.serverErrorResponse(w, r, err) return } default: app.serverErrorResponse(w, r, err) return } } else { r = app.contextSetAuthenticatedUser(r, user) } } next.ServeHTTP(w, r) }) } func (app *application) requireActivatedUser(next http.HandlerFunc) http.HandlerFunc { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { authenticatedUser, found := app.contextGetAuthenticatedUser(r) if !found { app.authenticationRequiredResponse(w, r) return } if !authenticatedUser.Activated { app.inactiveAccountResponse(w, r) return } next.ServeHTTP(w, r) }) } func (app *application) requireMutableAccount(next http.HandlerFunc) http.HandlerFunc { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { user, found := app.contextGetAuthenticatedUser(r) if !found { app.authenticationRequiredResponse(w, r) return } if app.isProtectedDemoAccount(user.Email) { app.demoAccountProtectedResponse(w, r) return } next.ServeHTTP(w, r) }) } func (app *application) isProtectedDemoAccount(email string) bool { return app.config.DemoAccountEmail != "" && strings.EqualFold(strings.TrimSpace(email), app.config.DemoAccountEmail) } func (app *application) requireGardenMember(next http.HandlerFunc) http.HandlerFunc { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { authenticatedUser, found := app.contextGetAuthenticatedUser(r) if !found { app.authenticationRequiredResponse(w, r) return } gardenID, err := app.readGardenIDParam(r) if err != nil { app.notFoundResponse(w, r) return } member, err := app.models.GardenMembers.Get(gardenID, authenticatedUser.ID) if err != nil { switch { case errors.Is(err, storage.ErrRecordNotFound): app.notFoundResponse(w, r) default: app.serverErrorResponse(w, r, err) } return } r = app.contextSetGardenMember(r, member) next.ServeHTTP(w, r) }) } func (app *application) requireGardenPermission(permission storage.GardenPermission, next http.HandlerFunc) http.HandlerFunc { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { member, found := app.contextGetGardenMember(r) if !found { app.serverErrorResponse(w, r, errors.New("garden permission check without membership context")) return } if !member.Can(permission) { app.permissionDeniedResponse(w, r) return } next.ServeHTTP(w, r) }) } // authorizeGardenResource checks an object permission after the object was loaded. // Objects created by the caller use the "own" permission; every other object uses // the corresponding "other" permission. func (app *application) authorizeGardenResource(w http.ResponseWriter, r *http.Request, createdBy int, own, other storage.GardenPermission) bool { member, found := app.contextGetGardenMember(r) if !found { app.serverErrorResponse(w, r, errors.New("resource permission check without membership context")) return false } user, found := app.contextGetAuthenticatedUser(r) if !found { app.authenticationRequiredResponse(w, r) return false } permission := other if createdBy == user.ID { permission = own } if !member.Can(permission) { app.permissionDeniedResponse(w, r) return false } return true } func (app *application) requireApplicationPermission(permission storage.ApplicationPermission, next http.HandlerFunc) http.HandlerFunc { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { user, found := app.contextGetAuthenticatedUser(r) if !found { app.authenticationRequiredResponse(w, r) return } if !user.Can(permission) { app.permissionDeniedResponse(w, r) return } next.ServeHTTP(w, r) }) } 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") w.Header().Add("Vary", "Access-Control-Request-Headers") origin := r.Header.Get("Origin") if origin != "" { trustedOrigin := false for i := range app.config.Cors.TrustedOrigins { if origin == app.config.Cors.TrustedOrigins[i] { trustedOrigin = true break } } if !trustedOrigin { app.untrustedOriginResponse(w, r) return } w.Header().Set("Access-Control-Allow-Origin", origin) w.Header().Set("Access-Control-Allow-Credentials", "true") if r.Method == http.MethodOptions && r.Header.Get("Access-Control-Request-Method") != "" { w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, PATCH, DELETE, OPTIONS") w.Header().Set("Access-Control-Allow-Headers", "Authorization, Content-Type") w.WriteHeader(http.StatusNoContent) return } } next.ServeHTTP(w, r) }) } type metricsResponseWriter struct { wrapped http.ResponseWriter statusCode int headerWritten bool } func newMetricsResponseWriter(w http.ResponseWriter) *metricsResponseWriter { return &metricsResponseWriter{ wrapped: w, statusCode: http.StatusOK, } } // Header implements http.ResponseWriter. func (mw *metricsResponseWriter) Header() http.Header { return mw.wrapped.Header() } // WriteHeader implements http.ResponseWriter while retaining the first status // code for metrics. func (mw *metricsResponseWriter) WriteHeader(statusCode int) { mw.wrapped.WriteHeader(statusCode) if !mw.headerWritten { mw.statusCode = statusCode mw.headerWritten = true } } // Write implements http.ResponseWriter. func (mw *metricsResponseWriter) Write(b []byte) (int, error) { mw.headerWritten = true return mw.wrapped.Write(b) } // Unwrap exposes the underlying writer to net/http response-controller logic. func (mw *metricsResponseWriter) Unwrap() http.ResponseWriter { return mw.wrapped } func (app *application) metrics(next http.Handler) http.Handler { var ( 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) { start := time.Now() totalRequestsReceived.Add(1) mw := newMetricsResponseWriter(w) next.ServeHTTP(mw, r) totalResponsesSent.Add(1) totalResponsesSentByStatus.Add(strconv.Itoa(mw.statusCode), 1) duration := time.Since(start).Microseconds() totalProcessingTimeMicroseconds.Add(duration) }) }