feat(api): add rate limiting
This commit is contained in:
parent
52a03b4aa6
commit
2ee1087eec
4 changed files with 41 additions and 0 deletions
|
@ -43,6 +43,8 @@ type APIError struct {
|
|||
Message string `json:"message,omitempty"`
|
||||
Details string `json:"details,omitempty"`
|
||||
|
||||
RatelimitReset *int `json:"ratelimit_reset,omitempty"`
|
||||
|
||||
// Status is set as the HTTP status code.
|
||||
Status int `json:"-"`
|
||||
}
|
||||
|
@ -67,6 +69,7 @@ const (
|
|||
ErrForbidden = 403
|
||||
ErrNotFound = 404
|
||||
ErrMethodNotAllowed = 405
|
||||
ErrTooManyRequests = 429
|
||||
ErrInternalServerError = 500 // catch-all code for unknown errors
|
||||
|
||||
// Login/authorize error codes
|
||||
|
@ -83,6 +86,7 @@ var errCodeMessages = map[int]string{
|
|||
ErrForbidden: "Forbidden",
|
||||
ErrInternalServerError: "Internal server error",
|
||||
ErrNotFound: "Not found",
|
||||
ErrTooManyRequests: "Rate limit reached",
|
||||
ErrMethodNotAllowed: "Method not allowed",
|
||||
|
||||
ErrInvalidState: "Invalid OAuth state",
|
||||
|
@ -97,6 +101,7 @@ var errCodeStatuses = map[int]int{
|
|||
ErrForbidden: http.StatusForbidden,
|
||||
ErrInternalServerError: http.StatusInternalServerError,
|
||||
ErrNotFound: http.StatusNotFound,
|
||||
ErrTooManyRequests: http.StatusTooManyRequests,
|
||||
ErrMethodNotAllowed: http.StatusMethodNotAllowed,
|
||||
|
||||
ErrInvalidState: http.StatusBadRequest,
|
||||
|
|
|
@ -3,11 +3,14 @@ package server
|
|||
import (
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"codeberg.org/u1f320/pronouns.cc/backend/db"
|
||||
"codeberg.org/u1f320/pronouns.cc/backend/server/auth"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/go-chi/chi/v5/middleware"
|
||||
"github.com/go-chi/httprate"
|
||||
"github.com/go-chi/render"
|
||||
)
|
||||
|
||||
|
@ -41,6 +44,33 @@ func New() (*Server, error) {
|
|||
// enable authentication for all routes (but don't require it)
|
||||
s.Router.Use(s.maybeAuth)
|
||||
|
||||
// rate limit handling
|
||||
// - 120 req/minute (2/s)
|
||||
// - keyed by Authorization header if valid token is provided, otherwise by IP
|
||||
// - returns rate limit reset info in error
|
||||
s.Router.Use(httprate.Limit(
|
||||
120, time.Minute,
|
||||
httprate.WithKeyFuncs(func(r *http.Request) (string, error) {
|
||||
_, ok := ClaimsFromContext(r.Context())
|
||||
if token := r.Header.Get("Authorization"); ok && token != "" {
|
||||
return token, nil
|
||||
}
|
||||
|
||||
ip, err := httprate.KeyByIP(r)
|
||||
return ip, err
|
||||
}),
|
||||
httprate.WithLimitHandler(func(w http.ResponseWriter, r *http.Request) {
|
||||
reset, _ := strconv.Atoi(w.Header().Get("X-RateLimit-Reset"))
|
||||
|
||||
render.Status(r, http.StatusTooManyRequests)
|
||||
render.JSON(w, r, APIError{
|
||||
Code: ErrTooManyRequests,
|
||||
Message: errCodeMessages[ErrTooManyRequests],
|
||||
RatelimitReset: &reset,
|
||||
})
|
||||
}),
|
||||
))
|
||||
|
||||
// return an API error for not found + method not allowed
|
||||
s.Router.NotFound(func(w http.ResponseWriter, r *http.Request) {
|
||||
render.Status(r, errCodeStatuses[ErrNotFound])
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue