From c95285e26b6f3be76cf7a0fe833e66f55cfe0138 Mon Sep 17 00:00:00 2001 From: Sam Date: Fri, 24 Feb 2023 15:53:35 +0100 Subject: [PATCH] feat(backend): separate rate limits into buckets --- backend/server/rate/rate.go | 83 +++++++++++++++++++++++++++++++++++++ backend/server/server.go | 34 +++++++++++++-- go.mod | 5 ++- go.sum | 7 +--- 4 files changed, 118 insertions(+), 11 deletions(-) create mode 100644 backend/server/rate/rate.go diff --git a/backend/server/rate/rate.go b/backend/server/rate/rate.go new file mode 100644 index 0000000..d65c90f --- /dev/null +++ b/backend/server/rate/rate.go @@ -0,0 +1,83 @@ +package rate + +import ( + "net/http" + "sort" + "strings" + "time" + + "github.com/go-chi/httprate" + "github.com/gobwas/glob" +) + +type Limiter struct { + scopes []*scopedLimiter + defaultLimiter func(http.Handler) http.Handler + + windowLength time.Duration + options []httprate.Option + + wildcardScopes []*scopedLimiter +} + +type scopedLimiter struct { + Method, Pattern string + + glob glob.Glob + handler func(http.Handler) http.Handler +} + +func NewLimiter(defaultLimit int, windowLength time.Duration, options ...httprate.Option) *Limiter { + return &Limiter{ + windowLength: windowLength, + options: options, + } +} + +func (l *Limiter) Scope(method, pattern string, requestLimit int) error { + handler := httprate.Limit(requestLimit, l.windowLength, l.options...) + + g, err := glob.Compile("/v*"+pattern, '/') + if err != nil { + return err + } + + if method == "*" { + l.wildcardScopes = append(l.wildcardScopes, &scopedLimiter{method, pattern, g, handler}) + } else { + l.scopes = append(l.scopes, &scopedLimiter{method, pattern, g, handler}) + } + return nil +} + +func (l *Limiter) Handler() func(http.Handler) http.Handler { + sort.Slice(l.scopes, func(i, j int) bool { + len1 := len(strings.Split(l.scopes[i].Pattern, "/")) + len2 := len(strings.Split(l.scopes[j].Pattern, "/")) + + return len1 > len2 + }) + l.scopes = append(l.scopes, l.wildcardScopes...) + + return l.handle +} + +func (l *Limiter) handle(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + for _, s := range l.scopes { + if (r.Method == s.Method || s.Method == "*") && s.glob.Match(r.URL.Path) { + bucket := s.Pattern + if s.Method != "*" { + bucket = s.Method + " " + s.Pattern + } + w.Header().Set("X-RateLimit-Bucket", bucket) + + s.handler(next).ServeHTTP(w, r) + return + } + } + + w.Header().Set("X-RateLimit-Bucket", "/") + l.defaultLimiter(next).ServeHTTP(w, r) + }) +} diff --git a/backend/server/server.go b/backend/server/server.go index 40b971b..1b72412 100644 --- a/backend/server/server.go +++ b/backend/server/server.go @@ -8,6 +8,7 @@ import ( "codeberg.org/u1f320/pronouns.cc/backend/db" "codeberg.org/u1f320/pronouns.cc/backend/server/auth" + "codeberg.org/u1f320/pronouns.cc/backend/server/rate" "github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5/middleware" "github.com/go-chi/httprate" @@ -45,11 +46,10 @@ func New() (*Server, error) { s.Router.Use(s.maybeAuth) // rate limit handling - // - 120 req/minute (2/s) + // - base is 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, + rateLimiter := rate.NewLimiter(120, time.Minute, httprate.WithKeyFuncs(func(r *http.Request) (string, error) { _, ok := ClaimsFromContext(r.Context()) if token := r.Header.Get("Authorization"); ok && token != "" { @@ -69,7 +69,33 @@ func New() (*Server, error) { RatelimitReset: &reset, }) }), - )) + ) + + // set scopes + // users + rateLimiter.Scope("GET", "/users/*", 60) + rateLimiter.Scope("PATCH", "/users/@me", 5) + + // members + rateLimiter.Scope("GET", "/users/*/members", 60) + rateLimiter.Scope("GET", "/users/*/members/*", 60) + + rateLimiter.Scope("POST", "/members", 5) + rateLimiter.Scope("GET", "/members/*", 60) + rateLimiter.Scope("PATCH", "/members/*", 5) + rateLimiter.Scope("DELETE", "/members/*", 5) + + // auth + rateLimiter.Scope("*", "/auth/*", 20) + rateLimiter.Scope("*", "/auth/tokens", 10) + rateLimiter.Scope("*", "/auth/invites", 10) + rateLimiter.Scope("POST", "/auth/discord/*", 10) + + // 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(rateLimiter.Handler()) // return an API error for not found + method not allowed s.Router.NotFound(func(w http.ResponseWriter, r *http.Request) { diff --git a/go.mod b/go.mod index e73e952..7092a88 100644 --- a/go.mod +++ b/go.mod @@ -10,11 +10,14 @@ require ( 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/gobwas/glob v0.2.3 github.com/golang-jwt/jwt/v4 v4.4.1 github.com/jackc/pgconn v1.12.0 + github.com/jackc/pgtype v1.11.0 github.com/jackc/pgx/v4 v4.16.0 github.com/joho/godotenv v1.4.0 github.com/mediocregopher/radix/v4 v4.1.0 + github.com/minio/minio-go/v7 v7.0.37 github.com/rs/xid v1.4.0 github.com/rubenv/sql-migrate v1.1.1 go.uber.org/zap v1.21.0 @@ -33,7 +36,6 @@ require ( github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgproto3/v2 v2.3.0 // indirect github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b // indirect - github.com/jackc/pgtype v1.11.0 // indirect github.com/jackc/puddle v1.2.1 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/compress v1.15.9 // indirect @@ -41,7 +43,6 @@ require ( github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect github.com/minio/md5-simd v1.1.2 // indirect - github.com/minio/minio-go/v7 v7.0.37 // indirect github.com/minio/sha256-simd v1.0.0 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect diff --git a/go.sum b/go.sum index faffad0..42793d4 100644 --- a/go.sum +++ b/go.sum @@ -120,6 +120,8 @@ github.com/gobuffalo/packd v1.0.1 h1:U2wXfRr4E9DH8IdsDLlRFwTZTK7hLfq9qT/QHXGVe/0 github.com/gobuffalo/packd v1.0.1/go.mod h1:PP2POP3p3RXGz7Jh6eYEf93S7vA2za6xM7QT85L4+VY= github.com/gobuffalo/packr/v2 v2.8.3 h1:xE1yzvnO56cUC0sTpKR3DIbxZgB54AftTFMhB2XEWlY= github.com/gobuffalo/packr/v2 v2.8.3/go.mod h1:0SahksCVcx4IMnigTjiFuyldmTrdTctXsOdiU5KwbKc= +github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= +github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/godror/godror v0.24.2/go.mod h1:wZv/9vPiUib6tkoDl+AZ/QLf5YZgMravZ7jxH2eQWAE= github.com/gofrs/flock v0.8.1 h1:+gYjHKf32LDeiEEFhQaotPbLuUXjY5ZqxKgXy7n59aw= @@ -397,7 +399,6 @@ github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1: github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= -github.com/rs/xid v1.2.1 h1:mhH9Nq+C1fY2l1XIpgxIiUOfNpRBYH1kKcr+qfKgjRc= github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= github.com/rs/xid v1.4.0 h1:qd7wPTDkN6KQx2VmMBLrpHkiyQwgFXRnkOLacUiaSNY= github.com/rs/xid v1.4.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= @@ -416,7 +417,6 @@ github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFR github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= -github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0= github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= @@ -497,7 +497,6 @@ golang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c/go.mod h1:jdWPYTVW3xRLrWP golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97 h1:/UOmuWzQfxxo9UtlXMwuQU8CMgg1eZXqTRwkSQJWKOI= golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa h1:zuSxTR4o9y82ebqCUJYNGJbGPo6sKVl54f/TVDObg1c= golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= @@ -573,7 +572,6 @@ golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= -golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d h1:20cMwl2fHAzkJMEA+8J4JgqBQcQGzbisXo31MIeenXI= golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220722155237-a158d28d115b h1:PxfKdU9lEEDYjdIzOtC4qFWgkU2rGHdKlKowJSMN9h0= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= @@ -653,7 +651,6 @@ golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf h1:2ucpDCmfkl8Bd/FsLtiD653Wf96cW37s+iGx93zsu4k= golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=