CurbIt makes it easy to add application level rate limiting to your Rails app by using a controller macro.
CurbIt is NOT a replacement for properly configured rate limiting at fronting services. Properly defending your app against DoS attacks or other malicious client behavior should always include configuring rate limiting and throttling at the services that sit in front of your app. This includes firewalls, load balancers, and reverse proxies. But sometimes that just isn’t enough. Sometimes you want to rate limit requests from users based on application logic that is not practical to get to or replicate in those services.
Quick setup inside your Rails controller. ActionController::Base is already extended to include Curbit when installed as a plugin. This will add a rate_limit “macro” to your controllers.
class InvitesController < ApplicationController def invite # invite logic... end rate_limit :invite, :max_calls => 2, :time_limit => 30.seconds, :wait_time => 1.minute end
If a user calls the invite service from the same remote address more than 2 times within 30 seconds, CurbIt will render a ‘503 Service Unavailable’ response and the invite method is never called. The user will then need to wait 1 minute before being allowed to make the request again. Default response messages for html, xml, and json formats are rendered as required.
If you don’t want to use the remote address to identify the client, you can specify a method that CurbIt will call to get a key from.
class InvitesController < ApplicationController def invite # invite logic... end rate_limit :invite, :key => :userid, :max_calls => 2, :time_limit => 30.seconds, :wait_time => 1.minute def userid session[:user_id] end end
CurbIt will call the :userid method and use the returned value to create a unique identifier. This identifier is used to index cached information about the request.
You can alternatively pass a Proc that will take the controller instance as an argument.
rate_limit :invite, :key => proc {|c| c.session[:user_id]}, :max_calls => 2, :time_limit => 30.seconds, :wait_time => 1.minute
(If you’re wondering why CurbIt passes the controller into the proc, it’s because the Proc is not bound to the controller instance when it’s defined. This way, you can at least have access to stuff you might need.)
You may want to apply rate limiting only in certain conditions, for example, if a user is authenticated. Curbit provides the :if and :unless parameters that both accept a symbol or Proc. If a symbol is passed then Curbit attempts to execute a method using the symbol as the method name. In the case of the :if parameter, the rate limit is applied if the method returns true. For the :unless paramter, the rate limit is not applied if the method returns true.
rate_limit :invite, :max_calls => 2, :time_limit => 30.seconds, :wait_time => 1.minute, :unless => :logged_in?
In this case, the rate limiting is being used with the acts_as_authenticated method logged_in? so that the rate limiting is only applied if the user is not logged in.
You might like to customize the messages returned by CurbIt.
class InvitesController < ApplicationController def invite # invite logic... end rate_limit :invite, :max_calls => 2, :time_limit => 30.seconds, :wait_time => 1.minute, :message => "Hey! Slow down there cow polk.", :status => 200 end
After reaching the maximum threshold of requests, CurbIt will render the message “Hey! Slow down there cow polk.” with a response status of 200. If :status is not defined, CurbIt will set a 503 status on the response. CurbIt will also embed this message into some default json or xml containers based on the request format.
CurbIt does it’s best to render a response based on the requested format, but you might have an obscure mime-type you’re using or you might like to customize the response rendering.
class InvitesController < ApplicationController def invite # invite logic... end rate_limit :invite, :max_calls => 2, :time_limit => 30.seconds, :wait_time => 1.minute, :message => :limit_response def limit_response(wait_time) respond_to {|fmt| fmt.csv { render :text => "Plese wait #{wait_time} seconds before trying again", :status => 200 } } end end
Here, CurbIt will relinquish all control for response rendering to your method. This will ignore any :status argument set in the cofig options.
-
xml: <error>message</error>
-
json: {“error”:{“message”}}
-
html: message
-
text: message
$ gem install curbit --source http://gemcutter.org
Specify the gem dependency in your config/environment.rb file:
Rails::Initializer.run do |config| #... config.gem "curbit", :source => "http://gemcutter.org #... end
Then:
$ rake gems:install $ rake gems:unpack
Then include in your controllers:
class MyController < ApplicationController include Curbit::Controller #... end
$ script/plugin install git://github.com/ssayles/curbit.git
Curbit::Controller will automatically be added to your controllers so you can call the rate_limit macro whenever you want.
CurbIt utilizes Rails.cache to store information about requests. Be mindful that if you are running a cluster, you’ll want to make sure you’re using a shared cache like memcached.
That’s it!
CurbIt is written and maintained by Scott Sayles.
Copyright © 2009 Scott Sayles. See LICENSE for details.