sinatra-rate-limiter 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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: []