sinatra-rate-limiter 0.2.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (5) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +20 -0
  3. data/README.md +87 -0
  4. data/lib/sinatra/rate-limiter.rb +128 -0
  5. metadata +74 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 030b21e819260562c4da8c1472360e4d8cdb1c8d
4
+ data.tar.gz: 456ad3d77b05fe72a5d01aba6ee3699b7da49916
5
+ SHA512:
6
+ metadata.gz: 920f5b487a6dd84cd220a27869811d834102ff591a68d2446758b851546893af17f1e1dbdf8fbb9b44f9bf88547089263bc30c0af137c786b3bc2db710cd2303
7
+ data.tar.gz: 14427e370cc5f040287edfba31247480fac29bb2c66a7b06199df0e7f778f9c75c35cb840b926253bca41fe17c3748cabc65dd1c88076fbf50cfdd53a0b36a72
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (C) 2015 Warren Guy <warren@guy.net.au>
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of
6
+ this software and associated documentation files (the "Software"), to deal in
7
+ the Software without restriction, including without limitation the rights to
8
+ use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
9
+ the Software, and to permit persons to whom the Software is furnished to do so,
10
+ subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
17
+ FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
18
+ COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
19
+ IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
20
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,87 @@
1
+ # sinatra/rate-limiter
2
+
3
+ A customisable redis backed rate limiter for Sinatra applications.
4
+
5
+ ## Installing
6
+
7
+ * Add the gem to your Gemfile
8
+
9
+ ```ruby
10
+ source 'https://rubygems.org'
11
+ gem 'sinatra-rate-limiter'
12
+ ```
13
+
14
+ * Require and enable it in your app after including Sinatra
15
+
16
+ ```ruby
17
+ require 'sinatra/rate-limiter'
18
+ enable :rate_limiter
19
+ ```
20
+
21
+ ## Usage
22
+
23
+ Use `rate_limit` in the pipeline of any route (i.e. in the route itself, or
24
+ in a `before` filter, or in a Padrino controller, etc. `rate_limit` takes
25
+ zero to infinite parameters, with the syntax:
26
+
27
+ ```rate_limit [String], [<Fixnum>, <Fixnum>], [<Fixnum>, <Fixnum>], ...```
28
+
29
+ The following route will be limited to 10 requests per minute and 100
30
+ requests per hour:
31
+
32
+ ```ruby
33
+ get '/rate-limited' do
34
+ rate_limit 'default', 10, 60, 100, 60*60
35
+
36
+ "now you see me"
37
+ end
38
+ ```
39
+
40
+ The following will apply an unnamed limit of 1000 requests per hour to all
41
+ routes and stricter individual rate limits to two particular routes:
42
+
43
+ ```ruby
44
+ set :rate_limiter_default_limits, [1000, 60*60]
45
+ before do
46
+ rate_limit
47
+ end
48
+
49
+ get '/' do
50
+ "this route has the global limit applied"
51
+ end
52
+
53
+ get '/rate-limit-1' do
54
+ rate_limit 'ratelimit1', 2, 5,
55
+ 10, 60
56
+ end
57
+
58
+ get '/rate-limit-2' do
59
+ rate_limit 'ratelimit2', 1, 10
60
+ end
61
+ ```
62
+
63
+ N.B. in the last example, be aware that the more specific rate limits do not
64
+ override any rate limit already defined during route processing, and the
65
+ global rate limit will apply additionally. If you call `rate_limit` more than
66
+ once with the same (or no) name, it will be double counted.
67
+
68
+ ## Configuration
69
+
70
+ All configuration is optional. If no default limits are specified here,
71
+ you must specify limits with each call of `rate_limit`
72
+
73
+ ### Defaults
74
+
75
+ ```ruby
76
+ set :rate_limiter_default_limits, []
77
+ set :rate_limiter_environments, [:production]
78
+ set :rate_limiter_error_code, 429
79
+ set :rate_limiter_error_template, nil
80
+ set :rate_limiter_send_headers, true
81
+ set :rate_limiter_custom_user_id, nil
82
+ set :rate_limiter_redis_conn, Redis.new
83
+ set :rate_limiter_redis_namespace, 'rate_limit'
84
+ set :rate_limiter_redis_expires, 24*60*60
85
+ ```
86
+
87
+ TODO: document each setting here explicitly
@@ -0,0 +1,128 @@
1
+ require 'sinatra/base'
2
+ require 'redis'
3
+
4
+ module Sinatra
5
+
6
+ module RateLimiter
7
+
8
+ module Helpers
9
+
10
+ def rate_limit(*args)
11
+ return unless settings.rate_limiter and settings.rate_limiter_environments.include?(settings.environment)
12
+
13
+ limit_name, limits = parse_args(args)
14
+
15
+ if error_locals = limits_exceeded?(limits, limit_name)
16
+ rate_limit_headers(limits, limit_name)
17
+ response.headers['Retry-After'] = error_locals[:try_again] if settings.rate_limiter_send_headers
18
+ halt settings.rate_limiter_error_code, error_response(error_locals)
19
+ end
20
+
21
+ redis.setex([namespace,user_identifier,limit_name,Time.now.to_f.to_s].join('/'),
22
+ settings.rate_limiter_redis_expires,
23
+ request.env['REQUEST_URI'])
24
+
25
+ rate_limit_headers(limits, limit_name) if settings.rate_limiter_send_headers
26
+ end
27
+
28
+ private
29
+
30
+ def parse_args(args)
31
+ limit_name = args.map{|a| a.class}.first.eql?(String) ? args.shift : 'default'
32
+ args = settings.rate_limiter_default_limits if args.size < 1
33
+
34
+ if (args.size < 1)
35
+ raise ArgumentError, 'No explicit or default limits values provided.'
36
+ elsif (args.map{|a| a.class}.select{|a| a != Fixnum}.count > 0)
37
+ raise ArgumentError, 'Non-Fixnum parameters supplied. All parameters must be Fixnum except the first which may be a String.'
38
+ elsif ((args.map{|a| a.class}.size % 2) != 0)
39
+ raise ArgumentError, 'Wrong number of Fixnum parameters supplied.'
40
+ end
41
+
42
+ limits = args.each_slice(2).to_a.map{|a| {requests: a[0], seconds: a[1]}}
43
+
44
+ return [limit_name, limits]
45
+ end
46
+
47
+ def redis
48
+ settings.rate_limiter_redis_conn
49
+ end
50
+
51
+ def namespace
52
+ settings.rate_limiter_redis_namespace
53
+ end
54
+
55
+ def limit_history(limit_name, seconds=0)
56
+ redis.
57
+ keys("#{[namespace,user_identifier,limit_name].join('/')}/*").
58
+ map{|k| k.split('/')[3].to_f}.
59
+ select{|t| seconds.eql?(0) ? true : t > (Time.now.to_f - seconds)}
60
+ end
61
+
62
+ def limit_remaining(limit, limit_name)
63
+ limit[:requests] - limit_history(limit_name, limit[:seconds]).length
64
+ end
65
+
66
+ def limit_reset(limit, limit_name)
67
+ limit[:seconds] - (Time.now.to_f - limit_history(limit_name, limit[:seconds]).first).to_i
68
+ end
69
+
70
+ def limits_exceeded?(limits, limit_name)
71
+ exceeded = limits.select {|limit| limit_remaining(limit, limit_name) < 1}.sort_by{|e| e[:seconds]}.last
72
+
73
+ if exceeded
74
+ try_again = limit_reset(exceeded, limit_name)
75
+ return exceeded.merge({try_again: try_again.to_i})
76
+ end
77
+ end
78
+
79
+ def rate_limit_headers(limits, limit_name)
80
+ header_prefix = 'X-Rate-Limit' + (limit_name.eql?('default') ? '' : '-' + limit_name)
81
+ limit_no = 0 if limits.length > 1
82
+ limits.each do |limit|
83
+ limit_no = limit_no + 1 if limit_no
84
+ response.headers[header_prefix + (limit_no ? "-#{limit_no}" : '') + '-Limit'] = limit[:requests]
85
+ response.headers[header_prefix + (limit_no ? "-#{limit_no}" : '') + '-Remaining'] = limit_remaining(limit, limit_name)
86
+ response.headers[header_prefix + (limit_no ? "-#{limit_no}" : '') + '-Reset'] = limit_reset(limit, limit_name)
87
+ end
88
+ end
89
+
90
+ def error_response(locals)
91
+ if settings.rate_limiter_error_template
92
+ render settings.rate_limiter_error_template, locals: locals
93
+ else
94
+ content_type 'text/plain'
95
+ "Rate limit exceeded (#{locals[:requests]} requests in #{locals[:seconds]} seconds). Try again in #{locals[:try_again]} seconds."
96
+ end
97
+ end
98
+
99
+ def user_identifier
100
+ if settings.rate_limiter_custom_user_id.class == Proc
101
+ return settings.rate_limiter_custom_user_id.call(request)
102
+ else
103
+ return request.ip
104
+ end
105
+ end
106
+
107
+ end
108
+
109
+ def self.registered(app)
110
+ app.helpers RateLimiter::Helpers
111
+
112
+ app.set :rate_limiter, false
113
+ app.set :rate_limiter_default_limits, [] # 10 requests per minute: [{requests: 10, seconds: 60}]
114
+ app.set :rate_limiter_environments, [:production]
115
+ app.set :rate_limiter_error_code, 429 # http://tools.ietf.org/html/rfc6585
116
+ app.set :rate_limiter_error_template, nil # locals: requests, seconds, try_again
117
+ app.set :rate_limiter_send_headers, true
118
+ app.set :rate_limiter_custom_user_id, nil # Proc.new { Proc.new{ |request| request.ip } }
119
+ # must be wrapped with another Proc because Sinatra
120
+ # evaluates Procs in settings when reading them.
121
+ app.set :rate_limiter_redis_conn, Redis.new
122
+ app.set :rate_limiter_redis_namespace, 'rate_limit'
123
+ app.set :rate_limiter_redis_expires, 24*60*60
124
+ end
125
+
126
+ end
127
+
128
+ end
metadata ADDED
@@ -0,0 +1,74 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: sinatra-rate-limiter
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.2.1
5
+ platform: ruby
6
+ authors:
7
+ - Warren Guy
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2015-05-28 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: sinatra
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.3'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.3'
27
+ - !ruby/object:Gem::Dependency
28
+ name: redis
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '3.0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '3.0'
41
+ description: A redis based rate limiter for Sinatra
42
+ email: warren@guy.net.au
43
+ executables: []
44
+ extensions: []
45
+ extra_rdoc_files: []
46
+ files:
47
+ - LICENSE
48
+ - README.md
49
+ - lib/sinatra/rate-limiter.rb
50
+ homepage: https://github.com/warrenguy/sinatra-rate-limiter
51
+ licenses:
52
+ - MIT
53
+ metadata: {}
54
+ post_install_message:
55
+ rdoc_options: []
56
+ require_paths:
57
+ - lib
58
+ required_ruby_version: !ruby/object:Gem::Requirement
59
+ requirements:
60
+ - - ">="
61
+ - !ruby/object:Gem::Version
62
+ version: '0'
63
+ required_rubygems_version: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - ">="
66
+ - !ruby/object:Gem::Version
67
+ version: '0'
68
+ requirements: []
69
+ rubyforge_project:
70
+ rubygems_version: 2.2.2
71
+ signing_key:
72
+ specification_version: 4
73
+ summary: A redis based rate limiter for Sinatra
74
+ test_files: []