mirror of
https://github.com/discourse/discourse.git
synced 2025-03-06 11:19:51 +00:00
FEATURE: optional default off global per ip rate limiter
This commit is contained in:
parent
3c230d8f97
commit
4986ebcf24
@ -175,3 +175,8 @@ max_user_api_reqs_per_minute = 20
|
|||||||
max_user_api_reqs_per_day = 2880
|
max_user_api_reqs_per_day = 2880
|
||||||
|
|
||||||
max_admin_api_reqs_per_key_per_minute = 60
|
max_admin_api_reqs_per_key_per_minute = 60
|
||||||
|
|
||||||
|
max_requests_per_ip_per_minute = 200
|
||||||
|
max_requests_per_ip_per_10_seconds = 50
|
||||||
|
# global rate limiter will simply warn if the limit is exceeded, can be warn, block or none
|
||||||
|
max_requests_per_ip_mode = none
|
||||||
|
@ -126,6 +126,13 @@ class Middleware::RequestTracker
|
|||||||
end
|
end
|
||||||
|
|
||||||
def call(env)
|
def call(env)
|
||||||
|
result = nil
|
||||||
|
|
||||||
|
if rate_limit(env)
|
||||||
|
result = [429, {}, ["Slow down, too Many Requests from this IP Address"]]
|
||||||
|
return result
|
||||||
|
end
|
||||||
|
|
||||||
env["discourse.request_tracker"] = self
|
env["discourse.request_tracker"] = self
|
||||||
MethodProfiler.start if @@detailed_request_loggers
|
MethodProfiler.start if @@detailed_request_loggers
|
||||||
result = @app.call(env)
|
result = @app.call(env)
|
||||||
@ -135,6 +142,46 @@ class Middleware::RequestTracker
|
|||||||
log_request_info(env, result, info) unless env["discourse.request_tracker.skip"]
|
log_request_info(env, result, info) unless env["discourse.request_tracker.skip"]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def rate_limit(env)
|
||||||
|
if (
|
||||||
|
GlobalSetting.max_requests_per_ip_mode == "block" ||
|
||||||
|
GlobalSetting.max_requests_per_ip_mode == "warn"
|
||||||
|
)
|
||||||
|
|
||||||
|
ip = Rack::Request.new(env).ip
|
||||||
|
|
||||||
|
limiter10 = RateLimiter.new(
|
||||||
|
nil,
|
||||||
|
"global_ip_limit_10_#{ip}",
|
||||||
|
GlobalSetting.max_requests_per_ip_per_10_seconds,
|
||||||
|
10,
|
||||||
|
global: true
|
||||||
|
)
|
||||||
|
|
||||||
|
limiter60 = RateLimiter.new(
|
||||||
|
nil,
|
||||||
|
"global_ip_limit_60_#{ip}",
|
||||||
|
GlobalSetting.max_requests_per_ip_per_10_seconds,
|
||||||
|
10,
|
||||||
|
global: true
|
||||||
|
)
|
||||||
|
|
||||||
|
type = 10
|
||||||
|
begin
|
||||||
|
limiter10.performed!
|
||||||
|
type = 60
|
||||||
|
limiter60.performed!
|
||||||
|
rescue RateLimiter::LimitExceeded
|
||||||
|
if GlobalSetting.max_requests_per_ip_mode == "warn"
|
||||||
|
Rails.logger.warn("Global IP rate limit exceeded for #{ip} type: #{type}")
|
||||||
|
false
|
||||||
|
else
|
||||||
|
true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def log_later(data, host)
|
def log_later(data, host)
|
||||||
Scheduler::Defer.later("Track view", _db = nil) do
|
Scheduler::Defer.later("Track view", _db = nil) do
|
||||||
self.class.log_request_on_site(data, host)
|
self.class.log_request_on_site(data, host)
|
||||||
|
@ -28,6 +28,12 @@ class RateLimiter
|
|||||||
$redis.delete_prefixed(RateLimiter.key_prefix)
|
$redis.delete_prefixed(RateLimiter.key_prefix)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def self.clear_all_global!
|
||||||
|
$redis.without_namespace.keys("GLOBAL::#{key_prefix}*").each do |k|
|
||||||
|
$redis.without_namespace.del k
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def build_key(type)
|
def build_key(type)
|
||||||
"#{RateLimiter.key_prefix}:#{@user && @user.id}:#{type}"
|
"#{RateLimiter.key_prefix}:#{@user && @user.id}:#{type}"
|
||||||
end
|
end
|
||||||
@ -38,7 +44,7 @@ class RateLimiter
|
|||||||
@key = build_key(type)
|
@key = build_key(type)
|
||||||
@max = max
|
@max = max
|
||||||
@secs = secs
|
@secs = secs
|
||||||
@global = false
|
@global = global
|
||||||
end
|
end
|
||||||
|
|
||||||
def clear!
|
def clear!
|
||||||
|
@ -68,6 +68,79 @@ describe Middleware::RequestTracker do
|
|||||||
|
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context "rate limiting" do
|
||||||
|
|
||||||
|
class TestLogger
|
||||||
|
attr_accessor :warnings
|
||||||
|
|
||||||
|
def initialize
|
||||||
|
@warnings = 0
|
||||||
|
end
|
||||||
|
|
||||||
|
def warn(*args)
|
||||||
|
@warnings += 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
before do
|
||||||
|
RateLimiter.enable
|
||||||
|
RateLimiter.clear_all_global!
|
||||||
|
|
||||||
|
@old_logger = Rails.logger
|
||||||
|
Rails.logger = TestLogger.new
|
||||||
|
end
|
||||||
|
|
||||||
|
after do
|
||||||
|
RateLimiter.disable
|
||||||
|
Rails.logger = @old_logger
|
||||||
|
end
|
||||||
|
|
||||||
|
let :middleware do
|
||||||
|
app = lambda do |env|
|
||||||
|
[200, {}, ["OK"]]
|
||||||
|
end
|
||||||
|
|
||||||
|
Middleware::RequestTracker.new(app)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "does nothing by default" do
|
||||||
|
global_setting :max_requests_per_ip_per_10_seconds, 1
|
||||||
|
|
||||||
|
status, _ = middleware.call(env)
|
||||||
|
status, _ = middleware.call(env)
|
||||||
|
|
||||||
|
expect(status).to eq(200)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "does warn if rate limiter is enabled" do
|
||||||
|
global_setting :max_requests_per_ip_per_10_seconds, 1
|
||||||
|
global_setting :max_requests_per_ip_mode, 'warn'
|
||||||
|
|
||||||
|
status, _ = middleware.call(env)
|
||||||
|
status, _ = middleware.call(env)
|
||||||
|
|
||||||
|
expect(Rails.logger.warnings).to eq(1)
|
||||||
|
expect(status).to eq(200)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "does block if rate limiter is enabled" do
|
||||||
|
global_setting :max_requests_per_ip_per_10_seconds, 1
|
||||||
|
global_setting :max_requests_per_ip_mode, 'block'
|
||||||
|
|
||||||
|
env1 = env("REMOTE_ADDR" => "1.1.1.1")
|
||||||
|
env2 = env("REMOTE_ADDR" => "1.1.1.2")
|
||||||
|
|
||||||
|
status, _ = middleware.call(env1)
|
||||||
|
status, _ = middleware.call(env1)
|
||||||
|
|
||||||
|
expect(status).to eq(429)
|
||||||
|
|
||||||
|
status, _ = middleware.call(env2)
|
||||||
|
expect(status).to eq(200)
|
||||||
|
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
context "callbacks" do
|
context "callbacks" do
|
||||||
def app(result, sql_calls: 0, redis_calls: 0)
|
def app(result, sql_calls: 0, redis_calls: 0)
|
||||||
lambda do |env|
|
lambda do |env|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user