diff --git a/LICENSE b/LICENSE index 25e9875..7023e1b 100644 --- a/LICENSE +++ b/LICENSE @@ -1,5 +1,5 @@ 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 @@ -7,10 +7,10 @@ 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 diff --git a/VERSION.yml b/VERSION.yml index a324906..acaec69 100644 --- a/VERSION.yml +++ b/VERSION.yml @@ -1,4 +1,4 @@ ---- +--- :major: 0 :minor: 2 :patch: 1 diff --git a/lib/api_throttling.rb b/lib/api_throttling.rb index 9166ac6..975c01d 100644 --- a/lib/api_throttling.rb +++ b/lib/api_throttling.rb @@ -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) @@ -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 diff --git a/lib/handlers/active_support_cache_store_handler.rb b/lib/handlers/active_support_cache_store_handler.rb index 487c9cd..d28919a 100644 --- a/lib/handlers/active_support_cache_store_handler.rb +++ b/lib/handlers/active_support_cache_store_handler.rb @@ -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 diff --git a/lib/handlers/handlers.rb b/lib/handlers/handlers.rb index 04ea3f3..a8b863a 100644 --- a/lib/handlers/handlers.rb +++ b/lib/handlers/handlers.rb @@ -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 @@ -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 \ No newline at end of file diff --git a/lib/handlers/hash_handler.rb b/lib/handlers/hash_handler.rb index 00129d3..eb3d2ac 100644 --- a/lib/handlers/hash_handler.rb +++ b/lib/handlers/hash_handler.rb @@ -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 \ No newline at end of file diff --git a/lib/handlers/memcache_handler.rb b/lib/handlers/memcache_handler.rb index c0b8f87..45ace47 100644 --- a/lib/handlers/memcache_handler.rb +++ b/lib/handlers/memcache_handler.rb @@ -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 \ No newline at end of file diff --git a/lib/handlers/redis_handler.rb b/lib/handlers/redis_handler.rb index 316141e..e8d9816 100644 --- a/lib/handlers/redis_handler.rb +++ b/lib/handlers/redis_handler.rb @@ -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 diff --git a/test/test_api_throttling.rb b/test/test_api_throttling.rb index 46b2f3b..548f6af 100644 --- a/test/test_api_throttling.rb +++ b/test/test_api_throttling.rb @@ -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 @@ -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 { @@ -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 @@ -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 diff --git a/test/test_api_throttling_hash.rb b/test/test_api_throttling_hash.rb index 7dc56b5..9b7cd9b 100644 --- a/test/test_api_throttling_hash.rb +++ b/test/test_api_throttling_hash.rb @@ -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 diff --git a/test/test_api_throttling_memcache.rb b/test/test_api_throttling_memcache.rb index 1ffebdb..fed26b6 100644 --- a/test/test_api_throttling_memcache.rb +++ b/test/test_api_throttling_memcache.rb @@ -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 diff --git a/test/test_handlers.rb b/test/test_handlers.rb index 1a66ef0..ec66cb1 100644 --- a/test/test_handlers.rb +++ b/test/test_handlers.rb @@ -13,11 +13,11 @@ class HandlersTest < Test::Unit::TestCase assert_equal Handlers::MemCacheHandler, Handlers.cache_handler_for(key) end end - + should "select hash handler" do [:hash, 'hash', 'Hash', {}].each do |key| assert_equal Handlers::HashHandler, Handlers.cache_handler_for(key) end end - + end diff --git a/test/test_helper.rb b/test/test_helper.rb index d22366e..edc124a 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -10,9 +10,9 @@ def test_first_request_should_return_hello_world authorize "joe", "secret" get '/' assert_equal 200, last_response.status - assert_equal "Hello World!", last_response.body + assert_equal "Hello World!", last_response.body end - + def test_fourth_request_should_be_blocked authorize "joe", "secret" 3.times do @@ -22,30 +22,30 @@ def test_fourth_request_should_be_blocked get '/' assert_equal 503, last_response.status end - + def test_over_rate_limit_should_only_apply_to_user_that_went_over_the_limit - authorize "joe", "secret" + authorize "joe", "secret" 5.times { get '/' } assert_equal 503, last_response.status authorize "luc", "secret" get '/' assert_equal 200, last_response.status end - + def test_over_rate_limit_should_return_a_retry_after_header authorize "joe", "secret" 4.times { get '/' } assert_equal 503, last_response.status assert_not_nil last_response.headers['Retry-After'] end - + def test_retry_after_should_be_less_than_60_minutes authorize "joe", "secret" 4.times { get '/' } assert_equal 503, last_response.status assert last_response.headers['Retry-After'].to_i <= (60 * 60) end - + def test_should_require_authorization get '/' assert_equal 401, last_response.status