mirror of
https://github.com/project-zot/zot.git
synced 2026-06-15 20:07:55 +08:00
controller: support rate-limiting incoming requests
helps constraining resource usage and against flood attacks. Signed-off-by: Ramkumar Chinchani <rchincha@cisco.com>
This commit is contained in:
committed by
Ramkumar Chinchani
parent
f251e7af10
commit
1e5ea7e09c
@@ -47,6 +47,16 @@ type BearerConfig struct {
|
||||
Cert string
|
||||
}
|
||||
|
||||
type MethodRatelimitConfig struct {
|
||||
Method string
|
||||
Rate int
|
||||
}
|
||||
|
||||
type RatelimitConfig struct {
|
||||
Rate *int // requests per second
|
||||
Methods []MethodRatelimitConfig `mapstructure:",omitempty"`
|
||||
}
|
||||
|
||||
type HTTPConfig struct {
|
||||
Address string
|
||||
Port string
|
||||
@@ -54,8 +64,9 @@ type HTTPConfig struct {
|
||||
Auth *AuthConfig
|
||||
RawAccessControl map[string]interface{} `mapstructure:"accessControl,omitempty"`
|
||||
Realm string
|
||||
AllowReadAccess bool `mapstructure:",omitempty"`
|
||||
ReadOnly bool `mapstructure:",omitempty"`
|
||||
AllowReadAccess bool `mapstructure:",omitempty"`
|
||||
ReadOnly bool `mapstructure:",omitempty"`
|
||||
Ratelimit *RatelimitConfig `mapstructure:",omitempty"`
|
||||
}
|
||||
|
||||
type LDAPConfig struct {
|
||||
|
||||
+42
-1
@@ -8,7 +8,10 @@ import (
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"net/http"
|
||||
"runtime"
|
||||
"strings"
|
||||
goSync "sync"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/docker/distribution/registry/storage/driver/factory"
|
||||
@@ -68,6 +71,27 @@ func DefaultHeaders() mux.MiddlewareFunc {
|
||||
}
|
||||
}
|
||||
|
||||
func DumpRuntimeParams(log log.Logger) {
|
||||
var rLimit syscall.Rlimit
|
||||
|
||||
evt := log.Info().Int("cpus", runtime.NumCPU())
|
||||
|
||||
err := syscall.Getrlimit(syscall.RLIMIT_NOFILE, &rLimit)
|
||||
if err == nil {
|
||||
evt = evt.Uint64("max. open files", rLimit.Cur)
|
||||
}
|
||||
|
||||
if content, err := ioutil.ReadFile("/proc/sys/net/core/somaxconn"); err == nil {
|
||||
evt = evt.Str("listen backlog", strings.TrimSuffix(string(content), "\n"))
|
||||
}
|
||||
|
||||
if content, err := ioutil.ReadFile("/proc/sys/user/max_inotify_watches"); err == nil {
|
||||
evt = evt.Str("max. inotify watches", strings.TrimSuffix(string(content), "\n"))
|
||||
}
|
||||
|
||||
evt.Msg("runtime params")
|
||||
}
|
||||
|
||||
func (c *Controller) Run() error {
|
||||
// validate configuration
|
||||
if err := c.Config.Validate(c.Log); err != nil {
|
||||
@@ -79,8 +103,25 @@ func (c *Controller) Run() error {
|
||||
// print the current configuration, but strip secrets
|
||||
c.Log.Info().Interface("params", c.Config.Sanitize()).Msg("configuration settings")
|
||||
|
||||
// print the current runtime environment
|
||||
DumpRuntimeParams(c.Log)
|
||||
|
||||
// setup HTTP API router
|
||||
engine := mux.NewRouter()
|
||||
engine.Use(DefaultHeaders(),
|
||||
|
||||
// rate-limit HTTP requests if enabled
|
||||
if c.Config.HTTP.Ratelimit != nil {
|
||||
if c.Config.HTTP.Ratelimit.Rate != nil {
|
||||
engine.Use(RateLimiter(c, *c.Config.HTTP.Ratelimit.Rate))
|
||||
}
|
||||
|
||||
for _, mrlim := range c.Config.HTTP.Ratelimit.Methods {
|
||||
engine.Use(MethodRateLimiter(c, mrlim.Method, mrlim.Rate))
|
||||
}
|
||||
}
|
||||
|
||||
engine.Use(
|
||||
DefaultHeaders(),
|
||||
SessionLogger(c),
|
||||
handlers.RecoveryHandler(handlers.RecoveryLogger(c.Log),
|
||||
handlers.PrintRecoveryStack(false)))
|
||||
|
||||
@@ -388,6 +388,127 @@ func TestHtpasswdFiveCreds(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestRatelimit(t *testing.T) {
|
||||
Convey("Make a new controller", t, func() {
|
||||
port := GetFreePort()
|
||||
baseURL := GetBaseURL(port)
|
||||
conf := config.New()
|
||||
conf.HTTP.Port = port
|
||||
|
||||
rate := 1
|
||||
conf.HTTP.Ratelimit = &config.RatelimitConfig{
|
||||
Rate: &rate,
|
||||
}
|
||||
ctlr := api.NewController(conf)
|
||||
dir, err := ioutil.TempDir("", "oci-repo-test")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer os.RemoveAll(dir)
|
||||
ctlr.Config.Storage.RootDirectory = dir
|
||||
|
||||
go startServer(ctlr)
|
||||
defer stopServer(ctlr)
|
||||
WaitTillServerReady(baseURL)
|
||||
|
||||
Convey("Ratelimit", func() {
|
||||
client := resty.New()
|
||||
// first request should succeed
|
||||
resp, err := client.R().Get(baseURL + "/v2/")
|
||||
So(err, ShouldBeNil)
|
||||
So(resp, ShouldNotBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
|
||||
// second request back-to-back should fail
|
||||
resp, err = client.R().Get(baseURL + "/v2/")
|
||||
So(err, ShouldBeNil)
|
||||
So(resp, ShouldNotBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, http.StatusTooManyRequests)
|
||||
})
|
||||
})
|
||||
|
||||
Convey("Make a new controller", t, func() {
|
||||
port := GetFreePort()
|
||||
baseURL := GetBaseURL(port)
|
||||
conf := config.New()
|
||||
conf.HTTP.Port = port
|
||||
|
||||
conf.HTTP.Ratelimit = &config.RatelimitConfig{
|
||||
Methods: []config.MethodRatelimitConfig{
|
||||
{
|
||||
Method: http.MethodGet,
|
||||
Rate: 1,
|
||||
},
|
||||
},
|
||||
}
|
||||
ctlr := api.NewController(conf)
|
||||
dir, err := ioutil.TempDir("", "oci-repo-test")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer os.RemoveAll(dir)
|
||||
ctlr.Config.Storage.RootDirectory = dir
|
||||
|
||||
go startServer(ctlr)
|
||||
defer stopServer(ctlr)
|
||||
WaitTillServerReady(baseURL)
|
||||
Convey("Method Ratelimit", func() {
|
||||
client := resty.New()
|
||||
// first request should succeed
|
||||
resp, err := client.R().Get(baseURL + "/v2/")
|
||||
So(err, ShouldBeNil)
|
||||
So(resp, ShouldNotBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
|
||||
// second request back-to-back should fail
|
||||
resp, err = client.R().Get(baseURL + "/v2/")
|
||||
So(err, ShouldBeNil)
|
||||
So(resp, ShouldNotBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, http.StatusTooManyRequests)
|
||||
})
|
||||
})
|
||||
|
||||
Convey("Make a new controller", t, func() {
|
||||
port := GetFreePort()
|
||||
baseURL := GetBaseURL(port)
|
||||
conf := config.New()
|
||||
conf.HTTP.Port = port
|
||||
|
||||
rate := 1
|
||||
conf.HTTP.Ratelimit = &config.RatelimitConfig{
|
||||
Rate: &rate, // this dominates
|
||||
Methods: []config.MethodRatelimitConfig{
|
||||
{
|
||||
Method: http.MethodGet,
|
||||
Rate: 100,
|
||||
},
|
||||
},
|
||||
}
|
||||
ctlr := api.NewController(conf)
|
||||
dir, err := ioutil.TempDir("", "oci-repo-test")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer os.RemoveAll(dir)
|
||||
ctlr.Config.Storage.RootDirectory = dir
|
||||
|
||||
go startServer(ctlr)
|
||||
defer stopServer(ctlr)
|
||||
WaitTillServerReady(baseURL)
|
||||
Convey("Global and Method Ratelimit", func() {
|
||||
client := resty.New()
|
||||
// first request should succeed
|
||||
resp, err := client.R().Get(baseURL + "/v2/")
|
||||
So(err, ShouldBeNil)
|
||||
So(resp, ShouldNotBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
|
||||
// second request back-to-back should fail
|
||||
resp, err = client.R().Get(baseURL + "/v2/")
|
||||
So(err, ShouldBeNil)
|
||||
So(resp, ShouldNotBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, http.StatusTooManyRequests)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestBasicAuth(t *testing.T) {
|
||||
Convey("Make a new controller", t, func() {
|
||||
port := GetFreePort()
|
||||
|
||||
+31
-1
@@ -7,6 +7,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/didip/tollbooth/v6"
|
||||
"github.com/gorilla/mux"
|
||||
"zotregistry.io/zot/pkg/extensions/monitoring"
|
||||
"zotregistry.io/zot/pkg/log"
|
||||
@@ -25,7 +26,7 @@ func (w *statusWriter) WriteHeader(status int) {
|
||||
|
||||
func (w *statusWriter) Write(b []byte) (int, error) {
|
||||
if w.status == 0 {
|
||||
w.status = 200
|
||||
w.status = http.StatusOK
|
||||
}
|
||||
|
||||
n, err := w.ResponseWriter.Write(b)
|
||||
@@ -34,6 +35,35 @@ func (w *statusWriter) Write(b []byte) (int, error) {
|
||||
return n, err
|
||||
}
|
||||
|
||||
// RateLimiter limits handling of incoming requests.
|
||||
func RateLimiter(ctlr *Controller, rate int) mux.MiddlewareFunc {
|
||||
ctlr.Log.Info().Int("rate", rate).Msg("ratelimiter enabled")
|
||||
|
||||
limiter := tollbooth.NewLimiter(float64(rate), nil)
|
||||
limiter.SetMessage(http.StatusText(http.StatusTooManyRequests)).
|
||||
SetStatusCode(http.StatusTooManyRequests).
|
||||
SetOnLimitReached(nil)
|
||||
|
||||
return func(next http.Handler) http.Handler {
|
||||
return tollbooth.LimitHandler(limiter, next)
|
||||
}
|
||||
}
|
||||
|
||||
// MethodRateLimiter limits handling of incoming requests.
|
||||
func MethodRateLimiter(ctlr *Controller, method string, rate int) mux.MiddlewareFunc {
|
||||
ctlr.Log.Info().Str("method", method).Int("rate", rate).Msg("per-method ratelimiter enabled")
|
||||
|
||||
limiter := tollbooth.NewLimiter(float64(rate), nil)
|
||||
limiter.SetMethods([]string{method}).
|
||||
SetMessage(http.StatusText(http.StatusTooManyRequests)).
|
||||
SetStatusCode(http.StatusTooManyRequests).
|
||||
SetOnLimitReached(nil)
|
||||
|
||||
return func(next http.Handler) http.Handler {
|
||||
return tollbooth.LimitHandler(limiter, next)
|
||||
}
|
||||
}
|
||||
|
||||
// SessionLogger logs session details.
|
||||
func SessionLogger(ctlr *Controller) mux.MiddlewareFunc {
|
||||
logger := ctlr.Log.With().Str("module", "http").Logger()
|
||||
|
||||
Reference in New Issue
Block a user