feat: Add rate-limits to the API (#848)

Closes #285.
This commit is contained in:
Kyle Carberry
2022-04-04 17:32:05 -05:00
committed by GitHub
parent 473aa6bd3a
commit 31536186f7
5 changed files with 82 additions and 2 deletions

View File

@ -47,14 +47,23 @@ func New(options *Options) (http.Handler, func()) {
r := chi.NewRouter() r := chi.NewRouter()
r.Route("/api/v2", func(r chi.Router) { r.Route("/api/v2", func(r chi.Router) {
r.Use(chitrace.Middleware()) r.Use(
chitrace.Middleware(),
// Specific routes can specify smaller limits.
httpmw.RateLimitPerMinute(512),
)
r.Get("/", func(w http.ResponseWriter, r *http.Request) { r.Get("/", func(w http.ResponseWriter, r *http.Request) {
httpapi.Write(w, http.StatusOK, httpapi.Response{ httpapi.Write(w, http.StatusOK, httpapi.Response{
Message: "👋", Message: "👋",
}) })
}) })
r.Route("/files", func(r chi.Router) { r.Route("/files", func(r chi.Router) {
r.Use(httpmw.ExtractAPIKey(options.Database, nil)) r.Use(
httpmw.ExtractAPIKey(options.Database, nil),
// This number is arbitrary, but reading/writing
// file content is expensive so it should be small.
httpmw.RateLimitPerMinute(12),
)
r.Get("/{hash}", api.fileByHash) r.Get("/{hash}", api.fileByHash)
r.Post("/", api.postFile) r.Post("/", api.postFile)
}) })

View File

@ -0,0 +1,35 @@
package httpmw
import (
"net/http"
"time"
"github.com/go-chi/httprate"
"github.com/go-chi/render"
"github.com/coder/coder/coderd/database"
"github.com/coder/coder/coderd/httpapi"
)
// RateLimitPerMinute returns a handler that limits requests per-minute based
// on IP, endpoint, and user ID (if available).
func RateLimitPerMinute(count int) func(http.Handler) http.Handler {
return httprate.Limit(
count,
1*time.Minute,
httprate.WithKeyFuncs(func(r *http.Request) (string, error) {
// Prioritize by user, but fallback to IP.
apiKey, ok := r.Context().Value(apiKeyContextKey{}).(database.APIKey)
if ok {
return apiKey.UserID.String(), nil
}
return httprate.KeyByIP(r)
}, httprate.KeyByEndpoint),
httprate.WithLimitHandler(func(w http.ResponseWriter, r *http.Request) {
render.Status(r, http.StatusTooManyRequests)
render.JSON(w, r, httpapi.Response{
Message: "You've been rate limited for sending too many requests!",
})
}),
)
}

View File

@ -0,0 +1,32 @@
package httpmw_test
import (
"net/http"
"net/http/httptest"
"testing"
"time"
"github.com/go-chi/chi/v5"
"github.com/stretchr/testify/require"
"github.com/coder/coder/coderd/httpmw"
)
func TestRateLimit(t *testing.T) {
t.Parallel()
t.Run("NoUser", func(t *testing.T) {
t.Parallel()
rtr := chi.NewRouter()
rtr.Use(httpmw.RateLimitPerMinute(5))
rtr.Get("/", func(rw http.ResponseWriter, r *http.Request) {
rw.WriteHeader(http.StatusOK)
})
require.Eventually(t, func() bool {
req := httptest.NewRequest("GET", "/", nil)
rec := httptest.NewRecorder()
rtr.ServeHTTP(rec, req)
return rec.Result().StatusCode == http.StatusTooManyRequests
}, 5*time.Second, time.Millisecond)
})
}

2
go.mod
View File

@ -94,6 +94,8 @@ require (
storj.io/drpc v0.0.30 storj.io/drpc v0.0.30
) )
require github.com/go-chi/httprate v0.5.3
require ( require (
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect
github.com/BurntSushi/toml v1.0.0 // indirect github.com/BurntSushi/toml v1.0.0 // indirect

2
go.sum
View File

@ -598,6 +598,8 @@ github.com/go-chi/chi/v4 v4.0.0-rc1/go.mod h1:Yfiy+5nynjDc7IMJiguACIro1KxlGW2dLU
github.com/go-chi/chi/v5 v5.0.0/go.mod h1:BBug9lr0cqtdAhsu6R4AAdvufI0/XBzAQSsUqJpoZOs= github.com/go-chi/chi/v5 v5.0.0/go.mod h1:BBug9lr0cqtdAhsu6R4AAdvufI0/XBzAQSsUqJpoZOs=
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-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q= github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q=