Skip to content

feat: Add rate-limits to the API #848

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Apr 4, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 11 additions & 2 deletions coderd/coderd.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,14 +47,23 @@ func New(options *Options) (http.Handler, func()) {

r := chi.NewRouter()
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) {
httpapi.Write(w, http.StatusOK, httpapi.Response{
Message: "👋",
})
})
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.Post("/", api.postFile)
})
Expand Down
35 changes: 35 additions & 0 deletions coderd/httpmw/ratelimit.go
Original file line number Diff line number Diff line change
@@ -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!",
})
}),
)
}
32 changes: 32 additions & 0 deletions coderd/httpmw/ratelimit_test.go
Original file line number Diff line number Diff line change
@@ -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 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,8 @@ require (
storj.io/drpc v0.0.30
)

require github.com/go-chi/httprate v0.5.3

require (
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect
github.com/BurntSushi/toml v1.0.0 // indirect
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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.7 h1:rDTPXLDHGATaeHvVlLcR4Qe0zftYethFucbjVQ1PxU8=
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/go.mod h1:pq4Rr7HbnsdaeHagklXub+p6Wd16Af5l9koip1OvJns=
github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q=
Expand Down