spectre-core 1.9.0 → 1.12.1

Sign up to get free protection for your applications and to get access to all the features.
data/lib/spectre/http.rb CHANGED
@@ -1,359 +1,373 @@
1
- require 'net/http'
2
- require 'openssl'
3
- require 'json'
4
- require 'securerandom'
5
- require 'logger'
6
- require 'ostruct'
7
-
8
-
9
- module Spectre
10
- module Http
11
- DEFAULT_HTTP_CONFIG = {
12
- 'method' => 'GET',
13
- 'path' => '',
14
- 'host' => nil,
15
- 'port' => 80,
16
- 'scheme' => 'http',
17
- 'use_ssl' => false,
18
- 'cert' => nil,
19
- 'headers' => nil,
20
- 'query' => nil,
21
- 'content_type' => '',
22
- }
23
-
24
- @@modules = []
25
-
26
- class SpectreHttpRequest < Spectre::DslClass
27
- def initialize request
28
- @__req = request
29
- end
30
-
31
- def method method_name
32
- @__req['method'] = method_name.upcase
33
- end
34
-
35
- def url base_url
36
- @__req['base_url'] = base_url
37
- end
38
-
39
- def path url_path
40
- @__req['path'] = url_path
41
- end
42
-
43
- def header name, value
44
- @__req['headers'] = [] if not @__req['headers']
45
- @__req['headers'].append [name, value.to_s.strip]
46
- end
47
-
48
- def param name, value
49
- @__req['query'] = [] if not @__req['query']
50
- @__req['query'].append [name, value.to_s.strip]
51
- end
52
-
53
- def content_type media_type
54
- @__req['headers'] = [] if not @__req['headers']
55
- @__req['headers'].append ['Content-Type', media_type]
56
- end
57
-
58
- def json data
59
- data = data.to_h if data.is_a? OpenStruct
60
- body JSON.pretty_generate(data)
61
- content_type 'application/json'
62
- end
63
-
64
- def body body_content
65
- @__req['body'] = body_content.to_s
66
- end
67
-
68
- def ensure_success!
69
- @__req['ensure_success'] = true
70
- end
71
-
72
- def ensure_success?
73
- @__req['ensure_success']
74
- end
75
-
76
- def authenticate method
77
- @__req['auth'] = method
78
- end
79
-
80
- def certificate path
81
- @__req['cert'] = path
82
- use_ssl!
83
- end
84
-
85
- def use_ssl!
86
- @__req['use_ssl'] = true
87
- end
88
-
89
- def to_s
90
- @__req.to_s
91
- end
92
-
93
- alias_method :auth, :authenticate
94
- alias_method :cert, :certificate
95
- alias_method :media_type, :content_type
96
- end
97
-
98
- class SpectreHttpHeader
99
- def initialize headers
100
- @headers = headers || {}
101
- end
102
-
103
- def [] key
104
- return nil if not @headers.has_key?(key.downcase)
105
- @headers[key.downcase].first
106
- end
107
-
108
- def to_s
109
- @headers.to_s
110
- end
111
- end
112
-
113
- class SpectreHttpResponse
114
- def initialize res
115
- @res = res
116
- @data = nil
117
- @headers = SpectreHttpHeader.new @res[:headers]
118
- end
119
-
120
- def code
121
- @res[:code]
122
- end
123
-
124
- def message
125
- @res[:message]
126
- end
127
-
128
- def headers
129
- @headers
130
- end
131
-
132
- def body
133
- @res[:body]
134
- end
135
-
136
- def json
137
- return nil if not @res[:body]
138
-
139
- if @data == nil
140
- begin
141
- @data = JSON.parse(@res[:body], object_class: OpenStruct)
142
- rescue
143
- raise "Body content is not a valid JSON:\n#{@res[:body]}"
144
- end
145
- end
146
-
147
- @data
148
- end
149
-
150
- def success?
151
- @res[:code] < 400
152
- end
153
-
154
- def to_s
155
- @res.to_s
156
- end
157
-
158
- def pretty
159
- @res.pretty
160
- end
161
- end
162
-
163
-
164
- class << self
165
- @@http_cfg = {}
166
- @@response = nil
167
- @@request = nil
168
- @@modules = []
169
- @@secure_keys = []
170
-
171
- def https name, &block
172
- http(name, secure: true, &block)
173
- end
174
-
175
- def http name, secure: nil, &block
176
- req = {}
177
-
178
- if @@http_cfg.key? name
179
- req.merge! @@http_cfg[name]
180
- raise "No `base_url' set for HTTP client '#{name}'. Check your HTTP config in your environment." if !req['base_url']
181
- else
182
- req['base_url'] = name
183
- end
184
-
185
- req['user_ssl'] = secure if secure != nil
186
-
187
- SpectreHttpRequest.new(req).instance_eval(&block) if block_given?
188
-
189
- invoke(req)
190
- end
191
-
192
- def request
193
- raise 'No request has been invoked yet' unless @@request
194
- @@request
195
- end
196
-
197
- def response
198
- raise 'There is no response. No request has been invoked yet.' unless @@response
199
- @@response
200
- end
201
-
202
- def register mod
203
- raise 'Module must not be nil' unless mod
204
- @@modules << mod
205
- end
206
-
207
- private
208
-
209
- def try_format_json str, pretty: false
210
- return str unless str or str.empty?
211
-
212
- begin
213
- json = JSON.parse(str)
214
- json.obfuscate!(@@secure_keys) if not @@debug
215
-
216
- if pretty
217
- str = JSON.pretty_generate(json)
218
- else
219
- str = JSON.dump(json)
220
- end
221
- # rescue => e
222
- # # do nothing
223
- # @@logger.error(e)
224
- end
225
-
226
- str
227
- end
228
-
229
- def is_secure? key
230
- @@secure_keys.any? { |x| key.to_s.downcase.include? x.downcase }
231
- end
232
-
233
- def header_to_s headers
234
- s = ''
235
- headers.each_header.each do |header, value|
236
- value = '*****' if is_secure?(header) and not @@debug
237
- s += "#{header.to_s.ljust(30, '.')}: #{value.to_s}\n"
238
- end
239
- s
240
- end
241
-
242
- def invoke req
243
- @@request = nil
244
-
245
- if req['cert'] or req['use_ssl']
246
- scheme = 'https'
247
- else
248
- scheme = 'http'
249
- end
250
-
251
- base_url = req['base_url']
252
-
253
- if not base_url.match /http(?:s)?:\/\//
254
- base_url = scheme + '://' + base_url
255
- end
256
-
257
- if req['path']
258
- base_url = base_url + '/' if not base_url.end_with? '/'
259
- base_url += req['path']
260
- end
261
-
262
- uri = URI(base_url)
263
-
264
- raise "'#{uri}' is not a valid uri" if not uri.host
265
-
266
- uri.query = URI.encode_www_form(req['query']) unless not req['query'] or req['query'].empty?
267
-
268
- net_http = Net::HTTP.new(uri.host, uri.port)
269
-
270
- if uri.scheme == 'https'
271
- net_http.use_ssl = true
272
-
273
- if req.key? 'cert'
274
- raise "Certificate '#{req['cert']}' does not exist" unless File.exists? req['cert']
275
- net_http.verify_mode = OpenSSL::SSL::VERIFY_PEER
276
- net_http.ca_file = req['cert']
277
- else
278
- net_http.verify_mode = OpenSSL::SSL::VERIFY_NONE
279
- end
280
- end
281
-
282
- net_req = Net::HTTPGenericRequest.new(req['method'], true, true, uri)
283
- net_req.body = req['body']
284
- net_req.content_type = req['content_type'] if req['content_type'] and not req['content_type'].empty?
285
-
286
- if req['headers']
287
- req['headers'].each do |header|
288
- net_req[header[0]] = header[1]
289
- end
290
- end
291
-
292
- req_id = SecureRandom.uuid()[0..5]
293
-
294
- # Run HTTP modules
295
-
296
- @@modules.each do |mod|
297
- mod.on_req(net_http, net_req, req) if mod.respond_to? :on_req
298
- end
299
-
300
- # Log request
301
-
302
- req_log = "[>] #{req_id} #{req['method']} #{uri}\n"
303
- req_log += header_to_s(net_req)
304
- req_log += try_format_json(req['body'], pretty: true) if req['body'] != nil and not req['body'].empty?
305
-
306
- @@logger.info req_log
307
-
308
- # Request
309
-
310
- start_time = Time.now
311
- net_res = net_http.request(net_req)
312
- end_time = Time.now
313
-
314
- # Run HTTP modules
315
-
316
- @@modules.each do |mod|
317
- mod.on_res(net_http, net_res, req) if mod.respond_to? :on_res
318
- end
319
-
320
- # Log response
321
-
322
- res_log = "[<] #{req_id} #{net_res.code} #{net_res.message} (#{end_time - start_time}s)\n"
323
- res_log += header_to_s(net_res)
324
- res_log += try_format_json(net_res.body, pretty: true) if net_res.body != nil and !net_res.body.empty?
325
-
326
- @@logger.info(res_log)
327
-
328
- if req['ensure_success']
329
- code = Integer(net_res.code)
330
- fail "Response code of #{req_id} did not indicate success: #{net_res.code} #{net_res.message}" if code >= 400
331
- end
332
-
333
- @@request = OpenStruct.new(req)
334
- @@response = SpectreHttpResponse.new({
335
- code: net_res.code.to_i,
336
- message: net_res.message,
337
- headers: net_res.to_hash,
338
- body: net_res.body
339
- })
340
- end
341
- end
342
-
343
- Spectre.register do |config|
344
- @@logger = ::Logger.new config['log_file'], progname: 'spectre/http'
345
- @@secure_keys = config['secure_keys'] || []
346
- @@debug = config['debug']
347
-
348
- if config.key? 'http'
349
- @@http_cfg = {}
350
-
351
- config['http'].each do |name, cfg|
352
- @@http_cfg[name] = cfg
353
- end
354
- end
355
- end
356
-
357
- Spectre.delegate :http, :https, :request, :response, to: self
358
- end
359
- end
1
+ require_relative '../spectre'
2
+
3
+ require 'net/http'
4
+ require 'openssl'
5
+ require 'json'
6
+ require 'securerandom'
7
+ require 'logger'
8
+ require 'ostruct'
9
+
10
+ module Spectre
11
+ module Http
12
+ DEFAULT_HTTP_CONFIG = {
13
+ 'method' => 'GET',
14
+ 'path' => '',
15
+ 'host' => nil,
16
+ 'port' => 80,
17
+ 'scheme' => 'http',
18
+ 'use_ssl' => false,
19
+ 'cert' => nil,
20
+ 'headers' => nil,
21
+ 'query' => nil,
22
+ 'content_type' => '',
23
+ 'timeout' => 180,
24
+ }.freeze
25
+
26
+ @@modules = []
27
+
28
+ class HttpError < Exception
29
+ def initialize message
30
+ super message
31
+ end
32
+ end
33
+
34
+ class SpectreHttpRequest < Spectre::DslClass
35
+ class Headers
36
+ CONTENT_TYPE = 'Content-Type'
37
+ UNIQUE_HEADERS = [CONTENT_TYPE].freeze
38
+ end
39
+
40
+ def initialize request
41
+ @__req = request
42
+ end
43
+
44
+ def method method_name
45
+ @__req['method'] = method_name.upcase
46
+ end
47
+
48
+ def url base_url
49
+ @__req['base_url'] = base_url
50
+ end
51
+
52
+ def path url_path
53
+ @__req['path'] = url_path
54
+ end
55
+
56
+ def timeout seconds
57
+ @__req['timeout'] = seconds
58
+ end
59
+
60
+ def header name, value
61
+ @__req['headers'] ||= []
62
+ @__req['headers'].append [name, value.to_s.strip]
63
+ end
64
+
65
+ def param name, value
66
+ @__req['query'] ||= []
67
+ @__req['query'].append [name, value.to_s.strip]
68
+ end
69
+
70
+ def content_type media_type
71
+ @__req['content_type'] = media_type
72
+ end
73
+
74
+ def json data
75
+ data = data.to_h if data.is_a? OpenStruct
76
+ body JSON.pretty_generate(data)
77
+
78
+ # TODO: Only set content type, if not explicitly set
79
+ content_type('application/json')
80
+ end
81
+
82
+ def body body_content
83
+ @__req['body'] = body_content.to_s
84
+ end
85
+
86
+ def ensure_success!
87
+ @__req['ensure_success'] = true
88
+ end
89
+
90
+ def ensure_success?
91
+ @__req['ensure_success']
92
+ end
93
+
94
+ def authenticate method
95
+ @__req['auth'] = method
96
+ end
97
+
98
+ def no_auth!
99
+ @__req['auth'] = 'none'
100
+ end
101
+
102
+ def certificate path
103
+ @__req['cert'] = path
104
+ end
105
+
106
+ def use_ssl!
107
+ @__req['use_ssl'] = true
108
+ end
109
+
110
+ def to_s
111
+ @__req.to_s
112
+ end
113
+
114
+ alias_method :auth, :authenticate
115
+ alias_method :cert, :certificate
116
+ alias_method :media_type, :content_type
117
+ end
118
+
119
+ class SpectreHttpHeader
120
+ def initialize headers
121
+ @headers = headers || {}
122
+ end
123
+
124
+ def [] key
125
+ return nil unless @headers.key?(key.downcase)
126
+
127
+ @headers[key.downcase].first
128
+ end
129
+
130
+ def to_s
131
+ @headers.to_s
132
+ end
133
+ end
134
+
135
+ class SpectreHttpResponse
136
+ attr_reader :code, :message, :headers, :body
137
+
138
+ def initialize net_res
139
+ @code = net_res.code.to_i
140
+ @message = net_res.message
141
+ @body = net_res.body
142
+ @headers = SpectreHttpHeader.new(net_res.to_hash)
143
+ @json_data = nil
144
+ end
145
+
146
+ def json
147
+ if !@body.nil? and @json_data.nil?
148
+ begin
149
+ @json_data = JSON.parse(@body, object_class: OpenStruct)
150
+ rescue JSON::ParserError
151
+ raise HttpError.new("Body content is not a valid JSON:\n#{@body}")
152
+ end
153
+ end
154
+
155
+ @json_data
156
+ end
157
+
158
+ def success?
159
+ @code < 400
160
+ end
161
+ end
162
+
163
+
164
+ class << self
165
+ @@http_cfg = {}
166
+ @@response = nil
167
+ @@request = nil
168
+ @@modules = []
169
+ @@secure_keys = []
170
+
171
+ def https name, &block
172
+ http(name, secure: true, &block)
173
+ end
174
+
175
+ def http name, secure: false, &block
176
+ req = {}
177
+
178
+ if @@http_cfg.key? name
179
+ req.deep_merge! @@http_cfg[name].deep_clone
180
+ raise HttpError.new("No `base_url' set for HTTP client '#{name}'. Check your HTTP config in your environment.") unless req['base_url']
181
+ else
182
+ req['base_url'] = name
183
+ end
184
+
185
+ req['use_ssl'] = secure unless secure.nil?
186
+
187
+ SpectreHttpRequest.new(req)._evaluate(&block) if block_given?
188
+
189
+ invoke(req)
190
+ end
191
+
192
+ def request
193
+ raise 'No request has been invoked yet' unless @@request
194
+
195
+ @@request
196
+ end
197
+
198
+ def response
199
+ raise 'There is no response. No request has been invoked yet.' unless @@response
200
+
201
+ @@response
202
+ end
203
+
204
+ def register mod
205
+ raise 'Module must not be nil' unless mod
206
+
207
+ @@modules << mod
208
+ end
209
+
210
+ private
211
+
212
+ def try_format_json str, pretty: false
213
+ return str unless str or str.empty?
214
+
215
+ begin
216
+ json = JSON.parse(str)
217
+ json.obfuscate!(@@secure_keys) unless @@debug
218
+
219
+ if pretty
220
+ str = JSON.pretty_generate(json)
221
+ else
222
+ str = JSON.dump(json)
223
+ end
224
+ rescue
225
+ # do nothing
226
+ end
227
+
228
+ str
229
+ end
230
+
231
+ def secure? key
232
+ @@secure_keys.any? { |x| key.to_s.downcase.include? x.downcase }
233
+ end
234
+
235
+ def header_to_s headers
236
+ s = ''
237
+ headers.each_header.each do |header, value|
238
+ value = '*****' if secure?(header) and not @@debug
239
+ s += "#{header.to_s.ljust(30, '.')}: #{value.to_s}\n"
240
+ end
241
+ s
242
+ end
243
+
244
+ def invoke req
245
+ @@request = nil
246
+
247
+ # Build URI
248
+
249
+ scheme = req['use_ssl'] ? 'https' : 'http'
250
+ base_url = req['base_url']
251
+
252
+ unless base_url.match /http(?:s)?:\/\//
253
+ base_url = scheme + '://' + base_url
254
+ end
255
+
256
+ if req['path']
257
+ base_url = base_url + '/' unless base_url.end_with? '/'
258
+ base_url += req['path']
259
+ end
260
+
261
+ uri = URI(base_url)
262
+
263
+ raise HttpError.new("'#{uri}' is not a valid uri") unless uri.host
264
+
265
+ # Build query parameters
266
+
267
+ uri.query = URI.encode_www_form(req['query']) unless not req['query'] or req['query'].empty?
268
+
269
+ # Create HTTP client
270
+
271
+ net_http = Net::HTTP.new(uri.host, uri.port)
272
+ net_http.read_timeout = req['timeout']
273
+
274
+ if uri.scheme == 'https'
275
+ net_http.use_ssl = true
276
+
277
+ if req.key? 'cert'
278
+ raise HttpError.new("Certificate '#{req['cert']}' does not exist") unless File.exists? req['cert']
279
+
280
+ net_http.verify_mode = OpenSSL::SSL::VERIFY_PEER
281
+ net_http.ca_file = req['cert']
282
+ else
283
+ net_http.verify_mode = OpenSSL::SSL::VERIFY_NONE
284
+ end
285
+ end
286
+
287
+ # Create HTTP Request
288
+
289
+ net_req = Net::HTTPGenericRequest.new(req['method'], true, true, uri)
290
+ net_req.body = req['body']
291
+ net_req.content_type = req['content_type'] if req['content_type'] and not req['content_type'].empty?
292
+
293
+ if req['headers']
294
+ req['headers'].each do |header|
295
+ net_req[header[0]] = header[1]
296
+ end
297
+ end
298
+
299
+ req_id = SecureRandom.uuid()[0..5]
300
+
301
+ # Run HTTP modules
302
+
303
+ @@modules.each do |mod|
304
+ mod.on_req(net_http, net_req, req) if mod.respond_to? :on_req
305
+ end
306
+
307
+ # Log request
308
+
309
+ req_log = "[>] #{req_id} #{req['method']} #{uri}\n"
310
+ req_log += header_to_s(net_req)
311
+ req_log += try_format_json(req['body'], pretty: true) if req['body'] != nil and not req['body'].empty?
312
+
313
+ @@logger.info(req_log)
314
+
315
+ # Request
316
+
317
+ start_time = Time.now
318
+
319
+ begin
320
+ net_res = net_http.request(net_req)
321
+ rescue SocketError => e
322
+ raise HttpError.new("The request '#{req['method']} #{uri}' failed. Please check if the given URL '#{uri}' is valid and available or a corresponding HTTP config in the environment file exists. See log for more details. Original.\nOriginal error was: #{e.message}")
323
+ rescue Net::ReadTimeout
324
+ raise HttpError.new("HTTP timeout of #{net_http.read_timeout}s exceeded")
325
+ end
326
+
327
+ end_time = Time.now
328
+
329
+ req['started_at'] = start_time
330
+ req['finished_at'] = end_time
331
+
332
+ # Run HTTP modules
333
+
334
+ @@modules.each do |mod|
335
+ mod.on_res(net_http, net_res, req) if mod.respond_to? :on_res
336
+ end
337
+
338
+ # Log response
339
+
340
+ res_log = "[<] #{req_id} #{net_res.code} #{net_res.message} (#{end_time - start_time}s)\n"
341
+ res_log += header_to_s(net_res)
342
+ res_log += try_format_json(net_res.body, pretty: true) unless net_res.body.nil? or net_res.body.empty?
343
+
344
+ @@logger.info(res_log)
345
+
346
+ fail "Response code of #{req_id} did not indicate success: #{net_res.code} #{net_res.message}" if req['ensure_success'] and net_res.code.to_i >= 400
347
+
348
+ # Set global request and response variables
349
+
350
+ @@request = OpenStruct.new(req)
351
+ @@request.freeze
352
+
353
+ @@response = SpectreHttpResponse.new(net_res)
354
+ end
355
+ end
356
+
357
+ Spectre.register do |config|
358
+ @@logger = ::Logger.new(config['log_file'], progname: 'spectre/http')
359
+ @@secure_keys = config['secure_keys'] || []
360
+ @@debug = config['debug']
361
+
362
+ if config.key? 'http'
363
+ @@http_cfg = {}
364
+
365
+ config['http'].each do |name, cfg|
366
+ @@http_cfg[name] = cfg
367
+ end
368
+ end
369
+ end
370
+
371
+ Spectre.delegate :http, :https, :request, :response, to: self
372
+ end
373
+ end