spectre-core 1.12.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,364 +1,373 @@
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
+ }.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