Skip to content

Commit

Permalink
Add API docs, limit request per token
Browse files Browse the repository at this point in the history
  • Loading branch information
ntamvl committed Oct 20, 2016
1 parent bfad6d7 commit d698bcf
Show file tree
Hide file tree
Showing 8 changed files with 174 additions and 4 deletions.
2 changes: 2 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ gem 'awesome_print', '1.7.0'

gem 'swagger-docs', '0.2.9'

gem "redis", '3.3.0'

group :development, :test do
# Call 'byebug' anywhere in the code to stop execution and get a debugger console
gem 'byebug', platform: :mri
Expand Down
2 changes: 2 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ GEM
rb-fsevent (0.9.7)
rb-inotify (0.9.7)
ffi (>= 0.5.0)
redis (3.3.0)
rspec-core (3.5.4)
rspec-support (~> 3.5.0)
rspec-expectations (3.5.0)
Expand Down Expand Up @@ -169,6 +170,7 @@ DEPENDENCIES
rack-attack (= 5.0.1)
rack-cors (= 0.4.0)
rails (~> 5.0.0, >= 5.0.0.1)
redis (= 3.3.0)
rspec-rails (= 3.5.2)
spring
spring-watcher-listen (~> 2.0.0)
Expand Down
105 changes: 105 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -625,6 +625,104 @@ module Api::V1
end
```

## Rate Limiting per token
#### Create file `config/initializers/throttle.rb`
```ruby
# config/initializers/throttle.rb

require "redis"

redis_conf = YAML.load(File.join(Rails.root, "config", "redis.yml"))
REDIS = Redis.new(:host => redis_conf["host"], :port => redis_conf["port"])

# We will allow a client a maximum of 60 requests in 15 minutes. The following constants need to be defined in throttle.rb
THROTTLE_TIME_WINDOW = 15 * 60
THROTTLE_MAX_REQUESTS = 60
```

The filter needs to be changed to respond with error messages when the rate limit is exceeded.
```ruby
class ApplicationController < ActionController::API
include ActionController::Serialization
include ActionController::HttpAuthentication::Token::ControllerMethods

before_action :authenticate
before_filter :throttle_token

protected

def authenticate
authenticate_token || render_unauthorized
end

def authenticate_token
authenticate_with_http_token do |token, options|
@current_user = User.find_by(api_key: token)
@token = token
end
end

def render_unauthorized(realm = "Application")
self.headers["WWW-Authenticate"] = %(Token realm="#{realm.gsub(/"/, "")}")
render json: {message: 'Bad credentials'}, status: :unauthorized
end

def throttle_ip
client_ip = request.env["REMOTE_ADDR"]
key = "count:#{client_ip}"
count = REDIS.get(key)

unless count
REDIS.set(key, 0)
REDIS.expire(key, THROTTLE_TIME_WINDOW)
return true
end

if count.to_i >= THROTTLE_MAX_REQUESTS
render :json => {:message => "You have fired too many requests. Please wait for some time."}, :status => 429
return
end
REDIS.incr(key)
true
end

def throttle_token
if @token.present?
key = "count:#{@token}"
count = REDIS.get(key)

unless count
REDIS.set(key, 0)
REDIS.expire(key, THROTTLE_TIME_WINDOW)
return true
end

if count.to_i >= THROTTLE_MAX_REQUESTS
render :json => {:message => "You have fired too many requests. Please wait for some time."}, :status => 429
return
end
REDIS.incr(key)
true
else
false
end
end
end
```

Let’s go ahead and test this `test_throttle.sh`.
```
for i in {1..300}
do
printf "\n------------------\n"
echo "Welcome $i times"
printf "\n"
# curl -i -H "Authorization: Token token=3Hu9orST5sKDHUPJBwjbogtt" http://localhost:3000/v1/users >> /dev/null
# curl -i -H "Authorization: Token token=3Hu9orST5sKDHUPJBwjbogtt" http://10.1.0.201:3000/v1/users
curl -i -H "Authorization: Token token=3Hu9orST5sKDHUPJBwjbogtt" http://localhost:3000/v1/users
done
```

## How to run
*Clone source from github: `[email protected]:ntamvl/rails_5_api_tutorial.git`*
```
Expand Down Expand Up @@ -674,3 +772,10 @@ Now you have the keys to the castle, and all the basics for building an API the
Hopefully then guide was helpful for you, and if you want any points clarified or just want to say thanks then feel free to use the comments below.

Cheers, and happy coding!

---------------------------------------------
Redis documentation for INCR command. [return]
redis - A Ruby client that tries to match Redis’ API one-to-one, while still providing an idiomatic interface. It features thread-safety, client-side sharding, pipelining, and an obsession for performance. [return]
Rails’ before filter. [return]
IETF: Additional HTTP Status Codes - 429 Too Many Requests. [return]
*If you have questions or comments about this blog post, you can get in touch with me on Twitter @nguyentamvn*
45 changes: 44 additions & 1 deletion app/controllers/application_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ class ApplicationController < ActionController::API
include ActionController::HttpAuthentication::Token::ControllerMethods

before_action :authenticate
before_filter :throttle_token

protected

Expand All @@ -13,12 +14,54 @@ def authenticate
def authenticate_token
authenticate_with_http_token do |token, options|
@current_user = User.find_by(api_key: token)
@token = token
end
end

def render_unauthorized(realm = "Application")
self.headers["WWW-Authenticate"] = %(Token realm="#{realm.gsub(/"/, "")}")
render json: 'Bad credentials', status: :unauthorized
render json: {message: 'Bad credentials'}, status: :unauthorized
end

def throttle_ip
client_ip = request.env["REMOTE_ADDR"]
key = "count:#{client_ip}"
count = REDIS.get(key)

unless count
REDIS.set(key, 0)
REDIS.expire(key, THROTTLE_TIME_WINDOW)
return true
end

if count.to_i >= THROTTLE_MAX_REQUESTS
render :json => {:message => "You have fired too many requests. Please wait for some time."}, :status => 429
return
end
REDIS.incr(key)
true
end

def throttle_token
if @token.present?
key = "count:#{@token}"
count = REDIS.get(key)

unless count
REDIS.set(key, 0)
REDIS.expire(key, THROTTLE_TIME_WINDOW)
return true
end

if count.to_i >= THROTTLE_MAX_REQUESTS
render :json => {:message => "You have fired too many requests. Please wait for some time."}, :status => 429
return
end
REDIS.incr(key)
true
else
false
end
end

end
6 changes: 3 additions & 3 deletions config/initializers/rack_attack.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ class Rack::Attack
'127.0.0.1' == req.ip || '::1' == req.ip
end

# Allow an IP address to make 5 requests every 5 seconds
throttle('req/ip', limit: 5, period: 5) do |req|
# Allow an IP address to make 100 requests every 5 seconds
throttle('req/ip', limit: 100, period: 5) do |req|
req.ip
end

Expand All @@ -20,7 +20,7 @@ class Rack::Attack
[
429,
{'Content-Type' => 'application/json', 'Retry-After' => retry_after.to_s},
[{error: "Throttle limit reached. Retry later."}.to_json]
[{error: "Throttle limit reached. Retry later.", status: 429}.to_json]
]
}
end
7 changes: 7 additions & 0 deletions config/initializers/throttle.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
require "redis"

redis_conf = YAML.load(File.join(Rails.root, "config", "redis.yml"))
REDIS = Redis.new(:host => redis_conf["host"], :port => redis_conf["port"])

THROTTLE_TIME_WINDOW = 15 * 60
THROTTLE_MAX_REQUESTS = 60
2 changes: 2 additions & 0 deletions config/redis.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
host: localhost
port: 6379
9 changes: 9 additions & 0 deletions test_throttle.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
for i in {1..300}
do
printf "\n------------------\n"
echo "Welcome $i times"
printf "\n"
# curl -i -H "Authorization: Token token=3Hu9orST5sKDHUPJBwjbogtt" http://localhost:3000/v1/users >> /dev/null
# curl -i -H "Authorization: Token token=3Hu9orST5sKDHUPJBwjbogtt" http://10.1.0.201:3000/v1/users
curl -i -H "Authorization: Token token=3Hu9orST5sKDHUPJBwjbogtt" http://localhost:3000/v1/users
done

0 comments on commit d698bcf

Please sign in to comment.