implement api limiting
This commit is contained in:
@@ -13,9 +13,16 @@ class ApplicationController < ActionController::Base
|
|||||||
|
|
||||||
protected
|
protected
|
||||||
def api_check
|
def api_check
|
||||||
if CurrentUser.is_anonymous? && request.format.to_s =~ /json|xml/
|
if request.format.to_s =~ /json|xml/
|
||||||
render :text => "401 Not Authorized\n", :layout => false, :status => 401
|
if CurrentUser.is_anonymous?
|
||||||
return false
|
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
|
end
|
||||||
|
|
||||||
return true
|
return true
|
||||||
|
|||||||
@@ -164,6 +164,10 @@ class AnonymousUser
|
|||||||
def enable_sequential_post_navigation
|
def enable_sequential_post_navigation
|
||||||
true
|
true
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def api_hourly_limit
|
||||||
|
500
|
||||||
|
end
|
||||||
|
|
||||||
%w(member banned privileged builder platinum contributor janitor moderator admin).each do |name|
|
%w(member banned privileged builder platinum contributor janitor moderator admin).each do |name|
|
||||||
define_method("is_#{name}?") do
|
define_method("is_#{name}?") do
|
||||||
|
|||||||
9
app/logical/api_limiter.rb
Normal file
9
app/logical/api_limiter.rb
Normal file
@@ -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
|
||||||
@@ -1,15 +1,11 @@
|
|||||||
class Cache
|
class Cache
|
||||||
def self.incr(key, expiry = 0)
|
def self.incr(key)
|
||||||
val = Cache.get(key, expiry)
|
MEMCACHE.incr(key)
|
||||||
Cache.put(key, val.to_i + 1)
|
|
||||||
ActiveRecord::Base.logger.debug('MemCache Incr %s' % [key])
|
ActiveRecord::Base.logger.debug('MemCache Incr %s' % [key])
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.decr(key, expiry = 0)
|
def self.decr(key)
|
||||||
val = Cache.get(key, expiry)
|
MEMCACHE.decr(key)
|
||||||
if val.to_i > 0
|
|
||||||
Cache.put(key, val.to_i - 1)
|
|
||||||
end
|
|
||||||
ActiveRecord::Base.logger.debug('MemCache Decr %s' % [key])
|
ActiveRecord::Base.logger.debug('MemCache Decr %s' % [key])
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -469,6 +469,16 @@ class User < ActiveRecord::Base
|
|||||||
20_000
|
20_000
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def api_hourly_limit
|
||||||
|
if is_platinum?
|
||||||
|
20_000
|
||||||
|
elsif is_privileged?
|
||||||
|
10_000
|
||||||
|
else
|
||||||
|
3_000
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
module ApiMethods
|
module ApiMethods
|
||||||
|
|||||||
@@ -16,16 +16,32 @@ class PostsControllerTest < ActionController::TestCase
|
|||||||
end
|
end
|
||||||
|
|
||||||
context "for api calls" do
|
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
|
context "using http basic auth" do
|
||||||
should "succeed for password matches" 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
|
@request.env['HTTP_AUTHORIZATION'] = @basic_auth_string
|
||||||
get :index, {:format => "json"}
|
get :index, {:format => "json"}
|
||||||
assert_response :success
|
assert_response :success
|
||||||
end
|
end
|
||||||
|
|
||||||
should "fail for password mismatches" do
|
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
|
@request.env['HTTP_AUTHORIZATION'] = @basic_auth_string
|
||||||
get :index, {:format => "json"}
|
get :index, {:format => "json"}
|
||||||
assert_response 401
|
assert_response 401
|
||||||
|
|||||||
@@ -53,6 +53,23 @@ class MockMemcache
|
|||||||
def flush_all
|
def flush_all
|
||||||
@memory = {}
|
@memory = {}
|
||||||
end
|
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
|
def set key, value, expiry = 0
|
||||||
@memory[key] = value
|
@memory[key] = value
|
||||||
@@ -62,7 +79,7 @@ class MockMemcache
|
|||||||
@memory[key]
|
@memory[key]
|
||||||
end
|
end
|
||||||
|
|
||||||
def delete key, delay
|
def delete key, delay = 0
|
||||||
@memory.delete key
|
@memory.delete key
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user