spectre-core 1.12.0 → 1.12.3

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