Skip to content

Commit 3153618

Browse files
authored
feat: Add rate-limits to the API (#848)
Closes #285.
1 parent 473aa6b commit 3153618

File tree

5 files changed

+82
-2
lines changed

5 files changed

+82
-2
lines changed

coderd/coderd.go

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,14 +47,23 @@ func New(options *Options) (http.Handler, func()) {
4747

4848
r := chi.NewRouter()
4949
r.Route("/api/v2", func(r chi.Router) {
50-
r.Use(chitrace.Middleware())
50+
r.Use(
51+
chitrace.Middleware(),
52+
// Specific routes can specify smaller limits.
53+
httpmw.RateLimitPerMinute(512),
54+
)
5155
r.Get("/", func(w http.ResponseWriter, r *http.Request) {
5256
httpapi.Write(w, http.StatusOK, httpapi.Response{
5357
Message: "👋",
5458
})
5559
})
5660
r.Route("/files", func(r chi.Router) {
57-
r.Use(httpmw.ExtractAPIKey(options.Database, nil))
61+
r.Use(
62+
httpmw.ExtractAPIKey(options.Database, nil),
63+
// This number is arbitrary, but reading/writing
64+
// file content is expensive so it should be small.
65+
httpmw.RateLimitPerMinute(12),
66+
)
5867
r.Get("/{hash}", api.fileByHash)
5968
r.Post("/", api.postFile)
6069
})

coderd/httpmw/ratelimit.go

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package httpmw
2+
3+
import (
4+
"net/http"
5+
"time"
6+
7+
"github.com/go-chi/httprate"
8+
"github.com/go-chi/render"
9+
10+
"github.com/coder/coder/coderd/database"
11+
"github.com/coder/coder/coderd/httpapi"
12+
)
13+
14+
// RateLimitPerMinute returns a handler that limits requests per-minute based
15+
// on IP, endpoint, and user ID (if available).
16+
func RateLimitPerMinute(count int) func(http.Handler) http.Handler {
17+
return httprate.Limit(
18+
count,
19+
1*time.Minute,
20+
httprate.WithKeyFuncs(func(r *http.Request) (string, error) {
21+
// Prioritize by user, but fallback to IP.
22+
apiKey, ok := r.Context().Value(apiKeyContextKey{}).(database.APIKey)
23+
if ok {
24+
return apiKey.UserID.String(), nil
25+
}
26+
return httprate.KeyByIP(r)
27+
}, httprate.KeyByEndpoint),
28+
httprate.WithLimitHandler(func(w http.ResponseWriter, r *http.Request) {
29+
render.Status(r, http.StatusTooManyRequests)
30+
render.JSON(w, r, httpapi.Response{
31+
Message: "You've been rate limited for sending too many requests!",
32+
})
33+
}),
34+
)
35+
}

coderd/httpmw/ratelimit_test.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package httpmw_test
2+
3+
import (
4+
"net/http"
5+
"net/http/httptest"
6+
"testing"
7+
"time"
8+
9+
"github.com/go-chi/chi/v5"
10+
"github.com/stretchr/testify/require"
11+
12+
"github.com/coder/coder/coderd/httpmw"
13+
)
14+
15+
func TestRateLimit(t *testing.T) {
16+
t.Parallel()
17+
t.Run("NoUser", func(t *testing.T) {
18+
t.Parallel()
19+
rtr := chi.NewRouter()
20+
rtr.Use(httpmw.RateLimitPerMinute(5))
21+
rtr.Get("/", func(rw http.ResponseWriter, r *http.Request) {
22+
rw.WriteHeader(http.StatusOK)
23+
})
24+
25+
require.Eventually(t, func() bool {
26+
req := httptest.NewRequest("GET", "/", nil)
27+
rec := httptest.NewRecorder()
28+
rtr.ServeHTTP(rec, req)
29+
return rec.Result().StatusCode == http.StatusTooManyRequests
30+
}, 5*time.Second, time.Millisecond)
31+
})
32+
}

go.mod

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,8 @@ require (
9494
storj.io/drpc v0.0.30
9595
)
9696

97+
require github.com/go-chi/httprate v0.5.3
98+
9799
require (
98100
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect
99101
github.com/BurntSushi/toml v1.0.0 // indirect

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -598,6 +598,8 @@ github.com/go-chi/chi/v4 v4.0.0-rc1/go.mod h1:Yfiy+5nynjDc7IMJiguACIro1KxlGW2dLU
598598
github.com/go-chi/chi/v5 v5.0.0/go.mod h1:BBug9lr0cqtdAhsu6R4AAdvufI0/XBzAQSsUqJpoZOs=
599599
github.com/go-chi/chi/v5 v5.0.7 h1:rDTPXLDHGATaeHvVlLcR4Qe0zftYethFucbjVQ1PxU8=
600600
github.com/go-chi/chi/v5 v5.0.7/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
601+
github.com/go-chi/httprate v0.5.3 h1:5HPWb0N6ymIiuotMtCfOGpQKiKeqXVzMexHh1W1yXPc=
602+
github.com/go-chi/httprate v0.5.3/go.mod h1:kYR4lorHX3It9tTh4eTdHhcF2bzrYnCrRNlv5+IBm2M=
601603
github.com/go-chi/render v1.0.1 h1:4/5tis2cKaNdnv9zFLfXzcquC9HbeZgCnxGnKrltBS8=
602604
github.com/go-chi/render v1.0.1/go.mod h1:pq4Rr7HbnsdaeHagklXub+p6Wd16Af5l9koip1OvJns=
603605
github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q=

0 commit comments

Comments
 (0)