diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 0cdd5f64d..c68707cb5 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -13,9 +13,16 @@ class ApplicationController < ActionController::Base protected def api_check - if CurrentUser.is_anonymous? && request.format.to_s =~ /json|xml/ - render :text => "401 Not Authorized\n", :layout => false, :status => 401 - return false + if request.format.to_s =~ /json|xml/ + if CurrentUser.is_anonymous? + render :text => "401 Not Authorized\n", :layout => false, :status => 401 + return false + end + + if ApiLimiter.throttled?(request.remote_ip) + render :text => "421 User Throttled\n", :layout => false, :status => 421 + return false + end end return true diff --git a/app/logical/anonymous_user.rb b/app/logical/anonymous_user.rb index 409b25e63..1e8add5ad 100644 --- a/app/logical/anonymous_user.rb +++ b/app/logical/anonymous_user.rb @@ -164,6 +164,10 @@ class AnonymousUser def enable_sequential_post_navigation true end + + def api_hourly_limit + 500 + end %w(member banned privileged builder platinum contributor janitor moderator admin).each do |name| define_method("is_#{name}?") do diff --git a/app/logical/api_limiter.rb b/app/logical/api_limiter.rb new file mode 100644 index 000000000..ca2af02dd --- /dev/null +++ b/app/logical/api_limiter.rb @@ -0,0 +1,9 @@ +module ApiLimiter + def throttled?(ip_addr) + key = "#{ip_addr}:#{Time.now.hour}" + MEMCACHE.fetch(key, 1.hour, true) {0} + MEMCACHE.incr(key).to_i > CurrentUser.user.api_hourly_limit + end + + module_function :throttled? +end diff --git a/app/logical/cache.rb b/app/logical/cache.rb index 73016c56d..8f372dd3e 100644 --- a/app/logical/cache.rb +++ b/app/logical/cache.rb @@ -1,15 +1,11 @@ class Cache - def self.incr(key, expiry = 0) - val = Cache.get(key, expiry) - Cache.put(key, val.to_i + 1) + def self.incr(key) + MEMCACHE.incr(key) ActiveRecord::Base.logger.debug('MemCache Incr %s' % [key]) end - def self.decr(key, expiry = 0) - val = Cache.get(key, expiry) - if val.to_i > 0 - Cache.put(key, val.to_i - 1) - end + def self.decr(key) + MEMCACHE.decr(key) ActiveRecord::Base.logger.debug('MemCache Decr %s' % [key]) end diff --git a/app/models/user.rb b/app/models/user.rb index d4d1680c0..38b576654 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -469,6 +469,16 @@ class User < ActiveRecord::Base 20_000 end end + + def api_hourly_limit + if is_platinum? + 20_000 + elsif is_privileged? + 10_000 + else + 3_000 + end + end end module ApiMethods diff --git a/test/functional/posts_controller_test.rb b/test/functional/posts_controller_test.rb index b08ed3c72..69605690a 100644 --- a/test/functional/posts_controller_test.rb +++ b/test/functional/posts_controller_test.rb @@ -16,16 +16,32 @@ class PostsControllerTest < ActionController::TestCase end context "for api calls" do + context "passing the api limit" do + setup do + User.any_instance.stubs(:api_hourly_limit).returns(5) + end + + should "work" do + CurrentUser.user.api_hourly_limit.times do + get :index, {:format => "json", :login => @user.name, :api_key => @user.bcrypt_cookie_password_hash} + assert_response :success + end + + get :index, {:format => "json", :login => @user.name, :api_key => @user.bcrypt_cookie_password_hash} + assert_response 421 + end + end + context "using http basic auth" do should "succeed for password matches" do - @basic_auth_string = "Basic #{ActiveSupport::Base64.encode64("#{@user.name}:#{@user.bcrypt_cookie_password_hash}")}" + @basic_auth_string = "Basic #{::Base64.encode64("#{@user.name}:#{@user.bcrypt_cookie_password_hash}")}" @request.env['HTTP_AUTHORIZATION'] = @basic_auth_string get :index, {:format => "json"} assert_response :success end should "fail for password mismatches" do - @basic_auth_string = "Basic #{ActiveSupport::Base64.encode64("#{@user.name}:badpassword")}" + @basic_auth_string = "Basic #{::Base64.encode64("#{@user.name}:badpassword")}" @request.env['HTTP_AUTHORIZATION'] = @basic_auth_string get :index, {:format => "json"} assert_response 401 diff --git a/test/test_helper.rb b/test/test_helper.rb index d1481479d..503d56287 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -53,6 +53,23 @@ class MockMemcache def flush_all @memory = {} end + + def fetch key, expiry = 0, raw = false + if @memory.has_key?(key) + @memory[key] + else + @memory[key] = yield + end + @memory[key] + end + + def incr key + @memory[key] += 1 + end + + def decr key + @memory[key] -= 1 + end def set key, value, expiry = 0 @memory[key] = value @@ -62,7 +79,7 @@ class MockMemcache @memory[key] end - def delete key, delay + def delete key, delay = 0 @memory.delete key end