Skip to content

Commit

Permalink
Add support for rate limiting signup requests
Browse files Browse the repository at this point in the history
  • Loading branch information
tomhughes committed Aug 22, 2023
1 parent 7054cea commit 63bf18a
Show file tree
Hide file tree
Showing 4 changed files with 82 additions and 1 deletion.
25 changes: 24 additions & 1 deletion app/controllers/users_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,9 @@ def save
end

if current_user.save
SIGNUP_IP_LIMITER&.update(request.remote_ip)
SIGNUP_EMAIL_LIMITER&.update(canonical_email(current_user.email))

flash[:matomo_goal] = Settings.matomo["goals"]["signup"] if defined?(Settings.matomo)

referer = welcome_path
Expand Down Expand Up @@ -344,7 +347,13 @@ def check_signup_allowed(email = nil)
domain_mx_servers(domain)
end

if blocked = Acl.no_account_creation(request.remote_ip, :domain => domain, :mx => mx_servers)
blocked = Acl.no_account_creation(request.remote_ip, :domain => domain, :mx => mx_servers)

blocked ||= SIGNUP_IP_LIMITER && !SIGNUP_IP_LIMITER.allow?(request.remote_ip)

blocked ||= email && SIGNUP_EMAIL_LIMITER && !SIGNUP_EMAIL_LIMITER.allow?(canonical_email(email))

if blocked
logger.info "Blocked signup from #{request.remote_ip} for #{email}"

render :action => "blocked"
Expand All @@ -353,6 +362,20 @@ def check_signup_allowed(email = nil)
!blocked
end

def canonical_email(email)
local_part, domain = if email.nil?
nil
else
email.split("@")
end

local_part.sub!(/\+.*$/, "")

local_part.delete!(".") if %w[gmail.com googlemail.com].include?(domain)

"#{local_part}@#{domain}"
end

##
# get list of MX servers for a domains
def domain_mx_servers(domain)
Expand Down
15 changes: 15 additions & 0 deletions config/initializers/rate_limits.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
require "rate_limiter"

SIGNUP_IP_LIMITER = if Settings.memcache_servers && Settings.signup_ip_per_day && Settings.signup_ip_max_burst
RateLimiter.new(
Dalli::Client.new(Settings.memcache_servers, :namespace => "rails:signup:ip"),
86400, Settings.signup_ip_per_day, Settings.signup_ip_max_burst
)
end

SIGNUP_EMAIL_LIMITER = if Settings.memcache_servers && Settings.signup_email_per_day && Settings.signup_email_max_burst
RateLimiter.new(
Dalli::Client.new(Settings.memcache_servers, :namespace => "rails:signup:email"),
86400, Settings.signup_email_per_day, Settings.signup_email_max_burst
)
end
5 changes: 5 additions & 0 deletions config/settings.yml
Original file line number Diff line number Diff line change
Expand Up @@ -140,3 +140,8 @@ smtp_user_name: null
smtp_password: null
# Matomo settings for analytics
#matomo:
# Signup rate limits
#signup_ip_per_day:
#signup_ip_max_burst:
#signup_email_per_day:
#signup_email_max_burst:
38 changes: 38 additions & 0 deletions lib/rate_limiter.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
class RateLimiter
def initialize(cache, interval, limit, max_burst)
@cache = cache
@requests_per_second = limit.to_f / interval
@burst_limit = max_burst
end

def allow?(key)
last_update, requests = @cache.get(key)

if last_update
elapsed = Time.now.to_i - last_update

requests -= elapsed * @requests_per_second
else
requests = 0.0
end

requests < @burst_limit
end

def update(key)
now = Time.now.to_i

last_update, requests = @cache.get(key)

if last_update
elapsed = now - last_update

requests -= elapsed * @requests_per_second
requests += 1.0
else
requests = 1.0
end

@cache.set(key, [now, [requests, 1.0].max])
end
end

0 comments on commit 63bf18a

Please sign in to comment.