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: []