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.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)
})

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)
})
}