spectre-core 1.10.0 → 1.12.2

Sign up to get free protection for your applications and to get access to all the features.
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