galago-rate_limiter 0.0.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.
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA1:
|
|
3
|
+
metadata.gz: 6af298c6a194e5e8d7939e3e18890698efc6c4ef
|
|
4
|
+
data.tar.gz: 16a8dd9c1a13dfdf993c7b1f42ac0377ab65f1a9
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 2e046b041e5376884f6d79b693c92277b4b34ccc8836d98645badf33754f2a4879b5c84ba4d0240d7ef3166da2e71255fb2846d6621bb6c2d88260fd438532a2
|
|
7
|
+
data.tar.gz: 69563efbb8f7b0dfae163918c642a1d321e85d280d226666f090380e4f2e46617988239a7d3764224f0361853682b336f291179e1f03753f422f628c1f791f71
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
require 'singleton'
|
|
2
|
+
|
|
3
|
+
module Galago
|
|
4
|
+
class RateLimiter
|
|
5
|
+
class Configuration
|
|
6
|
+
include Singleton
|
|
7
|
+
|
|
8
|
+
DEFAULT_LIMIT = 5_000
|
|
9
|
+
DEFAULT_API_KEY_HEADER = 'HTTP_X_API_KEY'.freeze
|
|
10
|
+
|
|
11
|
+
attr_reader :api_key_header, :limit, :counter
|
|
12
|
+
|
|
13
|
+
def initialize
|
|
14
|
+
@limit = DEFAULT_LIMIT
|
|
15
|
+
@api_key_header = DEFAULT_API_KEY_HEADER
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def limit=(limit)
|
|
19
|
+
raise ArgumentError.new("Limit must be a positive number") if limit < 1
|
|
20
|
+
@limit = limit
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def api_key_header=(api_key_header)
|
|
24
|
+
header = api_key_header.dup
|
|
25
|
+
header.gsub!('-', '_')
|
|
26
|
+
header.upcase!
|
|
27
|
+
|
|
28
|
+
@api_key_header = "HTTP_#{header}".freeze
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def counter=(counter)
|
|
32
|
+
@counter = case counter
|
|
33
|
+
when Dalli::Client then MemcachedCounter.new(counter)
|
|
34
|
+
when Redis then RedisCounter.new(counter)
|
|
35
|
+
else counter
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def reset!
|
|
40
|
+
@limit = DEFAULT_LIMIT
|
|
41
|
+
@api_key_header = DEFAULT_API_KEY_HEADER
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
module Galago
|
|
2
|
+
class RateLimiter
|
|
3
|
+
class MemcachedCounter
|
|
4
|
+
def initialize(client)
|
|
5
|
+
@memcached = client
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def increment(key, amount, options = {})
|
|
9
|
+
@memcached.incr(key, amount, options[:expires_in], 1)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def reset!
|
|
13
|
+
@memcached.flush
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
module Galago
|
|
2
|
+
class RateLimiter::Railtie < ::Rails::Railtie
|
|
3
|
+
initializer "galago.rate_limiter.configure_counter" do |app|
|
|
4
|
+
app.config.middleware.use "Galago::RateLimiter"
|
|
5
|
+
|
|
6
|
+
RateLimiter.configure do |config|
|
|
7
|
+
config.counter = Rails.cache
|
|
8
|
+
end
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
module Galago
|
|
2
|
+
class RateLimiter
|
|
3
|
+
class RedisCounter
|
|
4
|
+
def initialize(client)
|
|
5
|
+
@redis = client
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def increment(key, amount, options = {})
|
|
9
|
+
count, _ = @redis.multi do |multi|
|
|
10
|
+
multi.incrby(key, amount)
|
|
11
|
+
multi.expire(key, options[:expires_in])
|
|
12
|
+
end
|
|
13
|
+
count
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def reset!
|
|
17
|
+
@redis.flushdb
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
require "json"
|
|
2
|
+
require_relative "./rate_limiter/configuration"
|
|
3
|
+
require_relative "./rate_limiter/memcached_counter"
|
|
4
|
+
require_relative "./rate_limiter/redis_counter"
|
|
5
|
+
require_relative "./rate_limiter/railtie" if defined?(Rails)
|
|
6
|
+
|
|
7
|
+
module Galago
|
|
8
|
+
class RateLimiter
|
|
9
|
+
X_LIMIT_HEADER = 'X-RateLimit-Limit'.freeze
|
|
10
|
+
X_RESET_HEADER = 'X-RateLimit-Reset'.freeze
|
|
11
|
+
X_REMAINING_HEADER = 'X-RateLimit-Remaining'.freeze
|
|
12
|
+
|
|
13
|
+
def self.configure
|
|
14
|
+
yield Configuration.instance
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def initialize(app)
|
|
18
|
+
@app = app
|
|
19
|
+
@config = Configuration.instance
|
|
20
|
+
@counter = @config.counter
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def call(env)
|
|
24
|
+
api_key = env[@config.api_key_header]
|
|
25
|
+
return @app.call(env) if api_key.nil?
|
|
26
|
+
throughput = @counter.increment(api_key, 1, expires_in: expires_in)
|
|
27
|
+
|
|
28
|
+
if limit_exceeded?(throughput)
|
|
29
|
+
status = 403
|
|
30
|
+
headers = {
|
|
31
|
+
X_LIMIT_HEADER => @config.limit.to_s,
|
|
32
|
+
X_REMAINING_HEADER => "0",
|
|
33
|
+
X_RESET_HEADER => limit_resets_at.to_s
|
|
34
|
+
}
|
|
35
|
+
body = [JSON(message: "API rate limit exceeded for #{api_key}")]
|
|
36
|
+
else
|
|
37
|
+
status, headers, body = @app.call(env)
|
|
38
|
+
headers[X_LIMIT_HEADER] = @config.limit.to_s
|
|
39
|
+
headers[X_REMAINING_HEADER] = (@config.limit - throughput).to_s
|
|
40
|
+
headers[X_RESET_HEADER] = limit_resets_at.to_s
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
[status, headers, body]
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
private
|
|
47
|
+
|
|
48
|
+
def limit_exceeded?(throughput)
|
|
49
|
+
throughput > @config.limit
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def timestamp
|
|
53
|
+
@timestamp ||= Time.now.utc.to_i
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def expires_in
|
|
57
|
+
timestamp % 3600
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Reset at the beginning of every hour.
|
|
61
|
+
def limit_resets_at
|
|
62
|
+
timestamp - (timestamp % 3600) + 3600
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
metadata
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: galago-rate_limiter
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.0.1
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Joe Karayusuf
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2015-03-22 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: bundler
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - "~>"
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '1.7'
|
|
20
|
+
type: :development
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - "~>"
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: '1.7'
|
|
27
|
+
- !ruby/object:Gem::Dependency
|
|
28
|
+
name: rake
|
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
|
30
|
+
requirements:
|
|
31
|
+
- - "~>"
|
|
32
|
+
- !ruby/object:Gem::Version
|
|
33
|
+
version: '10.0'
|
|
34
|
+
type: :development
|
|
35
|
+
prerelease: false
|
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
37
|
+
requirements:
|
|
38
|
+
- - "~>"
|
|
39
|
+
- !ruby/object:Gem::Version
|
|
40
|
+
version: '10.0'
|
|
41
|
+
- !ruby/object:Gem::Dependency
|
|
42
|
+
name: dalli
|
|
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
|
+
description:
|
|
56
|
+
email:
|
|
57
|
+
- jkarayusuf@gmail.com
|
|
58
|
+
executables: []
|
|
59
|
+
extensions: []
|
|
60
|
+
extra_rdoc_files: []
|
|
61
|
+
files:
|
|
62
|
+
- lib/galago/rate_limiter.rb
|
|
63
|
+
- lib/galago/rate_limiter/configuration.rb
|
|
64
|
+
- lib/galago/rate_limiter/memcached_counter.rb
|
|
65
|
+
- lib/galago/rate_limiter/railtie.rb
|
|
66
|
+
- lib/galago/rate_limiter/redis_counter.rb
|
|
67
|
+
homepage: ''
|
|
68
|
+
licenses:
|
|
69
|
+
- MIT
|
|
70
|
+
metadata: {}
|
|
71
|
+
post_install_message:
|
|
72
|
+
rdoc_options: []
|
|
73
|
+
require_paths:
|
|
74
|
+
- lib
|
|
75
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
76
|
+
requirements:
|
|
77
|
+
- - ">="
|
|
78
|
+
- !ruby/object:Gem::Version
|
|
79
|
+
version: '0'
|
|
80
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
81
|
+
requirements:
|
|
82
|
+
- - ">="
|
|
83
|
+
- !ruby/object:Gem::Version
|
|
84
|
+
version: '0'
|
|
85
|
+
requirements: []
|
|
86
|
+
rubyforge_project:
|
|
87
|
+
rubygems_version: 2.2.0
|
|
88
|
+
signing_key:
|
|
89
|
+
specification_version: 4
|
|
90
|
+
summary: GitHub style API Rate limiter
|
|
91
|
+
test_files: []
|