rack-ratelimit 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/lib/rack-ratelimit.rb +1 -0
- data/lib/rack/ratelimit.rb +162 -0
- metadata +101 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 73b500a4dca2655841490da8c02106a49f69f62b
|
4
|
+
data.tar.gz: 265204cbba7c818c0190eee355d55a7147083d1a
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: da84278754fd180b780687f5819f4847d2e8fcf49378029e023b95706324b3b06eedd5cedf1c3b453e26723ed8b409ea78799e190497076db3411be9f9f55081
|
7
|
+
data.tar.gz: 2af1a03a36288b8cbec652185ff1207b3e9052b58d3c0737c7a07159f349eeadaacf42c79acfde8ad2ae8a46f011b61fd04619d21b629117ae1f059ec4e9d7b3
|
@@ -0,0 +1 @@
|
|
1
|
+
require 'rack/ratelimit'
|
@@ -0,0 +1,162 @@
|
|
1
|
+
require 'dalli'
|
2
|
+
require 'logger'
|
3
|
+
require 'time'
|
4
|
+
|
5
|
+
module Rack
|
6
|
+
# = Ratelimit
|
7
|
+
#
|
8
|
+
# * Run multiple rate limiters in a single app
|
9
|
+
# * Scope each rate limit to certain requests: API, files, GET vs POST, etc.
|
10
|
+
# * Apply each rate limit by request characteristics: IP, subdomain, OAuth2 token, etc.
|
11
|
+
# * Flexible time window to limit burst traffic vs hourly or daily traffic:
|
12
|
+
# 100 requests per 10 sec, 500 req/minute, 10000 req/hour, etc.
|
13
|
+
# * Fast, low-overhead implementation using memcache counters per time window:
|
14
|
+
# timeslice = window * ceiling(current time / window)
|
15
|
+
# memcache.incr(counter for timeslice)
|
16
|
+
class Ratelimit
|
17
|
+
# Takes a block that classifies requests for rate limiting. Given a
|
18
|
+
# Rack env, return a string such as IP address, API token, etc. If the
|
19
|
+
# block returns nil, the request won't be rate-limited. If a block is
|
20
|
+
# not given, all requests get the same limits.
|
21
|
+
#
|
22
|
+
# Required configuration:
|
23
|
+
# rate: an array of [max requests, period in seconds]: [500, 5.minutes]
|
24
|
+
# cache: a Dalli::Client instance, or an object that quacks like it.
|
25
|
+
#
|
26
|
+
# Optional configuration:
|
27
|
+
# name: name of the rate limiter. Defaults to 'HTTP'. Used in messages.
|
28
|
+
# status: HTTP response code. Defaults to 429.
|
29
|
+
# conditions: array of procs that take a rack env, all of which must
|
30
|
+
# return true to rate-limit the request.
|
31
|
+
# exceptions: array of procs that take a rack env, any of which may
|
32
|
+
# return true to exclude the request from rate limiting.
|
33
|
+
# logger: responds to #info(message). If provided, the rate limiter
|
34
|
+
# logs the first request that hits the rate limit, but none of the
|
35
|
+
# subsequently blocked requests.
|
36
|
+
# error_message: the message returned in the response body when the rate
|
37
|
+
# limit is exceeded. Defaults to "<name> rate limit exceeded. Please
|
38
|
+
# wait <period> seconds then retry your request."
|
39
|
+
#
|
40
|
+
# Example:
|
41
|
+
#
|
42
|
+
# Rate-limit bursts of POST/PUT/DELETE by IP address, return 503:
|
43
|
+
# use(Rack::Ratelimit, name: 'POST',
|
44
|
+
# exceptions: ->(env) { env['REQUEST_METHOD'] == 'GET' },
|
45
|
+
# rate: [50, 10.seconds],
|
46
|
+
# status: 503,
|
47
|
+
# cache: Dalli::Client.new,
|
48
|
+
# logger: Rails.logger) { |env| Rack::Request.new(env).ip }
|
49
|
+
#
|
50
|
+
# Rate-limit API traffic by user (set by Rack::Auth::Basic):
|
51
|
+
# use(Rack::Ratelimit, name: 'API',
|
52
|
+
# conditions: ->(env) { env['REMOTE_USER'] },
|
53
|
+
# rate: [1000, 1.hour],
|
54
|
+
# cache: Dalli::Client.new,
|
55
|
+
# logger: Rails.logger) { |env| env['REMOTE_USER'] }
|
56
|
+
def initialize(app, options, &classifier)
|
57
|
+
@app, @classifier = app, classifier
|
58
|
+
@classifier ||= lambda { |env| :request }
|
59
|
+
|
60
|
+
@name = options.fetch(:name, 'HTTP')
|
61
|
+
@max, @period = options.fetch(:rate)
|
62
|
+
@status = options.fetch(:status, 429)
|
63
|
+
|
64
|
+
@counter = Counter.new(options.fetch(:cache), @name, @period)
|
65
|
+
|
66
|
+
@logger = options[:logger]
|
67
|
+
@error_message = options.fetch(:error_message, "#{@name} rate limit exceeded. Please wait #{@period} seconds then retry your request.")
|
68
|
+
|
69
|
+
@conditions = Array(options[:conditions])
|
70
|
+
@exceptions = Array(options[:exceptions])
|
71
|
+
end
|
72
|
+
|
73
|
+
# Add a condition that must be met before applying the rate limit.
|
74
|
+
# Pass a block or a proc argument that takes a Rack env and returns
|
75
|
+
# true if the request should be limited.
|
76
|
+
def condition(predicate = nil, &block)
|
77
|
+
@conditions << predicate if predicate
|
78
|
+
@conditions << block if block_given?
|
79
|
+
end
|
80
|
+
|
81
|
+
# Add an exception that excludes requests from the rate limit.
|
82
|
+
# Pass a block or a proc argument that takes a Rack env and returns
|
83
|
+
# true if the request should be excluded from rate limiting.
|
84
|
+
def exception(predicate = nil, &block)
|
85
|
+
@exceptions << predicate if predicate
|
86
|
+
@exceptions << block if block_given?
|
87
|
+
end
|
88
|
+
|
89
|
+
# Apply the rate limiter if none of the exceptions apply and all the
|
90
|
+
# conditions are met.
|
91
|
+
def apply_rate_limit?(env)
|
92
|
+
@exceptions.none? { |e| e.call(env) } && @conditions.all? { |c| c.call(env) }
|
93
|
+
end
|
94
|
+
|
95
|
+
# Handle a Rack request:
|
96
|
+
# * Check whether the rate limit applies to the request.
|
97
|
+
# * Classify the request by IP, API token, etc.
|
98
|
+
# * Calculate the end of the current time window.
|
99
|
+
# * Increment the counter for this classification and time window.
|
100
|
+
# * If count exceeds limit, return a 429 response.
|
101
|
+
# * If it's the first request that exceeds the limit, log it.
|
102
|
+
# * If the count doesn't exceed the limit, pass through the request.
|
103
|
+
def call(env)
|
104
|
+
if apply_rate_limit?(env) && classification = @classifier.call(env)
|
105
|
+
|
106
|
+
# Marks the end of the current rate-limiting window.
|
107
|
+
timestamp = @period * (Time.now.to_f / @period).ceil
|
108
|
+
time = Time.at(timestamp).utc.xmlschema
|
109
|
+
|
110
|
+
# Increment the request counter.
|
111
|
+
count = @counter.increment(classification, timestamp)
|
112
|
+
remaining = @max - count + 1
|
113
|
+
|
114
|
+
json = %({"name":"#{@name}","period":#{@period},"limit":#{@max},"remaining":#{remaining},"until":"#{time}"})
|
115
|
+
|
116
|
+
# If exceeded, return a 429 Rate Limit Exceeded response.
|
117
|
+
if remaining <= 0
|
118
|
+
# Only log the first hit that exceeds the limit.
|
119
|
+
if @logger && remaining == 0
|
120
|
+
@logger.info '%s: %s exceeded %d request limit for %s' % [@name, classification, @max, time]
|
121
|
+
end
|
122
|
+
|
123
|
+
[ @status,
|
124
|
+
{ 'X-Ratelimit' => json, 'Retry-After' => @period.to_s },
|
125
|
+
[@error_message] ]
|
126
|
+
|
127
|
+
# Otherwise, pass through then add some informational headers.
|
128
|
+
else
|
129
|
+
@app.call(env).tap do |status, headers, body|
|
130
|
+
headers['X-Ratelimit'] = [headers['X-Ratelimit'], json].compact.join("\n")
|
131
|
+
end
|
132
|
+
end
|
133
|
+
else
|
134
|
+
@app.call(env)
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
class Counter
|
139
|
+
def initialize(cache, name, period)
|
140
|
+
@cache, @name, @period = cache, name, period
|
141
|
+
end
|
142
|
+
|
143
|
+
# Increment the request counter and return the current count.
|
144
|
+
def increment(classification, timestamp)
|
145
|
+
key = 'rack-ratelimit/%s/%s/%i' % [@name, classification, timestamp]
|
146
|
+
|
147
|
+
# Try to increment the counter if it's present.
|
148
|
+
if count = @cache.incr(key, 1)
|
149
|
+
count.to_i
|
150
|
+
|
151
|
+
# If not, add the counter and set expiry.
|
152
|
+
elsif @cache.add(key, 1, @period, :raw => true)
|
153
|
+
1
|
154
|
+
|
155
|
+
# If adding failed, someone else added it concurrently. Increment.
|
156
|
+
else
|
157
|
+
@cache.incr(key, 1).to_i
|
158
|
+
end
|
159
|
+
end
|
160
|
+
end
|
161
|
+
end
|
162
|
+
end
|
metadata
ADDED
@@ -0,0 +1,101 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: rack-ratelimit
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Jeremy Kemper
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2014-03-21 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: rack
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: dalli
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rake
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: minitest
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: 5.3.0
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: 5.3.0
|
69
|
+
description:
|
70
|
+
email: jeremy@bitsweat.net
|
71
|
+
executables: []
|
72
|
+
extensions: []
|
73
|
+
extra_rdoc_files: []
|
74
|
+
files:
|
75
|
+
- "./lib/rack-ratelimit.rb"
|
76
|
+
- "./lib/rack/ratelimit.rb"
|
77
|
+
homepage:
|
78
|
+
licenses:
|
79
|
+
- MIT
|
80
|
+
metadata: {}
|
81
|
+
post_install_message:
|
82
|
+
rdoc_options: []
|
83
|
+
require_paths:
|
84
|
+
- lib
|
85
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - ">="
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '1.8'
|
90
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
91
|
+
requirements:
|
92
|
+
- - ">="
|
93
|
+
- !ruby/object:Gem::Version
|
94
|
+
version: '0'
|
95
|
+
requirements: []
|
96
|
+
rubyforge_project:
|
97
|
+
rubygems_version: 2.2.0
|
98
|
+
signing_key:
|
99
|
+
specification_version: 4
|
100
|
+
summary: Flexible rate limits for your Rack apps
|
101
|
+
test_files: []
|