diff --git a/lib/volumen.rb b/lib/volumen.rb index 51701a6..d15d49e 100644 --- a/lib/volumen.rb +++ b/lib/volumen.rb @@ -18,11 +18,24 @@ require_relative "volumen/users" # The Sinatra app (`Volumen::Server`) and the CLI (`Volumen::CLI`) are loaded # on demand to keep the core library light. module Volumen + CLEANUP_INTERVAL = 300 # seconds between full sweeps of login_attempts + def self.login_attempts @login_attempts ||= {} end def self.reset_login_attempts! @login_attempts = {} + @last_cleanup = nil + end + + def self.sweep_login_attempts! + now = Time.now.to_f + return if @last_cleanup && (now - @last_cleanup) < CLEANUP_INTERVAL + + cutoff = now - Volumen::Server::LOGIN_WINDOW + login_attempts.each_value { |timestamps| timestamps.select! { |t| t > cutoff } } + login_attempts.delete_if { |_ip, timestamps| timestamps.empty? } + @last_cleanup = now end end diff --git a/lib/volumen/server.rb b/lib/volumen/server.rb index 4697a0a..04a3fd9 100644 --- a/lib/volumen/server.rb +++ b/lib/volumen/server.rb @@ -224,6 +224,7 @@ module Volumen end def check_login_rate_limit! + Volumen.sweep_login_attempts! ip = request.ip.to_s now = Time.now.to_f cutoff = now - LOGIN_WINDOW diff --git a/test/admin_test.rb b/test/admin_test.rb index 1026797..bc72f93 100644 --- a/test/admin_test.rb +++ b/test/admin_test.rb @@ -221,6 +221,14 @@ class AdminTest < Minitest::Test assert File.exist?(File.join(@posts_dir, "by-author.md")) end + def test_login_rate_limit_sweep_removes_stale_entries + Volumen.login_attempts["1.2.3.4"] = [Time.now.to_f - 120] + Volumen.instance_variable_set(:@last_cleanup, Time.now.to_f - 600) + Volumen.sweep_login_attempts! + refute Volumen.login_attempts.key?("1.2.3.4"), + "sweep should remove IPs with only stale entries" + end + private def csrf(body)