Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
Copyright (c) 2009 Luc Castera

Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:

The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
Expand Down
2 changes: 1 addition & 1 deletion VERSION.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
---
---
:major: 0
:minor: 2
:patch: 1
22 changes: 11 additions & 11 deletions lib/api_throttling.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,14 @@ def initialize(app, options={})
@handler = Handlers.cache_handler_for(@options[:cache])
raise "Sorry, we couldn't find a handler for the cache you specified: #{@options[:cache]}" unless @handler
end

def call(env, options={})
if @options[:auth]
auth = Rack::Auth::Basic::Request.new(env)
return auth_required unless auth.provided?
return bad_request unless auth.basic?
end

begin
cache = @handler.new(@options[:cache])
key = generate_key(env, auth)
Expand All @@ -29,30 +29,30 @@ def call(env, options={})
end
@app.call(env)
end

def generate_key(env, auth)
return @options[:key].call(env, auth) if @options[:key]
auth ? "#{auth.username}_#{Time.now.strftime("%Y-%m-%d-%H")}" : "#{Time.now.strftime("%Y-%m-%d-%H")}"
end

def bad_request
body_text = "Bad Request"
[ 400, { 'Content-Type' => 'text/plain', 'Content-Length' => body_text.size.to_s }, [body_text] ]
end

def auth_required
body_text = "Authorization Required"
[ 401, { 'Content-Type' => 'text/plain', 'Content-Length' => body_text.size.to_s }, [body_text] ]
end

def over_rate_limit
body_text = "Over Rate Limit"
retry_after_in_seconds = (60 - Time.now.min) * 60
[ 503,
{ 'Content-Type' => 'text/plain',
'Content-Length' => body_text.size.to_s,
'Retry-After' => retry_after_in_seconds.to_s
},
[ 503,
{ 'Content-Type' => 'text/plain',
'Content-Length' => body_text.size.to_s,
'Retry-After' => retry_after_in_seconds.to_s
},
[body_text]
]
end
Expand Down
10 changes: 5 additions & 5 deletions lib/handlers/active_support_cache_store_handler.rb
Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
module Handlers
class ActiveSupportCacheStoreHandler < Handler

def initialize(object=nil)
raise "Must provide an existing ActiveSupport::Cache::Store" unless object.is_a?(ActiveSupport::Cache::Store)
@cache = object
end
def increment(key)

def increment(key)
@cache.write(key, (get(key)||0).to_i+1)
end

def get(key)
@cache.read(key)
end

%w(MemCacheStore FileStore MemoryStore SynchronizedMemoryStore DRbStore CompressedMemCacheStore).each do |store|
Handlers.add_handler(self, "ActiveSupport::Cache::#{store}".downcase)
end
Expand Down
20 changes: 10 additions & 10 deletions lib/handlers/handlers.rb
Original file line number Diff line number Diff line change
@@ -1,38 +1,38 @@
module Handlers
HANDLERS = {}

def self.cache_handler_for(info)
HANDLERS[info.to_s.downcase] || HANDLERS[info.class.to_s.downcase]
end

def self.add_handler(handler, key=nil)
HANDLERS[key || handler.cache_class.downcase] = handler
end

# creating a new cache handler is as simple as extending from the handler class,
# setting the class to use as the cache by calling cache_class("Redis")
# and then implementing the increment and get methods for that cache type.
#
# If you don't want to extend from Handler you can just create a class that implements
# increment(key), get(key) and handles?(info)
#
#
# you can then initialize the middleware and pass :cache=>CACHE_NAME as an option.
class Handler
def initialize(object=nil)
cache = Object.const_get(self.class.cache_class)
@cache = object.is_a?(cache) ? object : cache.new
end

def increment(key)
raise "Cache Handlers must implement an increment method"
end

def get(key)
raise "Cache Handlers must implement a get method"
end
class << self

class << self

def cache_class(name = nil)
@cache_class = name if name
@cache_class
Expand All @@ -42,5 +42,5 @@ def cache_class(name = nil)

%w(redis_handler memcache_handler hash_handler active_support_cache_store_handler).each do |handler|
require File.expand_path(File.dirname(__FILE__) + "/#{handler}")
end
end
end
6 changes: 3 additions & 3 deletions lib/handlers/hash_handler.rb
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
module Handlers
class HashHandler < Handler
cache_class "Hash"

def increment(key)
@cache[key] = (get(key)||0).to_i+1
end

def get(key)
@cache[key]
end

Handlers.add_handler self
end
end
6 changes: 3 additions & 3 deletions lib/handlers/memcache_handler.rb
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
module Handlers
class MemCacheHandler < Handler
cache_class "MemCache"

def increment(key)
@cache.set(key, (get(key)||0).to_i+1)
end

def get(key)
@cache.get(key)
end

Handlers.add_handler self
end
end
6 changes: 3 additions & 3 deletions lib/handlers/redis_handler.rb
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
module Handlers
class RedisHandler < Handler
cache_class "Redis"

def increment(key)
@cache.incr(key)
end

def get(key)
@cache[key]
end

Handlers.add_handler self
end
end
50 changes: 25 additions & 25 deletions test/test_api_throttling.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

class ApiThrottlingTest < Test::Unit::TestCase
include Rack::Test::Methods

context "using redis" do
before do
# Delete all the keys for 'joe' in Redis so that every test starts fresh
Expand All @@ -17,28 +17,28 @@ class ApiThrottlingTest < Test::Unit::TestCase
r.keys("*").each do |key|
r.delete key
end

rescue Errno::ECONNREFUSED
assert false, "You need to start redis-server"
end
end

context "with authentication required" do
include BasicTests

def app
Rack::Builder.new {
use ApiThrottling, :requests_per_hour => 3
run lambda {|env| [200, {'Content-Type' => 'text/plain', 'Content-Length' => '12'}, ["Hello World!"] ] }
}
end

should "have Redis as cache handler" do
assert_equal "Handlers::RedisHandler", app.to_app.instance_variable_get(:@handler).to_s
end

end

context "without authentication required" do
def app
Rack::Builder.new {
Expand All @@ -56,16 +56,16 @@ def app
assert_equal 503, last_response.status
end
end

context "with rate limit key based on url" do
def app
Rack::Builder.new {
use ApiThrottling, :requests_per_hour => 3,
use ApiThrottling, :requests_per_hour => 3,
:key=>Proc.new{ |env,auth| "#{auth.username}_#{env['PATH_INFO']}_#{Time.now.strftime("%Y-%m-%d-%H")}" }
run lambda {|env| [200, {'Content-Type' => 'text/plain', 'Content-Length' => '12'}, ["Hello World!"] ] }
}
end

should "throttle requests based on the user and url called" do
authorize "joe", "secret"
3.times do
Expand All @@ -74,67 +74,67 @@ def app
end
get '/'
assert_equal 503, last_response.status

3.times do
get '/awesome'
assert_equal 200, last_response.status
end
get '/awesome'
assert_equal 503, last_response.status

authorize "luc", "secret"
get '/awesome'
assert_equal 200, last_response.status

get '/'
assert_equal 200, last_response.status
end
end
end

context "using active support memory store" do
require 'active_support'

include BasicTests

before do
@@cache_store = ActiveSupport::Cache.lookup_store(:memory_store)
end

def app
Rack::Builder.new {
use ApiThrottling, :requests_per_hour => 3, :cache => @@cache_store
run lambda {|env| [200, {'Content-Type' => 'text/plain', 'Content-Length' => '12'}, ["Hello World!"] ] }
}
end

should "have memcache as cache handler" do
assert_equal "Handlers::ActiveSupportCacheStoreHandler", app.to_app.instance_variable_get(:@handler).to_s
end
end


context "using active support memcache store" do
require 'active_support'

#include BasicTests
before do

before do
@@cache_store = ActiveSupport::Cache.lookup_store(:memCache_store)
@@cache_store.clear
end

def app
Rack::Builder.new {
use ApiThrottling, :requests_per_hour => 3, :cache => @@cache_store
run lambda {|env| [200, {'Content-Type' => 'text/plain', 'Content-Length' => '12'}, ["Hello World!"] ] }
}
end

should "have memcache as cache handler" do
assert_equal "Handlers::ActiveSupportCacheStoreHandler", app.to_app.instance_variable_get(:@handler).to_s
end
end


end
4 changes: 2 additions & 2 deletions test/test_api_throttling_hash.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,11 @@ def app
run lambda {|env| [200, {'Content-Type' => 'text/plain', 'Content-Length' => '12'}, ["Hello World!"] ] }
}
end

def setup
HASH.replace({})
end

def test_cache_handler_should_be_memcache
assert_equal "Handlers::HashHandler", app.to_app.instance_variable_get(:@handler).to_s
end
Expand Down
4 changes: 2 additions & 2 deletions test/test_api_throttling_memcache.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,11 @@ def app
run lambda {|env| [200, {'Content-Type' => 'text/plain', 'Content-Length' => '12'}, ["Hello World!"] ] }
}
end

def setup
CACHE.flush_all
end

def test_cache_handler_should_be_memcache
assert_equal "Handlers::MemCacheHandler", app.to_app.instance_variable_get(:@handler).to_s
end
Expand Down
Loading