diff options
author | Jaime Martinez <jmartinez@gitlab.com> | 2021-09-22 09:38:25 +0300 |
---|---|---|
committer | Jaime Martinez <jmartinez@gitlab.com> | 2021-10-14 08:59:51 +0300 |
commit | ccfdff303646b86daed2bd9ae7e2f2a5eb4a2c5c (patch) | |
tree | 81bc3444ad0417cd4454077fd187323a292bfc93 | |
parent | 247bd7ba2fd9139711218c6a42ed03c551f958d9 (diff) |
feat: add source IP ratelimiter middleware
It gets the source IP from `r.RemoteAddr` or from the `X-Forwarded-For`
header for proxied requests (when `--listen-proxy` is enabled).
The first iteration will only report logs and metrics when an IP is
being rate limited.
The rate limiter uses a Token Bucket approach using
golang.org/x/time/rate, which can be configured with the newly added
flags `rate-limit-source-ip` and `rate-limit-source-ip-burst`.
To enable the rate limiter, set `rate-limit-source-ip` to value > 1,
which is the number of requests per second to allow. It is enabled by
default in "dry-run" mode so requests won't be dropped until the
environment variable
`FF_ENABLE_RATE_LIMITER` is set to `"true"`.
See metrics.go for the newly added metrics.
Changelog: added
-rw-r--r-- | app.go | 11 | ||||
-rw-r--r-- | go.mod | 1 | ||||
-rw-r--r-- | internal/config/config.go | 11 | ||||
-rw-r--r-- | internal/config/flags.go | 2 | ||||
-rw-r--r-- | internal/httperrors/httperrors.go | 13 | ||||
-rw-r--r-- | internal/logging/logging.go | 1 | ||||
-rw-r--r-- | internal/ratelimiter/middleware.go | 89 | ||||
-rw-r--r-- | internal/ratelimiter/middleware_test.go | 180 | ||||
-rw-r--r-- | internal/ratelimiter/ratelimiter.go | 15 | ||||
-rw-r--r-- | internal/ratelimiter/ratelimiter_test.go | 82 | ||||
-rw-r--r-- | internal/testhelpers/testhelpers.go | 14 | ||||
-rw-r--r-- | metrics/metrics.go | 31 | ||||
-rw-r--r-- | test/acceptance/ratelimiter_test.go | 39 |
13 files changed, 457 insertions, 32 deletions
@@ -32,6 +32,7 @@ import ( "gitlab.com/gitlab-org/gitlab-pages/internal/httperrors" "gitlab.com/gitlab-org/gitlab-pages/internal/logging" "gitlab.com/gitlab-org/gitlab-pages/internal/netutil" + "gitlab.com/gitlab-org/gitlab-pages/internal/ratelimiter" "gitlab.com/gitlab-org/gitlab-pages/internal/rejectmethods" "gitlab.com/gitlab-org/gitlab-pages/internal/request" "gitlab.com/gitlab-org/gitlab-pages/internal/routing" @@ -262,6 +263,16 @@ func (a *theApp) buildHandlerPipeline() (http.Handler, error) { handler = routing.NewMiddleware(handler, a.source) + if a.config.RateLimit.SourceIPLimitPerSecond > 0 { + rl := ratelimiter.New( + ratelimiter.WithSourceIPLimitPerSecond(a.config.RateLimit.SourceIPLimitPerSecond), + ratelimiter.WithSourceIPBurstSize(a.config.RateLimit.SourceIPBurst), + ratelimiter.WithProxied(len(a.config.Listeners.Proxy) > 0), + ) + + handler = rl.SourceIPLimiter(handler) + } + // Health Check handler, err = a.healthCheckMiddleware(handler) if err != nil { @@ -16,6 +16,7 @@ require ( github.com/pires/go-proxyproto v0.2.0 github.com/prometheus/client_golang v1.6.0 github.com/rs/cors v1.7.0 + github.com/sebest/xff v0.0.0-20160910043805-6c115e0ffa35 github.com/sirupsen/logrus v1.7.0 github.com/stretchr/testify v1.6.1 github.com/tj/assert v0.0.3 // indirect diff --git a/internal/config/config.go b/internal/config/config.go index 94c22328..3e03f7d6 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -16,6 +16,7 @@ import ( // Config stores all the config options relevant to GitLab Pages. type Config struct { General General + RateLimit RateLimit ArtifactsServer ArtifactsServer Authentication Auth GitLab GitLab @@ -62,6 +63,12 @@ type General struct { CustomHeaders []string } +// RateLimit config struct +type RateLimit struct { + SourceIPLimitPerSecond float64 + SourceIPBurst int +} + // ArtifactsServer groups settings related to configuring Artifacts // server type ArtifactsServer struct { @@ -184,6 +191,10 @@ func loadConfig() (*Config, error) { CustomHeaders: header.Split(), ShowVersion: *showVersion, }, + RateLimit: RateLimit{ + SourceIPLimitPerSecond: *rateLimitSourceIP, + SourceIPBurst: *rateLimitSourceIPBurst, + }, GitLab: GitLab{ ClientHTTPTimeout: *gitlabClientHTTPTimeout, JWTTokenExpiration: *gitlabClientJWTExpiry, diff --git a/internal/config/flags.go b/internal/config/flags.go index 52b7be18..c61447c7 100644 --- a/internal/config/flags.go +++ b/internal/config/flags.go @@ -15,6 +15,8 @@ var ( _ = flag.Bool("use-http2", true, "DEPRECATED: HTTP2 is always enabled for pages") pagesRoot = flag.String("pages-root", "shared/pages", "The directory where pages are stored") pagesDomain = flag.String("pages-domain", "gitlab-example.com", "The domain to serve static pages") + rateLimitSourceIP = flag.Float64("rate-limit-source-ip", 0.0, "Rate limit per source IP in number of requests per second, 0 means is disabled") + rateLimitSourceIPBurst = flag.Int("rate-limit-source-ip-burst", 100, "Rate limit per source IP maximum burst allowed per second") artifactsServer = flag.String("artifacts-server", "", "API URL to proxy artifact requests to, e.g.: 'https://gitlab.com/api/v4'") artifactsServerTimeout = flag.Int("artifacts-server-timeout", 10, "Timeout (in seconds) for a proxied request to the artifacts server") pagesStatus = flag.String("pages-status", "", "The url path for a status page, e.g., /@status") diff --git a/internal/httperrors/httperrors.go b/internal/httperrors/httperrors.go index ed56ee10..8e61d590 100644 --- a/internal/httperrors/httperrors.go +++ b/internal/httperrors/httperrors.go @@ -34,6 +34,14 @@ var ( <p>Make sure the address is correct and that the page hasn't moved.</p> <p>Please contact your GitLab administrator if you think this is a mistake.</p>`, } + + content429 = content{ + http.StatusTooManyRequests, + "Too many requests (429)", + "429", + "Too many requests.", + `<p>The resource that you are attempting to access is being rate limited.</p>`, + } content500 = content{ http.StatusInternalServerError, "Something went wrong (500)", @@ -176,6 +184,11 @@ func Serve404(w http.ResponseWriter) { serveErrorPage(w, content404) } +// Serve429 returns a 429 error response / HTML page to the http.ResponseWriter +func Serve429(w http.ResponseWriter) { + serveErrorPage(w, content429) +} + // Serve500 returns a 500 error response / HTML page to the http.ResponseWriter func Serve500(w http.ResponseWriter) { serveErrorPage(w, content500) diff --git a/internal/logging/logging.go b/internal/logging/logging.go index 4ffbeb4b..27edf865 100644 --- a/internal/logging/logging.go +++ b/internal/logging/logging.go @@ -85,6 +85,7 @@ func BasicAccessLogger(handler http.Handler, format string, extraFields log.Extr return log.AccessLogger(handler, log.WithExtraFields(enrichExtraFields(extraFields)), log.WithAccessLogger(accessLogger), + // TODO: log IP for HTTP requests https://gitlab.com/gitlab-org/gitlab-pages/-/issues/640 log.WithXFFAllowed(func(sip string) bool { return false }), ), nil } diff --git a/internal/ratelimiter/middleware.go b/internal/ratelimiter/middleware.go new file mode 100644 index 00000000..d0996021 --- /dev/null +++ b/internal/ratelimiter/middleware.go @@ -0,0 +1,89 @@ +package ratelimiter + +import ( + "net" + "net/http" + "os" + + "github.com/sebest/xff" + "github.com/sirupsen/logrus" + "gitlab.com/gitlab-org/labkit/correlation" + + "gitlab.com/gitlab-org/gitlab-pages/internal/httperrors" +) + +const ( + headerGitLabRealIP = "GitLab-Real-IP" + headerXForwardedFor = "X-Forwarded-For" + headerXForwardedProto = "X-Forwarded-Proto" +) + +// SourceIPLimiter middleware ensures that the originating +// rate limit. See -rate-limiter +func (rl *RateLimiter) SourceIPLimiter(logger logrus.FieldLogger, handler http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + host, sourceIP := rl.getReqDetails(r) + + if !rl.SourceIPAllowed(sourceIP) { + rl.logSourceIP(logger, r, host, sourceIP) + + // Only drop requests once FF_ENABLE_RATE_LIMITER is enabled + // https://gitlab.com/gitlab-org/gitlab-pages/-/issues/629 + if rateLimiterEnabled() { + rl.sourceIPBlockedCount.WithLabelValues("true").Inc() + httperrors.Serve429(w) + return + } + + rl.sourceIPBlockedCount.WithLabelValues("false").Inc() + } + + handler.ServeHTTP(w, r) + }) +} + +func (rl *RateLimiter) getReqDetails(r *http.Request) (string, string) { + host, _, err := net.SplitHostPort(r.Host) + if err != nil { + host = r.Host + } + + // choose between r.RemoteAddr and X-Forwarded-For. Only uses XFF when proxied + remoteAddr := xff.GetRemoteAddrIfAllowed(r, func(sip string) bool { + // We enable github.com/gorilla/handlers.ProxyHeaders which sets r.RemoteAddr + // with the value of X-Forwarded-For when --listen-proxy is set + return rl.proxied + }) + + ip, _, err := net.SplitHostPort(remoteAddr) + if err != nil { + ip = remoteAddr + } + + return host, ip +} + +func (rl *RateLimiter) logSourceIP(logger logrus.FieldLogger, r *http.Request, host, sourceIP string) { + logger.WithFields(logrus.Fields{ + "handler": "source_ip_rate_limiter", + "correlation_id": correlation.ExtractFromContext(r.Context()), + "req_scheme": r.URL.Scheme, + "req_host": r.Host, + "req_path": r.URL.Path, + "pages_domain": host, + "remote_addr": r.RemoteAddr, + "source_ip": sourceIP, + "proxied": rl.proxied, + "x_forwarded_proto": r.Header.Get(headerXForwardedProto), + "x_forwarded_for": r.Header.Get(headerXForwardedFor), + "gitlab_real_ip": r.Header.Get(headerGitLabRealIP), + "rate_limiter_enabled": rateLimiterEnabled(), + "rate_limiter_limit_per_second": rl.sourceIPLimitPerSecond, + "rate_limiter_burst_size": rl.sourceIPBurstSize, + }). // TODO: change to Debug with https://gitlab.com/gitlab-org/gitlab-pages/-/issues/629 + Info("source IP hit rate limit") +} + +func rateLimiterEnabled() bool { + return os.Getenv("FF_ENABLE_RATE_LIMITER") == "true" +} diff --git a/internal/ratelimiter/middleware_test.go b/internal/ratelimiter/middleware_test.go new file mode 100644 index 00000000..eaa199c4 --- /dev/null +++ b/internal/ratelimiter/middleware_test.go @@ -0,0 +1,180 @@ +package ratelimiter + +import ( + "io" + "net/http" + "net/http/httptest" + "testing" + + ghandlers "github.com/gorilla/handlers" + testlog "github.com/sirupsen/logrus/hooks/test" + "github.com/stretchr/testify/require" + + "gitlab.com/gitlab-org/gitlab-pages/internal/testhelpers" +) + +const ( + xForwardedFor = "172.16.123.1" + remoteAddr = "192.168.1.1" +) + +var next = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusNoContent) +}) + +func TestSourceIPLimiterWithDifferentLimits(t *testing.T) { + log, hook := testlog.NewNullLogger() + testhelpers.EnableRateLimiter(t) + + for tn, tc := range sharedTestCases { + t.Run(tn, func(t *testing.T) { + rl := New( + WithNow(mockNow), + WithSourceIPLimitPerSecond(tc.sourceIPLimit), + WithSourceIPBurstSize(tc.sourceIPBurstSize), + WithProxied(tc.proxied), + ) + + for i := 0; i < tc.reqNum; i++ { + ww := httptest.NewRecorder() + rr := httptest.NewRequest(http.MethodGet, "https://domain.gitlab.io", nil) + rr.Header.Set(headerXForwardedFor, xForwardedFor) + rr.RemoteAddr = remoteAddr + + handler := rl.SourceIPLimiter(log, next) + if tc.proxied { + handler = ghandlers.ProxyHeaders(handler) + } + + handler.ServeHTTP(ww, rr) + res := ww.Result() + + if i < tc.sourceIPBurstSize { + require.Equal(t, http.StatusNoContent, res.StatusCode, "req: %d failed", i) + } else { + // requests should fail after reaching tc.perDomainBurstPerSecond because mockNow + // always returns the same time + require.Equal(t, http.StatusTooManyRequests, res.StatusCode, "req: %d failed", i) + b, err := io.ReadAll(res.Body) + require.NoError(t, err) + + require.Contains(t, string(b), "Too many requests.") + res.Body.Close() + + assertSourceIPLog(t, tc.proxied, xForwardedFor, remoteAddr, hook) + } + } + }) + } +} + +func TestSourceIPLimiterDenyRequestsAfterBurst(t *testing.T) { + log, hook := testlog.NewNullLogger() + + tcs := map[string]struct { + enabled bool + proxied bool + host string + expectedStatus int + }{ + "disabled_rate_limit_http": { + enabled: false, + host: "http://gitlab.com", + expectedStatus: http.StatusNoContent, + }, + "disabled_rate_limit_https": { + enabled: false, + host: "https://gitlab.com", + expectedStatus: http.StatusNoContent, + }, + "enabled_rate_limit_http_blocks": { + enabled: true, + host: "http://gitlab.com", + expectedStatus: http.StatusTooManyRequests, + }, + "enabled_rate_limit_https_blocks": { + enabled: true, + host: "https://gitlab.com", + expectedStatus: http.StatusTooManyRequests, + }, + "disabled_rate_limit_http_proxied": { + enabled: false, + proxied: true, + host: "http://gitlab.com", + expectedStatus: http.StatusNoContent, + }, + "disabled_rate_limit_https_proxied": { + enabled: false, + proxied: true, + host: "https://gitlab.com", + expectedStatus: http.StatusNoContent, + }, + "enabled_rate_limit_http_blocks_proxied": { + enabled: true, + proxied: true, + host: "http://gitlab.com", + expectedStatus: http.StatusTooManyRequests, + }, + "enabled_rate_limit_https_blocks_proxied": { + enabled: true, + proxied: true, + host: "https://gitlab.com", + expectedStatus: http.StatusTooManyRequests, + }, + } + + for tn, tc := range tcs { + t.Run(tn, func(t *testing.T) { + rl := New( + WithNow(mockNow), + WithSourceIPLimitPerSecond(1), + WithSourceIPBurstSize(1), + WithProxied(tc.proxied), + ) + + for i := 0; i < 5; i++ { + ww := httptest.NewRecorder() + rr := httptest.NewRequest(http.MethodGet, tc.host, nil) + if tc.enabled { + testhelpers.EnableRateLimiter(t) + } + + rr.Header.Set(headerXForwardedFor, xForwardedFor) + rr.RemoteAddr = remoteAddr + + // middleware is evaluated in reverse order + handler := rl.SourceIPLimiter(log, next) + if tc.proxied { + handler = ghandlers.ProxyHeaders(handler) + } + + handler.ServeHTTP(ww, rr) + res := ww.Result() + + if i == 0 { + require.Equal(t, http.StatusNoContent, res.StatusCode) + continue + } + + // burst is 1 and limit is 1 per second, all subsequent requests should fail + require.Equal(t, tc.expectedStatus, res.StatusCode) + assertSourceIPLog(t, tc.proxied, xForwardedFor, remoteAddr, hook) + } + }) + } +} + +func assertSourceIPLog(t *testing.T, proxied bool, xForwardedFor, remoteAddr string, hook *testlog.Hook) { + t.Helper() + + require.NotNil(t, hook.LastEntry()) + + // source_ip that was rate limited + if proxied { + require.Equal(t, xForwardedFor, hook.LastEntry().Data["source_ip"]) + } else { + require.Equal(t, remoteAddr, hook.LastEntry().Data["source_ip"]) + } + + hook.Reset() +} diff --git a/internal/ratelimiter/ratelimiter.go b/internal/ratelimiter/ratelimiter.go index e1cf076d..36c72cde 100644 --- a/internal/ratelimiter/ratelimiter.go +++ b/internal/ratelimiter/ratelimiter.go @@ -7,6 +7,7 @@ import ( "golang.org/x/time/rate" "gitlab.com/gitlab-org/gitlab-pages/internal/lru" + "gitlab.com/gitlab-org/gitlab-pages/metrics" ) const ( @@ -34,8 +35,10 @@ type Option func(*RateLimiter) // It also holds a now function that can be mocked in unit tests. type RateLimiter struct { now func() time.Time + proxied bool sourceIPLimitPerSecond float64 sourceIPBurstSize int + sourceIPBlockedCount *prometheus.GaugeVec sourceIPCache *lru.Cache // TODO: add domainCache https://gitlab.com/gitlab-org/gitlab-pages/-/issues/630 } @@ -46,13 +49,13 @@ func New(opts ...Option) *RateLimiter { now: time.Now, sourceIPLimitPerSecond: DefaultSourceIPLimitPerSecond, sourceIPBurstSize: DefaultSourceIPBurstSize, + sourceIPBlockedCount: metrics.RateLimitSourceIPBlockedCount, sourceIPCache: lru.New( "source_ip", defaultSourceIPItems, defaultSourceIPExpirationInterval, - // TODO: @jaime to add proper metrics in subsequent MR - prometheus.NewGaugeVec(prometheus.GaugeOpts{}, []string{"op"}), - prometheus.NewCounterVec(prometheus.CounterOpts{}, []string{"op", "cache"}), + metrics.RateLimitSourceIPCachedEntries, + metrics.RateLimitSourceIPCacheRequests, ), } @@ -84,6 +87,12 @@ func WithSourceIPBurstSize(burst int) Option { } } +// WithProxied sets the proxy flag to true. Used by the SourceIPLimiter middleware. +func WithProxied(proxied bool) Option { + return func(rl *RateLimiter) { + rl.proxied = proxied + } +} func (rl *RateLimiter) getSourceIPLimiter(sourceIP string) *rate.Limiter { limiterI, _ := rl.sourceIPCache.FindOrFetch(sourceIP, sourceIP, func() (interface{}, error) { return rate.NewLimiter(rate.Limit(rl.sourceIPLimitPerSecond), rl.sourceIPBurstSize), nil diff --git a/internal/ratelimiter/ratelimiter_test.go b/internal/ratelimiter/ratelimiter_test.go index cdf12fe6..03e764f2 100644 --- a/internal/ratelimiter/ratelimiter_test.go +++ b/internal/ratelimiter/ratelimiter_test.go @@ -17,38 +17,62 @@ func mockNow() time.Time { return validTime } +var sharedTestCases = map[string]struct { + sourceIPLimit float64 + sourceIPBurstSize int + reqNum int + proxied bool +}{ + "one_request_per_second": { + sourceIPLimit: 1, + sourceIPBurstSize: 1, + reqNum: 2, + }, + "one_request_per_second_but_big_bucket": { + sourceIPLimit: 1, + sourceIPBurstSize: 10, + reqNum: 11, + }, + "three_req_per_second_bucket_size_one": { + sourceIPLimit: 3, + sourceIPBurstSize: 1, // max burst 1 means 1 at a time + reqNum: 3, + }, + "10_requests_per_second": { + sourceIPLimit: 10, + sourceIPBurstSize: 10, + reqNum: 11, + }, + "one_request_per_second_proxied": { + proxied: true, + sourceIPLimit: 1, + sourceIPBurstSize: 1, + reqNum: 2, + }, + "one_request_per_second_but_big_bucket_proxied": { + proxied: true, + sourceIPLimit: 1, + sourceIPBurstSize: 10, + reqNum: 11, + }, + "three_req_per_second_bucket_size_one_proxied": { + proxied: true, + sourceIPLimit: 3, + sourceIPBurstSize: 1, // max burst 1 means 1 at a time + reqNum: 3, + }, + "10_requests_per_second_proxied": { + proxied: true, + sourceIPLimit: 10, + sourceIPBurstSize: 10, + reqNum: 11, + }, +} + func TestSourceIPAllowed(t *testing.T) { t.Parallel() - tcs := map[string]struct { - now string - sourceIPLimit float64 - sourceIPBurstSize int - reqNum int - }{ - "one_request_per_second": { - sourceIPLimit: 1, - sourceIPBurstSize: 1, - reqNum: 2, - }, - "one_request_per_second_but_big_bucket": { - sourceIPLimit: 1, - sourceIPBurstSize: 10, - reqNum: 11, - }, - "three_req_per_second_bucket_size_one": { - sourceIPLimit: 3, - sourceIPBurstSize: 1, // max burst 1 means 1 at a time - reqNum: 3, - }, - "10_requests_per_second": { - sourceIPLimit: 10, - sourceIPBurstSize: 10, - reqNum: 11, - }, - } - - for tn, tc := range tcs { + for tn, tc := range sharedTestCases { t.Run(tn, func(t *testing.T) { rl := New( WithNow(mockNow), diff --git a/internal/testhelpers/testhelpers.go b/internal/testhelpers/testhelpers.go index 3ec97a79..04dfc3d2 100644 --- a/internal/testhelpers/testhelpers.go +++ b/internal/testhelpers/testhelpers.go @@ -77,3 +77,17 @@ func Getwd(t *testing.T) string { return wd } + +// EnableRateLimiter environment variable +func EnableRateLimiter(t *testing.T) { + t.Helper() + + orig := os.Getenv("FF_ENABLE_RATE_LIMITER") + + err := os.Setenv("FF_ENABLE_RATE_LIMITER", "true") + require.NoError(t, err) + + t.Cleanup(func() { + os.Setenv("FF_ENABLE_RATE_LIMITER", orig) + }) +} diff --git a/metrics/metrics.go b/metrics/metrics.go index b4ac9415..23962dc4 100644 --- a/metrics/metrics.go +++ b/metrics/metrics.go @@ -184,6 +184,34 @@ var ( Help: "The number of backlogged connections waiting on concurrency limit.", }, ) + + // RateLimitSourceIPCacheRequests is the number of cache hits/misses + RateLimitSourceIPCacheRequests = prometheus.NewCounterVec( + prometheus.CounterOpts{ + Name: "gitlab_pages_rate_limit_source_ip_cache_requests", + Help: "The number of source_ip cache hits/misses in the rate limiter", + }, + []string{"op", "cache"}, + ) + + // RateLimitSourceIPCachedEntries is the number of entries in the cache + RateLimitSourceIPCachedEntries = prometheus.NewGaugeVec( + prometheus.GaugeOpts{ + Name: "gitlab_pages_rate_limit_source_ip_cached_entries", + Help: "The number of entries in the cache", + }, + []string{"op"}, + ) + + // RateLimitSourceIPBlockedCount is the number of source IPs that have been blocked by the + // source IP rate limiter + RateLimitSourceIPBlockedCount = prometheus.NewGaugeVec( + prometheus.GaugeOpts{ + Name: "gitlab_pages_rate_limit_source_ip_blocked_count", + Help: "The number of source IP addresses that have been blocked by the rate limiter", + }, + []string{"enforced"}, + ) ) // MustRegister collectors with the Prometheus client @@ -211,5 +239,8 @@ func MustRegister() { LimitListenerMaxConns, LimitListenerConcurrentConns, LimitListenerWaitingConns, + RateLimitSourceIPCacheRequests, + RateLimitSourceIPCachedEntries, + RateLimitSourceIPBlockedCount, ) } diff --git a/test/acceptance/ratelimiter_test.go b/test/acceptance/ratelimiter_test.go new file mode 100644 index 00000000..d67b3d04 --- /dev/null +++ b/test/acceptance/ratelimiter_test.go @@ -0,0 +1,39 @@ +package acceptance_test + +import ( + "net/http" + "testing" + "time" + + "github.com/stretchr/testify/require" + + "gitlab.com/gitlab-org/gitlab-pages/internal/testhelpers" +) + +func TestRateLimitMiddleware(t *testing.T) { + testhelpers.EnableRateLimiter(t) + + RunPagesProcess(t, + withListeners([]ListenSpec{httpListener}), + // 10 = 1 req every 100ms + withExtraArgument("rate-limit-source-ip", "1.0"), + withExtraArgument("rate-limit-source-ip-burst", "1"), + ) + + for i := 0; i < 20; i++ { + rsp1, err := GetPageFromListener(t, httpListener, "group.gitlab-example.com", "project/") + require.NoError(t, err) + rsp1.Body.Close() + + // every other request should fail + //if i%2 != 0 { + // require.Equal(t, http.StatusTooManyRequests, rsp1.StatusCode, "group.gitlab-example.com request: %d failed", i) + // // wait for another token to become available + // time.Sleep(100 * time.Millisecond) + // continue + //} + + require.Equal(t, http.StatusOK, rsp1.StatusCode, "group.gitlab-example.com request: %d failed", i) + time.Sleep(time.Millisecond) + } +} |