tshield 0.8.0.0 → 0.9.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/Gemfile +3 -2
- data/README.md +147 -4
- data/Rakefile +13 -2
- data/bin/tshield +5 -5
- data/config/tshield.yml +9 -0
- data/lib/tshield/after_filter.rb +3 -2
- data/lib/tshield/before_filter.rb +3 -2
- data/lib/tshield/configuration.rb +57 -36
- data/lib/tshield/controller.rb +22 -10
- data/lib/tshield/controllers/requests.rb +20 -21
- data/lib/tshield/controllers/sessions.rb +2 -3
- data/lib/tshield/counter.rb +5 -5
- data/lib/tshield/logger.rb +10 -0
- data/lib/tshield/options.rb +61 -27
- data/lib/tshield/request.rb +25 -28
- data/lib/tshield/response.rb +2 -0
- data/lib/tshield/server.rb +24 -19
- data/lib/tshield/sessions.rb +6 -4
- data/lib/tshield/simple_tcp_server.rb +3 -2
- data/lib/tshield/version.rb +4 -2
- data/lib/tshield.rb +3 -2
- data/spec/spec_helper.rb +6 -6
- data/spec/tshield/after_filter_spec.rb +7 -0
- data/spec/tshield/configuration_spec.rb +57 -20
- data/spec/tshield/fixtures/config/tshield.yml +7 -1
- data/spec/tshield/fixtures/filters/example_filter.rb +9 -0
- data/spec/tshield/request_spec.rb +43 -2
- data/tshield.gemspec +28 -22
- metadata +139 -67
- data/lib/tshield/assets/favicon.ico +0 -0
- data/lib/tshield/assets/javascripts/application.js +0 -0
- data/lib/tshield/assets/javascripts/bootstrap.min.js +0 -7
- data/lib/tshield/assets/javascripts/jquery.min.js +0 -4
- data/lib/tshield/assets/stylesheets/application.css +0 -49
- data/lib/tshield/assets/stylesheets/bootstrap-theme.min.css +0 -6
- data/lib/tshield/assets/stylesheets/bootstrap.min.css +0 -6
- data/lib/tshield/controllers/admin/requests.rb +0 -62
- data/lib/tshield/controllers/admin/sessions.rb +0 -40
- data/lib/tshield/views/admin/requests/index.haml +0 -6
- data/lib/tshield/views/admin/requests/show.haml +0 -25
- data/lib/tshield/views/admin/sessions/index.haml +0 -6
- data/lib/tshield/views/layout/base.haml +0 -15
data/lib/tshield/options.rb
CHANGED
@@ -1,21 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'optparse'
|
2
4
|
|
5
|
+
require 'tshield/logger'
|
3
6
|
require 'tshield/version'
|
4
7
|
|
5
8
|
module TShield
|
9
|
+
# Options for command line
|
6
10
|
class Options
|
7
|
-
|
8
11
|
attr_reader :debug
|
9
12
|
|
10
13
|
def self.init
|
11
|
-
|
14
|
+
@instance = TShield::Options.new
|
12
15
|
end
|
13
16
|
|
14
17
|
def self.instance
|
15
|
-
|
18
|
+
@instance || TShield::Options.new
|
16
19
|
end
|
17
20
|
|
18
21
|
def initialize
|
22
|
+
@options = {}
|
19
23
|
parse
|
20
24
|
end
|
21
25
|
|
@@ -23,42 +27,72 @@ module TShield
|
|
23
27
|
check_breakpoint(args)
|
24
28
|
end
|
25
29
|
|
30
|
+
def configuration_file
|
31
|
+
@options.fetch(:configuration_file, 'config/tshield.yml')
|
32
|
+
end
|
33
|
+
|
26
34
|
private
|
35
|
+
|
27
36
|
def check_breakpoint(args)
|
28
|
-
check_breakpoint_moment(args)
|
37
|
+
check_breakpoint_moment(args)
|
29
38
|
end
|
30
39
|
|
31
40
|
def check_breakpoint_moment(args)
|
32
41
|
@options["#{args[:moment]}_pattern".to_sym] =~ args[:path]
|
33
42
|
end
|
34
43
|
|
44
|
+
def register_before_pattern(opts)
|
45
|
+
opts.on('-b', '--break-before-request [PATTERN]',
|
46
|
+
'Breakpoint before request') do |pattern|
|
47
|
+
@options[:before_pattern] = Regexp.new(pattern)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def register_after_pattern(opts)
|
52
|
+
opts.on('-a', '--break-after-request [PATTERN]',
|
53
|
+
'Breakpoint after request') do |pattern|
|
54
|
+
@options[:after_pattern] = Regexp.new(pattern)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def register_configuration(opts)
|
59
|
+
opts.on('-c', '--configuration [FILE]',
|
60
|
+
'Configuration File') do |file|
|
61
|
+
@options[:configuration_file] = file
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def register_patterns(opts)
|
66
|
+
register_before_pattern(opts)
|
67
|
+
register_after_pattern(opts)
|
68
|
+
end
|
69
|
+
|
70
|
+
def register_version(opts)
|
71
|
+
opts.on('-v', '--version', 'Show version') do
|
72
|
+
TShield.logger.info(TShield::Version.to_s)
|
73
|
+
exit
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def register_help(opts)
|
78
|
+
opts.on_tail('-h', '--help', 'Show this message') do
|
79
|
+
puts opts
|
80
|
+
exit
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
35
84
|
def parse
|
36
|
-
@options = {}
|
37
85
|
OptionParser.new do |opts|
|
38
|
-
opts.banner =
|
39
|
-
|
40
|
-
opts.on('-b', '--break-before-request [PATTERN]',
|
41
|
-
'Breakpoint before request') do |pattern|
|
42
|
-
@options[:before_pattern] = Regexp.new(pattern)
|
43
|
-
end
|
44
|
-
|
45
|
-
opts.on('-a', '--break-after-request [PATTERN]',
|
46
|
-
'Breakpoint after request') do |pattern|
|
47
|
-
@options[:after_pattern] = Regexp.new(pattern)
|
48
|
-
end
|
49
|
-
|
50
|
-
opts.on("-v", "--version", "Show version") do
|
51
|
-
puts TShield::Version
|
52
|
-
exit
|
53
|
-
end
|
54
|
-
|
55
|
-
opts.on_tail("-h", "--help", "Show this message") do
|
56
|
-
puts opts
|
57
|
-
exit
|
58
|
-
end
|
86
|
+
opts.banner = 'Usage: tshield [options]'
|
87
|
+
register(opts)
|
59
88
|
end.parse!
|
60
89
|
end
|
61
90
|
|
91
|
+
def register(opts)
|
92
|
+
register_configuration(opts)
|
93
|
+
register_patterns(opts)
|
94
|
+
register_version(opts)
|
95
|
+
register_help(opts)
|
96
|
+
end
|
62
97
|
end
|
63
98
|
end
|
64
|
-
|
data/lib/tshield/request.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'httparty'
|
2
4
|
require 'json'
|
3
5
|
require 'byebug'
|
@@ -10,29 +12,27 @@ require 'tshield/response'
|
|
10
12
|
require 'tshield/sessions'
|
11
13
|
|
12
14
|
module TShield
|
13
|
-
|
14
15
|
class Request
|
15
|
-
|
16
16
|
attr_reader :response
|
17
17
|
|
18
18
|
def initialize(path, options = {})
|
19
19
|
@path = path
|
20
|
-
@options = options
|
20
|
+
@options = options
|
21
21
|
@configuration = TShield::Configuration.singleton
|
22
|
-
@options[:timeout] =
|
23
|
-
@options[:verify] =
|
22
|
+
@options[:timeout] = @configuration.request['timeout']
|
23
|
+
@options[:verify] = @configuration.request['verify_ssl']
|
24
24
|
request
|
25
25
|
end
|
26
26
|
|
27
27
|
def request
|
28
|
-
|
28
|
+
unless @options[:raw_query].nil? || @options[:raw_query].empty?
|
29
29
|
@path = "#{@path}?#{@options[:raw_query]}"
|
30
30
|
end
|
31
31
|
|
32
32
|
@url = "#{domain}#{@path}"
|
33
33
|
|
34
34
|
if exists
|
35
|
-
@response = get_current_response
|
35
|
+
@response = get_current_response
|
36
36
|
@response.original = false
|
37
37
|
else
|
38
38
|
@method = method
|
@@ -40,7 +40,7 @@ module TShield
|
|
40
40
|
@method, @url, @options = filter.new.filter(@method, @url, @options)
|
41
41
|
end
|
42
42
|
|
43
|
-
raw = HTTParty.send(
|
43
|
+
raw = HTTParty.send(@method.to_s, @url, @options)
|
44
44
|
|
45
45
|
@configuration.get_after_filters(domain).each do |filter|
|
46
46
|
raw = filter.new.filter(raw)
|
@@ -56,6 +56,7 @@ module TShield
|
|
56
56
|
end
|
57
57
|
|
58
58
|
private
|
59
|
+
|
59
60
|
def domain
|
60
61
|
@domain ||= @configuration.get_domain_for(@path)
|
61
62
|
end
|
@@ -70,14 +71,12 @@ module TShield
|
|
70
71
|
|
71
72
|
def save(raw_response)
|
72
73
|
headers = {}
|
73
|
-
raw_response.headers.each do |k,v|
|
74
|
-
|
75
|
-
headers[k] = v
|
76
|
-
end
|
74
|
+
raw_response.headers.each do |k, v|
|
75
|
+
headers[k] = v unless @configuration.not_save_headers(domain).include? k
|
77
76
|
end
|
78
77
|
|
79
78
|
content = {
|
80
|
-
body: raw_response.body,
|
79
|
+
body: raw_response.body,
|
81
80
|
status: raw_response.code,
|
82
81
|
headers: headers
|
83
82
|
}
|
@@ -102,7 +101,7 @@ module TShield
|
|
102
101
|
def file_exists
|
103
102
|
session = current_session
|
104
103
|
@content_idx = session ? session[:counter].current(@path, method) : 0
|
105
|
-
File.
|
104
|
+
File.exist?(destiny)
|
106
105
|
end
|
107
106
|
|
108
107
|
def exists
|
@@ -110,7 +109,7 @@ module TShield
|
|
110
109
|
end
|
111
110
|
|
112
111
|
def get_current_response
|
113
|
-
TShield::Response.new(content['body'], content['headers'] || [], content['status'] || 200)
|
112
|
+
TShield::Response.new(content['body'], content['headers'] || [], content['status'] || 200)
|
114
113
|
end
|
115
114
|
|
116
115
|
def key
|
@@ -119,22 +118,23 @@ module TShield
|
|
119
118
|
|
120
119
|
def destiny(iscontent = false)
|
121
120
|
request_path = File.join('requests')
|
122
|
-
Dir.mkdir(request_path) unless File.
|
121
|
+
Dir.mkdir(request_path) unless File.exist?(request_path)
|
123
122
|
|
124
|
-
|
123
|
+
session = current_session
|
124
|
+
if session
|
125
125
|
request_path = File.join(request_path, session[:name])
|
126
|
-
Dir.mkdir(request_path) unless File.
|
126
|
+
Dir.mkdir(request_path) unless File.exist?(request_path)
|
127
127
|
end
|
128
128
|
|
129
129
|
name_path = File.join(request_path, name)
|
130
|
-
Dir.mkdir(name_path) unless File.
|
130
|
+
Dir.mkdir(name_path) unless File.exist?(name_path)
|
131
131
|
|
132
132
|
path_path = File.join(name_path, safe_dir(@path))
|
133
|
-
Dir.mkdir(path_path) unless File.
|
133
|
+
Dir.mkdir(path_path) unless File.exist?(path_path)
|
134
134
|
|
135
135
|
method_path = File.join(path_path, method)
|
136
|
-
Dir.mkdir(method_path) unless File.
|
137
|
-
|
136
|
+
Dir.mkdir(method_path) unless File.exist?(method_path)
|
137
|
+
|
138
138
|
destiny_name = iscontent ? "#{@content_idx}.content" : "#{@content_idx}.json"
|
139
139
|
File.join(method_path, destiny_name)
|
140
140
|
end
|
@@ -156,14 +156,11 @@ module TShield
|
|
156
156
|
def safe_dir(url)
|
157
157
|
if url.size > 225
|
158
158
|
path = url.gsub(/(\?.*)/, '')
|
159
|
-
params = Digest::SHA1.hexdigest
|
160
|
-
"#{path.gsub(
|
159
|
+
params = Digest::SHA1.hexdigest Regexp.last_match(1)
|
160
|
+
"#{path.gsub(%r{/}, '-').gsub(/^-/, '')}?#{params}"
|
161
161
|
else
|
162
|
-
url.gsub(
|
162
|
+
url.gsub(%r{/}, '-').gsub(/^-/, '')
|
163
163
|
end
|
164
164
|
end
|
165
|
-
|
166
165
|
end
|
167
|
-
|
168
166
|
end
|
169
|
-
|
data/lib/tshield/response.rb
CHANGED
data/lib/tshield/server.rb
CHANGED
@@ -1,22 +1,35 @@
|
|
1
|
+
# frozen_string_literal: false
|
2
|
+
|
1
3
|
require 'sinatra'
|
2
4
|
require 'haml'
|
3
5
|
|
4
6
|
require 'tshield/controllers/requests'
|
5
7
|
require 'tshield/controllers/sessions'
|
6
8
|
|
7
|
-
require 'tshield/controllers/admin/requests'
|
8
|
-
require 'tshield/controllers/admin/sessions'
|
9
|
-
|
10
9
|
module TShield
|
10
|
+
# Base of TShield Server
|
11
11
|
class Server < Sinatra::Base
|
12
|
-
|
13
12
|
include TShield::Controllers::Requests::Helpers
|
14
|
-
include TShield::Controllers::Admin::Sessions::Helpers
|
15
|
-
include TShield::Controllers::Admin::Requests::Helpers
|
16
13
|
|
17
|
-
|
14
|
+
set :protection, except: [:json_csrf]
|
15
|
+
set :public_dir, File.join(File.dirname(__FILE__), 'assets')
|
16
|
+
set :views, File.join(File.dirname(__FILE__), 'views')
|
17
|
+
set :bind, '0.0.0.0'
|
18
|
+
|
19
|
+
def self.register_resources
|
20
|
+
load_controllers
|
21
|
+
register TShield::Controllers::Sessions
|
22
|
+
register TShield::Controllers::Requests
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.load_controllers
|
26
|
+
return unless File.exist?('controllers')
|
27
|
+
|
18
28
|
Dir.entries('controllers').each do |entry|
|
29
|
+
require 'byebug'
|
30
|
+
debugger
|
19
31
|
next if entry =~ /^\.\.?$/
|
32
|
+
|
20
33
|
entry.gsub!('.rb', '')
|
21
34
|
require File.join('.', 'controllers', entry)
|
22
35
|
controller_name = entry.split('_').collect(&:capitalize).join
|
@@ -25,17 +38,9 @@ module TShield
|
|
25
38
|
end
|
26
39
|
end
|
27
40
|
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
register TShield::Controllers::Admin::Sessions
|
34
|
-
register TShield::Controllers::Admin::Requests
|
35
|
-
|
36
|
-
register TShield::Controllers::Sessions
|
37
|
-
register TShield::Controllers::Requests
|
38
|
-
|
41
|
+
def self.run!
|
42
|
+
register_resources
|
43
|
+
super.run!
|
44
|
+
end
|
39
45
|
end
|
40
46
|
end
|
41
|
-
|
data/lib/tshield/sessions.rb
CHANGED
@@ -1,10 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'byebug'
|
2
4
|
require 'tshield/counter'
|
3
5
|
|
4
6
|
module TShield
|
7
|
+
# Manage sessions
|
8
|
+
#
|
9
|
+
# Start and stop session for ip
|
5
10
|
module Sessions
|
6
11
|
def self.start(ip, name)
|
7
|
-
sessions[normalize_ip(ip)] = {name: name, counter: TShield::Counter.new}
|
12
|
+
sessions[normalize_ip(ip)] = { name: name, counter: TShield::Counter.new }
|
8
13
|
end
|
9
14
|
|
10
15
|
def self.stop(ip)
|
@@ -15,7 +20,6 @@ module TShield
|
|
15
20
|
sessions[normalize_ip(ip)]
|
16
21
|
end
|
17
22
|
|
18
|
-
protected
|
19
23
|
def self.sessions
|
20
24
|
@sessions ||= {}
|
21
25
|
end
|
@@ -23,7 +27,5 @@ module TShield
|
|
23
27
|
def self.normalize_ip(ip)
|
24
28
|
ip == '::1' ? '127.0.0.1' : ip
|
25
29
|
end
|
26
|
-
|
27
30
|
end
|
28
31
|
end
|
29
|
-
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'socket'
|
2
4
|
|
3
5
|
module TShield
|
@@ -6,7 +8,7 @@ module TShield
|
|
6
8
|
@running = true
|
7
9
|
end
|
8
10
|
|
9
|
-
def on_connect(
|
11
|
+
def on_connect(_client)
|
10
12
|
raise 'should implement method on_connect'
|
11
13
|
end
|
12
14
|
|
@@ -24,4 +26,3 @@ module TShield
|
|
24
26
|
end
|
25
27
|
end
|
26
28
|
end
|
27
|
-
|
data/lib/tshield/version.rb
CHANGED
data/lib/tshield.rb
CHANGED
data/spec/spec_helper.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: false
|
2
|
+
|
1
3
|
require 'bundler/setup'
|
2
4
|
Bundler.setup
|
3
5
|
|
@@ -5,14 +7,12 @@ require 'simplecov'
|
|
5
7
|
SimpleCov.start
|
6
8
|
|
7
9
|
require 'httparty'
|
8
|
-
require 'tshield'
|
9
|
-
|
10
10
|
require 'webmock/rspec'
|
11
11
|
|
12
12
|
RSpec.configure do |config|
|
13
|
-
|
14
|
-
|
13
|
+
config.before(:each) do
|
14
|
+
allow(File).to receive(:join).and_return(
|
15
|
+
'spec/tshield/fixtures/config/tshield.yml'
|
16
|
+
)
|
15
17
|
end
|
16
|
-
|
17
18
|
end
|
18
|
-
|
@@ -1,32 +1,69 @@
|
|
1
|
+
# frozen_string_literal: false
|
2
|
+
|
3
|
+
require 'tshield/configuration'
|
1
4
|
require 'spec_helper'
|
2
5
|
|
3
6
|
describe TShield::Configuration do
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
7
|
+
context 'on config exist' do
|
8
|
+
before :each do
|
9
|
+
options_instance = double
|
10
|
+
allow(options_instance).to receive(:configuration_file)
|
11
|
+
.and_return('spec/tshield/fixtures/config/tshield.yml')
|
12
|
+
allow(TShield::Options).to receive(:instance).and_return(options_instance)
|
13
|
+
allow(File).to receive(:join).and_return(
|
14
|
+
'./spec/tshield/fixtures/filters/example_filter.rb'
|
15
|
+
)
|
16
|
+
allow(File).to receive(:exist?) do
|
17
|
+
true
|
18
|
+
end
|
19
|
+
allow(Dir).to receive(:entries) do
|
20
|
+
['.', '..', 'example_filter.rb']
|
21
|
+
end
|
22
|
+
@configuration = TShield::Configuration.singleton
|
14
23
|
end
|
15
|
-
end
|
16
24
|
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
25
|
+
context 'load configurations from yaml' do
|
26
|
+
it 'recover domains' do
|
27
|
+
expect(@configuration.domains['example.org']['paths']).to(
|
28
|
+
include('/api/one', '/api/two')
|
29
|
+
)
|
30
|
+
end
|
21
31
|
|
22
|
-
|
23
|
-
|
32
|
+
context 'on load filters' do
|
33
|
+
it 'recover filters for a domain' do
|
34
|
+
expect(@configuration.get_filters('example.org')).to eq([ExampleFilter])
|
35
|
+
end
|
36
|
+
it 'return empty array if domain not have filters' do
|
37
|
+
expect(@configuration.get_filters('example.com')).to eq([])
|
38
|
+
end
|
39
|
+
end
|
24
40
|
end
|
25
41
|
|
26
|
-
|
27
|
-
|
42
|
+
describe 'get_domain_for' do
|
43
|
+
it 'return domain for example.org' do
|
44
|
+
expect(@configuration.get_domain_for('/api/two')).to eq('example.org')
|
45
|
+
end
|
46
|
+
|
47
|
+
it 'return domain for example.com' do
|
48
|
+
expect(@configuration.get_domain_for('/api/three')).to eq('example.com')
|
49
|
+
end
|
50
|
+
|
51
|
+
it 'return nil if domain not found' do
|
52
|
+
expect(@configuration.get_domain_for('/api/four')).to be_nil
|
53
|
+
end
|
28
54
|
end
|
29
55
|
end
|
56
|
+
context 'on config not exist' do
|
57
|
+
before :each do
|
58
|
+
options_instance = double
|
59
|
+
allow(options_instance).to receive(:configuration_file)
|
60
|
+
.and_return('not_found/config/tshield.yml')
|
61
|
+
allow(TShield::Options).to receive(:instance).and_return(options_instance)
|
62
|
+
TShield::Configuration.clear
|
63
|
+
end
|
30
64
|
|
65
|
+
it 'exit with error status' do
|
66
|
+
expect { TShield::Configuration.singleton }.to raise_error RuntimeError
|
67
|
+
end
|
68
|
+
end
|
31
69
|
end
|
32
|
-
|
@@ -1,11 +1,52 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'spec_helper'
|
2
4
|
|
5
|
+
require 'tshield/request'
|
6
|
+
|
3
7
|
describe TShield::Request do
|
8
|
+
before :each do
|
9
|
+
configuration = double
|
10
|
+
allow(TShield::Configuration)
|
11
|
+
.to receive(:singleton).and_return(configuration)
|
12
|
+
allow(configuration).to receive(:get_before_filters).and_return([])
|
13
|
+
allow(configuration).to receive(:get_after_filters).and_return([])
|
14
|
+
allow(configuration).to receive(:request).and_return('timeout' => 10)
|
15
|
+
allow(configuration).to receive(:get_domain_for).and_return('example.org')
|
16
|
+
allow(TShield::Options).to receive_message_chain(:instance, :break?)
|
17
|
+
end
|
4
18
|
|
5
19
|
describe 'when save response' do
|
6
|
-
it '' do
|
20
|
+
it 'should write response body, request status and headers' do
|
21
|
+
allow_any_instance_of(TShield::Request).to receive(:exists)
|
22
|
+
.and_return(false)
|
23
|
+
allow_any_instance_of(TShield::Request).to receive(:destiny)
|
24
|
+
allow(HTTParty).to receive(:send).and_return(RawResponse.new)
|
25
|
+
|
26
|
+
write_spy = double
|
27
|
+
allow(File).to receive(:open).and_return(write_spy)
|
28
|
+
|
29
|
+
expect(write_spy).to receive(:write).ordered.with('this is the body')
|
30
|
+
expect(write_spy).to receive(:write)
|
31
|
+
.ordered
|
32
|
+
.with("{\n \"status\": 200,\n \"headers\": {\n }\n}")
|
33
|
+
allow(write_spy).to receive(:close)
|
34
|
+
|
35
|
+
TShield::Request.new '/', method: 'GET'
|
7
36
|
end
|
8
37
|
end
|
9
38
|
|
10
|
-
|
39
|
+
class RawResponse
|
40
|
+
def headers
|
41
|
+
[]
|
42
|
+
end
|
43
|
+
|
44
|
+
def body
|
45
|
+
'this is the body'
|
46
|
+
end
|
11
47
|
|
48
|
+
def code
|
49
|
+
200
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|