feat(api): add rate limiting

This commit is contained in:
Sam 2022-05-26 00:41:06 +02:00
parent 52a03b4aa6
commit 2ee1087eec
4 changed files with 41 additions and 0 deletions

View File

@ -43,6 +43,8 @@ type APIError struct {
Message string `json:"message,omitempty"` Message string `json:"message,omitempty"`
Details string `json:"details,omitempty"` Details string `json:"details,omitempty"`
RatelimitReset *int `json:"ratelimit_reset,omitempty"`
// Status is set as the HTTP status code. // Status is set as the HTTP status code.
Status int `json:"-"` Status int `json:"-"`
} }
@ -67,6 +69,7 @@ const (
ErrForbidden = 403 ErrForbidden = 403
ErrNotFound = 404 ErrNotFound = 404
ErrMethodNotAllowed = 405 ErrMethodNotAllowed = 405
ErrTooManyRequests = 429
ErrInternalServerError = 500 // catch-all code for unknown errors ErrInternalServerError = 500 // catch-all code for unknown errors
// Login/authorize error codes // Login/authorize error codes
@ -83,6 +86,7 @@ var errCodeMessages = map[int]string{
ErrForbidden: "Forbidden", ErrForbidden: "Forbidden",
ErrInternalServerError: "Internal server error", ErrInternalServerError: "Internal server error",
ErrNotFound: "Not found", ErrNotFound: "Not found",
ErrTooManyRequests: "Rate limit reached",
ErrMethodNotAllowed: "Method not allowed", ErrMethodNotAllowed: "Method not allowed",
ErrInvalidState: "Invalid OAuth state", ErrInvalidState: "Invalid OAuth state",
@ -97,6 +101,7 @@ var errCodeStatuses = map[int]int{
ErrForbidden: http.StatusForbidden, ErrForbidden: http.StatusForbidden,
ErrInternalServerError: http.StatusInternalServerError, ErrInternalServerError: http.StatusInternalServerError,
ErrNotFound: http.StatusNotFound, ErrNotFound: http.StatusNotFound,
ErrTooManyRequests: http.StatusTooManyRequests,
ErrMethodNotAllowed: http.StatusMethodNotAllowed, ErrMethodNotAllowed: http.StatusMethodNotAllowed,
ErrInvalidState: http.StatusBadRequest, ErrInvalidState: http.StatusBadRequest,

View File

@ -3,11 +3,14 @@ package server
import ( import (
"net/http" "net/http"
"os" "os"
"strconv"
"time"
"codeberg.org/u1f320/pronouns.cc/backend/db" "codeberg.org/u1f320/pronouns.cc/backend/db"
"codeberg.org/u1f320/pronouns.cc/backend/server/auth" "codeberg.org/u1f320/pronouns.cc/backend/server/auth"
"github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware" "github.com/go-chi/chi/v5/middleware"
"github.com/go-chi/httprate"
"github.com/go-chi/render" "github.com/go-chi/render"
) )
@ -41,6 +44,33 @@ func New() (*Server, error) {
// enable authentication for all routes (but don't require it) // enable authentication for all routes (but don't require it)
s.Router.Use(s.maybeAuth) 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 // return an API error for not found + method not allowed
s.Router.NotFound(func(w http.ResponseWriter, r *http.Request) { s.Router.NotFound(func(w http.ResponseWriter, r *http.Request) {
render.Status(r, errCodeStatuses[ErrNotFound]) render.Status(r, errCodeStatuses[ErrNotFound])

2
go.mod
View File

@ -8,6 +8,7 @@ require (
github.com/bwmarrin/discordgo v0.25.0 github.com/bwmarrin/discordgo v0.25.0
github.com/georgysavva/scany v0.3.0 github.com/georgysavva/scany v0.3.0
github.com/go-chi/chi/v5 v5.0.7 github.com/go-chi/chi/v5 v5.0.7
github.com/go-chi/httprate v0.5.3
github.com/go-chi/render v1.0.1 github.com/go-chi/render v1.0.1
github.com/golang-jwt/jwt/v4 v4.4.1 github.com/golang-jwt/jwt/v4 v4.4.1
github.com/jackc/pgconn v1.12.0 github.com/jackc/pgconn v1.12.0
@ -21,6 +22,7 @@ require (
) )
require ( require (
github.com/cespare/xxhash/v2 v2.1.2 // indirect
github.com/go-gorp/gorp/v3 v3.0.2 // indirect github.com/go-gorp/gorp/v3 v3.0.2 // indirect
github.com/golang/protobuf v1.5.2 // indirect github.com/golang/protobuf v1.5.2 // indirect
github.com/gorilla/websocket v1.4.2 // indirect github.com/gorilla/websocket v1.4.2 // indirect

4
go.sum
View File

@ -60,6 +60,8 @@ github.com/bketelsen/crypt v0.0.4/go.mod h1:aI6NrJ0pMGgvZKL1iVgXLnfIFJtfV+bKCoqO
github.com/bwmarrin/discordgo v0.25.0 h1:NXhdfHRNxtwso6FPdzW2i3uBvvU7UIQTghmV2T4nqAs= github.com/bwmarrin/discordgo v0.25.0 h1:NXhdfHRNxtwso6FPdzW2i3uBvvU7UIQTghmV2T4nqAs=
github.com/bwmarrin/discordgo v0.25.0/go.mod h1:NJZpH+1AfhIcyQsPeuBKsUtYrRnjkyu0kIVMCHkZtRY= github.com/bwmarrin/discordgo v0.25.0/go.mod h1:NJZpH+1AfhIcyQsPeuBKsUtYrRnjkyu0kIVMCHkZtRY=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE=
github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
@ -95,6 +97,8 @@ github.com/georgysavva/scany v0.3.0/go.mod h1:q8QyrfXjmBk9iJD00igd4lbkAKEXAH/zIY
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/go-chi/chi/v5 v5.0.7 h1:rDTPXLDHGATaeHvVlLcR4Qe0zftYethFucbjVQ1PxU8= github.com/go-chi/chi/v5 v5.0.7 h1:rDTPXLDHGATaeHvVlLcR4Qe0zftYethFucbjVQ1PxU8=
github.com/go-chi/chi/v5 v5.0.7/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= github.com/go-chi/chi/v5 v5.0.7/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
github.com/go-chi/httprate v0.5.3 h1:5HPWb0N6ymIiuotMtCfOGpQKiKeqXVzMexHh1W1yXPc=
github.com/go-chi/httprate v0.5.3/go.mod h1:kYR4lorHX3It9tTh4eTdHhcF2bzrYnCrRNlv5+IBm2M=
github.com/go-chi/render v1.0.1 h1:4/5tis2cKaNdnv9zFLfXzcquC9HbeZgCnxGnKrltBS8= github.com/go-chi/render v1.0.1 h1:4/5tis2cKaNdnv9zFLfXzcquC9HbeZgCnxGnKrltBS8=
github.com/go-chi/render v1.0.1/go.mod h1:pq4Rr7HbnsdaeHagklXub+p6Wd16Af5l9koip1OvJns= github.com/go-chi/render v1.0.1/go.mod h1:pq4Rr7HbnsdaeHagklXub+p6Wd16Af5l9koip1OvJns=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=