spectre-core 1.10.0 → 1.12.2

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