spectre-core 1.9.0 → 1.12.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.
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