Skip to content

Commit d698bcf

Browse files
committed
Add API docs, limit request per token
1 parent bfad6d7 commit d698bcf

File tree

8 files changed

+174
-4
lines changed

8 files changed

+174
-4
lines changed

Gemfile

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ gem 'awesome_print', '1.7.0'
2828

2929
gem 'swagger-docs', '0.2.9'
3030

31+
gem "redis", '3.3.0'
32+
3133
group :development, :test do
3234
# Call 'byebug' anywhere in the code to stop execution and get a debugger console
3335
gem 'byebug', platform: :mri

Gemfile.lock

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,7 @@ GEM
115115
rb-fsevent (0.9.7)
116116
rb-inotify (0.9.7)
117117
ffi (>= 0.5.0)
118+
redis (3.3.0)
118119
rspec-core (3.5.4)
119120
rspec-support (~> 3.5.0)
120121
rspec-expectations (3.5.0)
@@ -169,6 +170,7 @@ DEPENDENCIES
169170
rack-attack (= 5.0.1)
170171
rack-cors (= 0.4.0)
171172
rails (~> 5.0.0, >= 5.0.0.1)
173+
redis (= 3.3.0)
172174
rspec-rails (= 3.5.2)
173175
spring
174176
spring-watcher-listen (~> 2.0.0)

README.md

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -625,6 +625,104 @@ module Api::V1
625625
end
626626
```
627627

628+
## Rate Limiting per token
629+
#### Create file `config/initializers/throttle.rb`
630+
```ruby
631+
# config/initializers/throttle.rb
632+
633+
require "redis"
634+
635+
redis_conf = YAML.load(File.join(Rails.root, "config", "redis.yml"))
636+
REDIS = Redis.new(:host => redis_conf["host"], :port => redis_conf["port"])
637+
638+
# We will allow a client a maximum of 60 requests in 15 minutes. The following constants need to be defined in throttle.rb
639+
THROTTLE_TIME_WINDOW = 15 * 60
640+
THROTTLE_MAX_REQUESTS = 60
641+
```
642+
643+
The filter needs to be changed to respond with error messages when the rate limit is exceeded.
644+
```ruby
645+
class ApplicationController < ActionController::API
646+
include ActionController::Serialization
647+
include ActionController::HttpAuthentication::Token::ControllerMethods
648+
649+
before_action :authenticate
650+
before_filter :throttle_token
651+
652+
protected
653+
654+
def authenticate
655+
authenticate_token || render_unauthorized
656+
end
657+
658+
def authenticate_token
659+
authenticate_with_http_token do |token, options|
660+
@current_user = User.find_by(api_key: token)
661+
@token = token
662+
end
663+
end
664+
665+
def render_unauthorized(realm = "Application")
666+
self.headers["WWW-Authenticate"] = %(Token realm="#{realm.gsub(/"/, "")}")
667+
render json: {message: 'Bad credentials'}, status: :unauthorized
668+
end
669+
670+
def throttle_ip
671+
client_ip = request.env["REMOTE_ADDR"]
672+
key = "count:#{client_ip}"
673+
count = REDIS.get(key)
674+
675+
unless count
676+
REDIS.set(key, 0)
677+
REDIS.expire(key, THROTTLE_TIME_WINDOW)
678+
return true
679+
end
680+
681+
if count.to_i >= THROTTLE_MAX_REQUESTS
682+
render :json => {:message => "You have fired too many requests. Please wait for some time."}, :status => 429
683+
return
684+
end
685+
REDIS.incr(key)
686+
true
687+
end
688+
689+
def throttle_token
690+
if @token.present?
691+
key = "count:#{@token}"
692+
count = REDIS.get(key)
693+
694+
unless count
695+
REDIS.set(key, 0)
696+
REDIS.expire(key, THROTTLE_TIME_WINDOW)
697+
return true
698+
end
699+
700+
if count.to_i >= THROTTLE_MAX_REQUESTS
701+
render :json => {:message => "You have fired too many requests. Please wait for some time."}, :status => 429
702+
return
703+
end
704+
REDIS.incr(key)
705+
true
706+
else
707+
false
708+
end
709+
end
710+
end
711+
```
712+
713+
Let’s go ahead and test this `test_throttle.sh`.
714+
```
715+
for i in {1..300}
716+
do
717+
printf "\n------------------\n"
718+
echo "Welcome $i times"
719+
printf "\n"
720+
# curl -i -H "Authorization: Token token=3Hu9orST5sKDHUPJBwjbogtt" http://localhost:3000/v1/users >> /dev/null
721+
# curl -i -H "Authorization: Token token=3Hu9orST5sKDHUPJBwjbogtt" http://10.1.0.201:3000/v1/users
722+
curl -i -H "Authorization: Token token=3Hu9orST5sKDHUPJBwjbogtt" http://localhost:3000/v1/users
723+
done
724+
```
725+
628726
## How to run
629727
*Clone source from github: `git@github.com:ntamvl/rails_5_api_tutorial.git`*
630728
```
@@ -674,3 +772,10 @@ Now you have the keys to the castle, and all the basics for building an API the
674772
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.
675773

676774
Cheers, and happy coding!
775+
776+
---------------------------------------------
777+
Redis documentation for INCR command. [return]
778+
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]
779+
Rails’ before filter. [return]
780+
IETF: Additional HTTP Status Codes - 429 Too Many Requests. [return]
781+
*If you have questions or comments about this blog post, you can get in touch with me on Twitter @nguyentamvn*

app/controllers/application_controller.rb

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ class ApplicationController < ActionController::API
33
include ActionController::HttpAuthentication::Token::ControllerMethods
44

55
before_action :authenticate
6+
before_filter :throttle_token
67

78
protected
89

@@ -13,12 +14,54 @@ def authenticate
1314
def authenticate_token
1415
authenticate_with_http_token do |token, options|
1516
@current_user = User.find_by(api_key: token)
17+
@token = token
1618
end
1719
end
1820

1921
def render_unauthorized(realm = "Application")
2022
self.headers["WWW-Authenticate"] = %(Token realm="#{realm.gsub(/"/, "")}")
21-
render json: 'Bad credentials', status: :unauthorized
23+
render json: {message: 'Bad credentials'}, status: :unauthorized
24+
end
25+
26+
def throttle_ip
27+
client_ip = request.env["REMOTE_ADDR"]
28+
key = "count:#{client_ip}"
29+
count = REDIS.get(key)
30+
31+
unless count
32+
REDIS.set(key, 0)
33+
REDIS.expire(key, THROTTLE_TIME_WINDOW)
34+
return true
35+
end
36+
37+
if count.to_i >= THROTTLE_MAX_REQUESTS
38+
render :json => {:message => "You have fired too many requests. Please wait for some time."}, :status => 429
39+
return
40+
end
41+
REDIS.incr(key)
42+
true
43+
end
44+
45+
def throttle_token
46+
if @token.present?
47+
key = "count:#{@token}"
48+
count = REDIS.get(key)
49+
50+
unless count
51+
REDIS.set(key, 0)
52+
REDIS.expire(key, THROTTLE_TIME_WINDOW)
53+
return true
54+
end
55+
56+
if count.to_i >= THROTTLE_MAX_REQUESTS
57+
render :json => {:message => "You have fired too many requests. Please wait for some time."}, :status => 429
58+
return
59+
end
60+
REDIS.incr(key)
61+
true
62+
else
63+
false
64+
end
2265
end
2366

2467
end

config/initializers/rack_attack.rb

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@ class Rack::Attack
99
'127.0.0.1' == req.ip || '::1' == req.ip
1010
end
1111

12-
# Allow an IP address to make 5 requests every 5 seconds
13-
throttle('req/ip', limit: 5, period: 5) do |req|
12+
# Allow an IP address to make 100 requests every 5 seconds
13+
throttle('req/ip', limit: 100, period: 5) do |req|
1414
req.ip
1515
end
1616

@@ -20,7 +20,7 @@ class Rack::Attack
2020
[
2121
429,
2222
{'Content-Type' => 'application/json', 'Retry-After' => retry_after.to_s},
23-
[{error: "Throttle limit reached. Retry later."}.to_json]
23+
[{error: "Throttle limit reached. Retry later.", status: 429}.to_json]
2424
]
2525
}
2626
end

config/initializers/throttle.rb

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
require "redis"
2+
3+
redis_conf = YAML.load(File.join(Rails.root, "config", "redis.yml"))
4+
REDIS = Redis.new(:host => redis_conf["host"], :port => redis_conf["port"])
5+
6+
THROTTLE_TIME_WINDOW = 15 * 60
7+
THROTTLE_MAX_REQUESTS = 60

config/redis.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
host: localhost
2+
port: 6379

test_throttle.sh

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
for i in {1..300}
2+
do
3+
printf "\n------------------\n"
4+
echo "Welcome $i times"
5+
printf "\n"
6+
# curl -i -H "Authorization: Token token=3Hu9orST5sKDHUPJBwjbogtt" http://localhost:3000/v1/users >> /dev/null
7+
# curl -i -H "Authorization: Token token=3Hu9orST5sKDHUPJBwjbogtt" http://10.1.0.201:3000/v1/users
8+
curl -i -H "Authorization: Token token=3Hu9orST5sKDHUPJBwjbogtt" http://localhost:3000/v1/users
9+
done

0 commit comments

Comments
 (0)