mirror of
https://github.com/coder/coder.git
synced 2025-07-21 01:28:49 +00:00
@ -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)
|
||||||
})
|
})
|
||||||
|
35
coderd/httpmw/ratelimit.go
Normal file
35
coderd/httpmw/ratelimit.go
Normal 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!",
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
32
coderd/httpmw/ratelimit_test.go
Normal file
32
coderd/httpmw/ratelimit_test.go
Normal 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
2
go.mod
@ -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
2
go.sum
@ -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=
|
||||||
|
Reference in New Issue
Block a user