rate-limiting 1.0.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,4 @@
1
+ *.gem
2
+ .bundle
3
+ Gemfile.lock
4
+ pkg/*
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --color
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in rate-limiting.gemspec
4
+ gemspec
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
data/init.rb ADDED
@@ -0,0 +1 @@
1
+ require 'rate_limiting'
@@ -0,0 +1,5 @@
1
+ module Rate
2
+ module Limiting
3
+ VERSION = "1.0.2"
4
+ end
5
+ end
@@ -0,0 +1,125 @@
1
+ require "json"
2
+ require "rule"
3
+
4
+ class RateLimiting
5
+
6
+ def initialize(app, &block)
7
+ @app = app
8
+ @rules = []
9
+ @cache = {}
10
+ block.call(self)
11
+ end
12
+
13
+ def call(env)
14
+ request = Rack::Request.new(env)
15
+ (limit_header = allowed?(request)) ? respond(env, limit_header) : rate_limit_exceeded(env['HTTP_ACCEPT'])
16
+ end
17
+
18
+ def respond(env, limit_header)
19
+ status, header, response = @app.call(env)
20
+ (limit_header.class == Hash) ? [status, header.merge(limit_header), response] : [status, header, response]
21
+ end
22
+
23
+ def rate_limit_exceeded(accept)
24
+ case accept.gsub(/;.*/, "").split(',')[0]
25
+ when "text/xml" then message, type = xml_error("403", "Rate Limit Exceeded"), "text/xml"
26
+ when "application/json" then message, type = ["Rate Limit Exceeded"].to_json, "application/json"
27
+ else
28
+ message, type = ["Rate Limit Exceeded"], "text/html"
29
+ end
30
+ [403, {"Content-Type" => type}, message]
31
+ end
32
+
33
+ def define_rule(options)
34
+ @rules << Rule.new(options)
35
+ end
36
+
37
+ def set_cache(cache)
38
+ @cache = cache
39
+ end
40
+
41
+ def cache
42
+ case @cache
43
+ when Proc then @cache.call
44
+ else @cache
45
+ end
46
+ end
47
+
48
+ def cache_has?(key)
49
+ case
50
+ when cache.respond_to?(:has_key?)
51
+ cache.has_key?(key)
52
+ when cache.respond_to?(:get)
53
+ cache.get(key) rescue false
54
+ else false
55
+ end
56
+ end
57
+
58
+ def cache_get(key)
59
+ case
60
+ when cache.respond_to?(:[])
61
+ return cache[key]
62
+ when cache.respond_to?(:get)
63
+ return cache.get(key) || nil
64
+ end
65
+ end
66
+
67
+ def cache_set(key, value)
68
+ case
69
+ when cache.respond_to?(:[])
70
+ begin
71
+ cache[key] = value
72
+ rescue TypeError => e
73
+ cache[key] = value.to_s
74
+ end
75
+ when cache.respond_to?(:set)
76
+ cache.set(key, value)
77
+ end
78
+ end
79
+
80
+ def allowed?(request)
81
+ if rule = find_matching_rule(request)
82
+ apply_rule(request, rule)
83
+ else
84
+ true
85
+ end
86
+ end
87
+
88
+ def find_matching_rule(request)
89
+ @rules.each do |rule|
90
+ return rule if request.path =~ rule.match
91
+ end
92
+ nil
93
+ end
94
+
95
+ def apply_rule(request, rule)
96
+ key = rule.get_key(request)
97
+ if cache_has?(key)
98
+ record = cache_get(key)
99
+ if (reset = record.split(':')[1]) > Time.now.strftime("%d%m%y%H%M%S")
100
+ if (times = record.split(':')[0].to_i) < rule.limit
101
+ response = get_header(times + 1, reset, rule.limit)
102
+ record = record.gsub(/.*:/, "#{times + 1}:")
103
+ else
104
+ return false
105
+ end
106
+ else
107
+ response = get_header(1, reset = rule.get_expiration, rule.limit)
108
+ cache_set(key, "1:" + rule.get_expiration)
109
+ end
110
+ else
111
+ response = get_header(1, reset = rule.get_expiration, rule.limit)
112
+ cache_set(key, "1:" + rule.get_expiration)
113
+ end
114
+ response
115
+ end
116
+
117
+ def get_header(times, reset, limit)
118
+ {'x-RateLimit-Limit' => limit.to_s, 'x-RateLimit-Remaining' => (limit - times).to_s, 'x-RateLimit-Reset' => reset.to_s }
119
+ end
120
+
121
+ def xml_error(code, message)
122
+ "<?xml version=\"1.0\"?>\n<error>\n <code>#{code}</code>\n <message>#{message}</message>\n</error>"
123
+ end
124
+
125
+ end
@@ -0,0 +1,57 @@
1
+ class Rule
2
+
3
+ def initialize(options)
4
+ default_options = {
5
+ :match => /.*/,
6
+ :metric => :rph,
7
+ :type => :frequency,
8
+ :limit => 100,
9
+ :per_ip => true,
10
+ :token => false
11
+ }
12
+ @options = default_options.merge(options)
13
+
14
+ end
15
+
16
+ def match
17
+ @options[:match].class == String ? Regexp.new(@options[:match] + "$") : @options[:match]
18
+ end
19
+
20
+ def limit
21
+ (@options[:type] == :frequency ? 1 : @options[:limit])
22
+ end
23
+
24
+ def get_expiration
25
+ (Time.now + ( @options[:type] == :frequency ? get_frequency : get_fixed )).strftime("%d%m%y%H%M%S")
26
+ end
27
+
28
+ def get_frequency
29
+ case @options[:metric]
30
+ when :rpd
31
+ return (86400/@options[:limit] == 0 ? 1 : 86400/@options[:limit])
32
+ when :rph
33
+ return (3600/@options[:limit] == 0 ? 1 : 3600/@options[:limit])
34
+ when :rpm
35
+ return (60/@options[:limit] == 0 ? 1 : 60/@options[:limit])
36
+ end
37
+ end
38
+
39
+ def get_fixed
40
+ case @options[:metric]
41
+ when :rpd
42
+ return 86400
43
+ when :rph
44
+ return 3600
45
+ when :rpm
46
+ return 60
47
+ end
48
+ end
49
+
50
+ def get_key(request)
51
+ key = request.path
52
+ key = key + request.ip.to_s if @options[:per_ip]
53
+ key = key + request.params[@options[:token].to_s] if @options[:token]
54
+ key
55
+ end
56
+ end
57
+
@@ -0,0 +1,29 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "rate-limiting/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "rate-limiting"
7
+ s.version = Rate::Limiting::VERSION
8
+ s.authors = ["alepaez, pnegri"]
9
+ s.email = ["alexandre@iugu.com.br"]
10
+ s.homepage = "https://github.com/iugu/rate-limiting"
11
+ s.summary = %q{Rack Rate-Limit Gem}
12
+ s.description = %q{Easy way to Rate Limit your Rack app}
13
+
14
+ s.rubyforge_project = "rate-limiting"
15
+
16
+ s.files = `git ls-files`.split("\n")
17
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
18
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
19
+ s.require_paths = ["lib"]
20
+
21
+ # specify any dependencies here; for example:
22
+ # s.add_development_dependency "rspec"
23
+ # s.add_runtime_dependency "rest-client"
24
+
25
+ s.add_development_dependency "rspec"
26
+ s.add_development_dependency "rack-test"
27
+ s.add_dependency "json"
28
+
29
+ end
@@ -0,0 +1,50 @@
1
+ Rate Limiting
2
+ ===============
3
+
4
+
5
+ How to use it
6
+ ----------------
7
+
8
+ **Adding to Rails 3.x**
9
+
10
+ \# config/application.rb
11
+
12
+ > class Application < Rails::Application
13
+ >
14
+ > config.middleware.use RateLimiting do |r|
15
+ >
16
+ > r.define_rule( :match => '/resource', :type => :fixed, :metric => :rph, :limit => 300 )
17
+ >
18
+ > end
19
+ >
20
+ > end
21
+
22
+ Rule Options
23
+ ----------------
24
+
25
+ **match**
26
+
27
+ Accepts aimed resource path or Regexp like '/resource' or "/resource/.*"
28
+
29
+ **metric**
30
+
31
+ :rpd - Requests per Day
32
+
33
+ :rph - Requests per Hour
34
+
35
+ :rpm - Requests per Minute
36
+
37
+ **type**
38
+
39
+ :frequency - 1 request per (time/limit)
40
+
41
+ :fixed - limit requests per time
42
+
43
+ **token**
44
+
45
+ :foo - limit by request parameter 'foo'
46
+
47
+ **per_ip**
48
+
49
+ Boolean, true = limit by IP
50
+
@@ -0,0 +1,17 @@
1
+ require 'spec_helper'
2
+
3
+ describe "Fixed/rpd rule request" do
4
+ include Rack::Test::Methods
5
+
6
+ it 'should be allowed if not exceed limit' do
7
+ get '/fixed/rpd', {}, {'HTTP_ACCEPT' => "text/html"}
8
+ last_response.body.should show_allowed_response
9
+ end
10
+
11
+ it 'should not be allowed if exceed limit' do
12
+ 2.times { get '/fixed/rpd', {}, {'HTTP_ACCEPT' => "text/html"} }
13
+ last_response.body.should show_not_allowed_response
14
+ end
15
+
16
+ end
17
+
@@ -0,0 +1,16 @@
1
+ require 'spec_helper'
2
+
3
+ describe "Fixed/rph rule request" do
4
+ include Rack::Test::Methods
5
+
6
+ it 'should be allowed if not exceed limit' do
7
+ get '/fixed/rph', {}, {'HTTP_ACCEPT' => "text/html"}
8
+ last_response.body.should show_allowed_response
9
+ end
10
+
11
+ it 'should not be allowed if exceed limit' do
12
+ 2.times { get '/fixed/rph', {}, {'HTTP_ACCEPT' => "text/html"} }
13
+ last_response.body.should show_not_allowed_response
14
+ end
15
+
16
+ end
@@ -0,0 +1,17 @@
1
+ require 'spec_helper'
2
+
3
+ describe "Fixed/rpm rule request" do
4
+ include Rack::Test::Methods
5
+
6
+ it 'should be allowed if not exceed limit' do
7
+ get '/fixed/rpm', {}, {'HTTP_ACCEPT' => "text/html"}
8
+ last_response.body.should show_allowed_response
9
+ end
10
+
11
+ it 'should not be allowed if exceed limit' do
12
+ 2.times { get '/fixed/rpm', {}, {'HTTP_ACCEPT' => "text/html"} }
13
+ last_response.body.should show_not_allowed_response
14
+ end
15
+
16
+ end
17
+
@@ -0,0 +1,16 @@
1
+ require 'spec_helper'
2
+
3
+ describe "Frequency/rpd rule request" do
4
+ include Rack::Test::Methods
5
+
6
+ it 'should be allowed if not exceed 1 request per min' do
7
+ get '/freq/rpd', {}, {'HTTP_ACCEPT' => "text/html"}
8
+ last_response.body.should show_allowed_response
9
+ end
10
+
11
+ it 'should not be allowed if exceed 1 request per min' do
12
+ 2.times { get '/freq/rpd', {}, {'HTTP_ACCEPT' => "text/html"} }
13
+ end
14
+
15
+ end
16
+
@@ -0,0 +1,16 @@
1
+ require 'spec_helper'
2
+
3
+ describe "Frequency/rph rule request" do
4
+ include Rack::Test::Methods
5
+
6
+ it 'should be allowed if not exceed 1 request per min' do
7
+ get '/freq/rph', {}, {'HTTP_ACCEPT' => "text/html"}
8
+ last_response.body.should show_allowed_response
9
+ end
10
+
11
+ it 'should not be allowed if exceed 1 request per min' do
12
+ 2.times { get '/freq/rph', {}, {'HTTP_ACCEPT' => "text/html"} }
13
+ last_response.body.should show_not_allowed_response
14
+ end
15
+
16
+ end
@@ -0,0 +1,17 @@
1
+ require 'spec_helper'
2
+
3
+ describe "Frequency/rpm rule request" do
4
+ include Rack::Test::Methods
5
+
6
+ it 'should be allowed if not exceed 1 request per min' do
7
+ get '/freq/rpm', {}, {'HTTP_ACCEPT' => "text/html"}
8
+ last_response.body.should show_allowed_response
9
+ end
10
+
11
+ it 'should not be allowed if exceed 1 request per min' do
12
+ 2.times { get '/freq/rpm', {}, {'HTTP_ACCEPT' => "text/html"} }
13
+ last_response.body.should show_not_allowed_response
14
+ end
15
+
16
+ end
17
+
@@ -0,0 +1,54 @@
1
+ require "spec_helper"
2
+
3
+ describe "response headers" do
4
+ include Rack::Test::Methods
5
+
6
+ context "limited request" do
7
+ before(:each) do
8
+ get '/header', {}, {'HTTP_ACCEPT' => 'text/html'}
9
+ end
10
+
11
+ it 'should have x-RateLimit-Limit' do
12
+ last_response.header.should include "x-RateLimit-Limit"
13
+ end
14
+
15
+ it 'should have x-RateLimit-Remaining' do
16
+ last_response.header.should include "x-RateLimit-Remaining"
17
+ end
18
+
19
+ it 'should have x-RateLimit-Reset' do
20
+ last_response.header.should include "x-RateLimit-Reset"
21
+ end
22
+
23
+ it 'should have the right limit' do
24
+ last_response.header['x-RateLimit-Limit'].should == 1
25
+ end
26
+
27
+ it 'should have the right remaining' do
28
+ last_response.header['x-RateLimit-Remaining'].should == 0
29
+ end
30
+
31
+ end
32
+
33
+ context "not limited request" do
34
+
35
+ before(:each) do
36
+ get '/not_limited'
37
+ end
38
+
39
+ it 'should have x-RateLimit-Limit' do
40
+ last_response.header.should_not include "x-RateLimit-Limit"
41
+ end
42
+
43
+ it 'should have x-RateLimit-Remaining' do
44
+ last_response.header.should_not include "x-RateLimit-Remaining"
45
+ end
46
+
47
+ it 'should have x-RateLimit-Reset' do
48
+ last_response.header.should_not include "x-RateLimit-Reset"
49
+ end
50
+
51
+ end
52
+
53
+
54
+ end
@@ -0,0 +1,19 @@
1
+ require 'spec_helper'
2
+
3
+ describe "html request" do
4
+
5
+ include Rack::Test::Methods
6
+
7
+ it 'should receive allowed' do
8
+ app.should_receive(:allowed?).twice
9
+ get '/test', {}, {'HTTP_ACCEPT' => "text/html"}
10
+ get '/test2', {}, {'HTTP_ACCEPT' => "text/html"}
11
+ end
12
+
13
+ it 'should receive allowed' do
14
+ 2.times { get '/html', {}, {'HTTP_ACCEPT' => "text/html"} }
15
+ last_response.content_type.should == "text/html"
16
+ end
17
+
18
+ end
19
+
@@ -0,0 +1,10 @@
1
+ require 'spec_helper'
2
+
3
+ describe "json request" do
4
+ include Rack::Test::Methods
5
+ it 'should receive allowed' do
6
+ 2.times { get '/json', {}, {'HTTP_ACCEPT' => "application/json"} }
7
+ last_response.content_type.should == "application/json"
8
+ end
9
+ end
10
+
@@ -0,0 +1,63 @@
1
+ require 'rspec'
2
+ require 'rack/test'
3
+ require 'rate_limiting'
4
+
5
+ def test_app
6
+ @test_app ||= mock("Test Rack App")
7
+ @test_app.stub!(:call).with(anything()).and_return([200, {}, "Test App Body"])
8
+ @test_app
9
+ end
10
+
11
+ def app
12
+ @test_app ||= test_app
13
+ @app ||= RateLimiting.new(@test_app) do |r|
14
+ r.define_rule(:match => '/html', :limit => 1)
15
+ r.define_rule(:match => '/json', :metric => :rph, :type => :frequency, :limit => 60)
16
+ r.define_rule(:match => '/xml', :metric => :rph, :type => :frequency, :limit => 60)
17
+ r.define_rule(:match => '/token/ip', :limit => 1, :token => :id, :per_ip => true)
18
+ r.define_rule(:match => '/token', :limit => 1, :token => :id, :per_ip => false)
19
+ r.define_rule(:match => '/fixed/rpm', :metric => :rpm, :type => :fixed, :limit => 1)
20
+ r.define_rule(:match => '/fixed/rph', :metric => :rph, :type => :fixed, :limit => 1)
21
+ r.define_rule(:match => '/fixed/rpd', :metric => :rpd, :type => :fixed, :limit => 1)
22
+ r.define_rule(:match => '/freq/rpm', :metric => :rpm, :type => :frequency, :limit => 1)
23
+ r.define_rule(:match => '/freq/rph', :metric => :rph, :type => :frequency, :limit => 60)
24
+ r.define_rule(:match => '/freq/rpd', :metric => :rpd, :type => :frequency, :limit => 1440)
25
+ r.define_rule(:match => '/header', :metric => :rph, :type => :frequency, :limit => 60)
26
+ end
27
+ end
28
+
29
+ Spec::Matchers.define :show_allowed_response do
30
+ match do |body|
31
+ body.include?("Test App Body")
32
+ end
33
+
34
+ failure_message_for_should do
35
+ "expected response to show the allowed response"
36
+ end
37
+
38
+ failure_message_for_should_not do
39
+ "expected response not to show the allowed response"
40
+ end
41
+
42
+ description do
43
+ "expected the allowed response"
44
+ end
45
+ end
46
+
47
+ Spec::Matchers.define :show_not_allowed_response do
48
+ match do |body|
49
+ body.include?("Rate Limit Exceeded")
50
+ end
51
+
52
+ failure_message_for_should do
53
+ "expected response to show the not allowed response"
54
+ end
55
+
56
+ failure_message_for_should_not do
57
+ "expected response not to show the not allowed response"
58
+ end
59
+
60
+ description do
61
+ "expected the not allowed response"
62
+ end
63
+ end
@@ -0,0 +1,36 @@
1
+ require "spec_helper"
2
+
3
+ describe "defined token rule" do
4
+ include Rack::Test::Methods
5
+
6
+ it 'should allow diferent ids' do
7
+ get '/token', { :id => "1" }, {'HTTP_ACCEPT' => "text/html"}
8
+ get '/token', { :id => "2" }, {'HTTP_ACCEPT' => "text/html"}
9
+ last_response.body.should show_allowed_response
10
+ end
11
+
12
+ it 'should not allow equal ids' do
13
+ 2.times { get '/token', { :id => "1" }, {'HTTP_ACCEPT' => "text/html"} }
14
+ last_response.body.should show_not_allowed_response
15
+ end
16
+
17
+ context "+ per_ip" do
18
+
19
+ it 'should allow diferent ids' do
20
+ get '/token/ip', { :id => "1" }, {'HTTP_ACCEPT' => "text/html"}
21
+ get '/token/ip', { :id => "2" }, {'HTTP_ACCEPT' => "text/html"}
22
+ last_response.body.should show_allowed_response
23
+ end
24
+
25
+ it 'should not allow equal ids' do
26
+ 2.times { get '/token/ip', { :id => "1" }, {'HTTP_ACCEPT' => "text/html"} }
27
+ last_response.body.should show_not_allowed_response
28
+ end
29
+
30
+
31
+ end
32
+
33
+ end
34
+
35
+
36
+
@@ -0,0 +1,13 @@
1
+ require 'spec_helper'
2
+
3
+ describe "xml request" do
4
+
5
+ include Rack::Test::Methods
6
+
7
+ it 'should receive allowed' do
8
+ 2.times { get '/xml', {}, {'HTTP_ACCEPT' => "text/xml"} }
9
+ last_response.content_type.should == "text/xml"
10
+ end
11
+
12
+ end
13
+
metadata ADDED
@@ -0,0 +1,127 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: rate-limiting
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.2
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - alepaez, pnegri
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-05-21 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: rspec
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
22
+ type: :development
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ! '>='
28
+ - !ruby/object:Gem::Version
29
+ version: '0'
30
+ - !ruby/object:Gem::Dependency
31
+ name: rack-test
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ! '>='
36
+ - !ruby/object:Gem::Version
37
+ version: '0'
38
+ type: :development
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ! '>='
44
+ - !ruby/object:Gem::Version
45
+ version: '0'
46
+ - !ruby/object:Gem::Dependency
47
+ name: json
48
+ requirement: !ruby/object:Gem::Requirement
49
+ none: false
50
+ requirements:
51
+ - - ! '>='
52
+ - !ruby/object:Gem::Version
53
+ version: '0'
54
+ type: :runtime
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - ! '>='
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ description: Easy way to Rate Limit your Rack app
63
+ email:
64
+ - alexandre@iugu.com.br
65
+ executables: []
66
+ extensions: []
67
+ extra_rdoc_files: []
68
+ files:
69
+ - .gitignore
70
+ - .rspec
71
+ - Gemfile
72
+ - Rakefile
73
+ - init.rb
74
+ - lib/rate-limiting/version.rb
75
+ - lib/rate_limiting.rb
76
+ - lib/rule.rb
77
+ - rate-limiting.gemspec
78
+ - readme.md
79
+ - spec/fixed/rpd_spec.rb
80
+ - spec/fixed/rph_spec.rb
81
+ - spec/fixed/rpm_spec.rb
82
+ - spec/frequency/rpd_spec.rb
83
+ - spec/frequency/rph_spec.rb
84
+ - spec/frequency/rpm_spec.rb
85
+ - spec/headers_spec.rb
86
+ - spec/html_request_spec.rb
87
+ - spec/json_request_spec.rb
88
+ - spec/spec_helper.rb
89
+ - spec/token_spec.rb
90
+ - spec/xml_request_spec.rb
91
+ homepage: https://github.com/iugu/rate-limiting
92
+ licenses: []
93
+ post_install_message:
94
+ rdoc_options: []
95
+ require_paths:
96
+ - lib
97
+ required_ruby_version: !ruby/object:Gem::Requirement
98
+ none: false
99
+ requirements:
100
+ - - ! '>='
101
+ - !ruby/object:Gem::Version
102
+ version: '0'
103
+ required_rubygems_version: !ruby/object:Gem::Requirement
104
+ none: false
105
+ requirements:
106
+ - - ! '>='
107
+ - !ruby/object:Gem::Version
108
+ version: '0'
109
+ requirements: []
110
+ rubyforge_project: rate-limiting
111
+ rubygems_version: 1.8.23
112
+ signing_key:
113
+ specification_version: 3
114
+ summary: Rack Rate-Limit Gem
115
+ test_files:
116
+ - spec/fixed/rpd_spec.rb
117
+ - spec/fixed/rph_spec.rb
118
+ - spec/fixed/rpm_spec.rb
119
+ - spec/frequency/rpd_spec.rb
120
+ - spec/frequency/rph_spec.rb
121
+ - spec/frequency/rpm_spec.rb
122
+ - spec/headers_spec.rb
123
+ - spec/html_request_spec.rb
124
+ - spec/json_request_spec.rb
125
+ - spec/spec_helper.rb
126
+ - spec/token_spec.rb
127
+ - spec/xml_request_spec.rb