spectre-core 1.8.4 → 1.12.0

Sign up to get free protection for your applications and to get access to all the features.
data/lib/spectre/curl.rb CHANGED
@@ -1,368 +1,397 @@
1
- require 'open3'
2
- require 'ostruct'
3
-
4
-
5
- module Spectre::Curl
6
- class SpectreHttpRequest < Spectre::DslClass
7
- def initialize request
8
- @__req = request
9
- end
10
-
11
- def method method_name
12
- @__req['method'] = method_name.upcase
13
- end
14
-
15
- def url base_url
16
- @__req['base_url'] = base_url
17
- end
18
-
19
- def path url_path
20
- @__req['path'] = url_path
21
- end
22
-
23
- def header name, value
24
- @__req['headers'] = [] if not @__req['headers']
25
- @__req['headers'].append [name, value.to_s.strip]
26
- end
27
-
28
- def param name, value
29
- @__req['query'] = [] if not @__req['query']
30
- @__req['query'].append [name, value.to_s.strip]
31
- end
32
-
33
- def content_type media_type
34
- @__req['headers'] = [] if not @__req['headers']
35
- @__req['headers'].append ['Content-Type', media_type]
36
- end
37
-
38
- def json data
39
- body JSON.pretty_generate(data)
40
- content_type 'application/json'
41
- end
42
-
43
- def body body_content
44
- @__req['body'] = body_content
45
- end
46
-
47
- def ensure_success!
48
- @__req['ensure_success'] = true
49
- end
50
-
51
- def ensure_success?
52
- @__req['ensure_success']
53
- end
54
-
55
- def authenticate method
56
- @__req['auth'] = method
57
- end
58
-
59
- def certificate path
60
- @__req['cert'] = path
61
- use_ssl!
62
- end
63
-
64
- def use_ssl!
65
- @__req['use_ssl'] = true
66
- end
67
-
68
- alias_method :auth, :authenticate
69
- alias_method :cert, :certificate
70
- alias_method :media_type, :content_type
71
- end
72
-
73
- class SpectreHttpHeader
74
- def initialize headers
75
- @headers = headers || {}
76
- end
77
-
78
- def [] key
79
- @headers[key.downcase]
80
- end
81
- end
82
-
83
- class SpectreHttpResponse
84
- def initialize res
85
- @res = res
86
- @data = nil
87
- end
88
-
89
- def code
90
- @res[:code]
91
- end
92
-
93
- def message
94
- @res[:message]
95
- end
96
-
97
- def protocol
98
- @res[:protocol]
99
- end
100
-
101
- def version
102
- @res[:version]
103
- end
104
-
105
- def headers
106
- SpectreHttpHeader.new @res[:headers]
107
- end
108
-
109
- def body
110
- @res[:body]
111
- end
112
-
113
- def json
114
- return nil if not @res[:body]
115
-
116
- if @data == nil
117
- begin
118
- @data = JSON.parse(@res[:body], object_class: OpenStruct)
119
- rescue
120
- raise 'invalid json'
121
- end
122
- end
123
-
124
- @data
125
- end
126
-
127
- def success?
128
- @res[:code] < 400
129
- end
130
-
131
- def pretty
132
- @res.pretty
133
- end
134
- end
135
-
136
- # DEFAULT_HTTP_REQUEST = {
137
- # 'method' => 'GET', # -X, --request <cmd>
138
- # 'base_url' => nil,
139
- # 'path' => nil,
140
- # 'headers' => nil, # -H, --header <header/@file>
141
- # 'query' => nil,
142
- # 'body' => nil, # -d, --data <data>
143
- # 'cert' => nil, # --cacert
144
- # 'follow' => false, # -L, --location
145
- # 'username' => nil, # -u, --user <user:password>
146
- # 'password' => nil,
147
- # 'use_ssl' => false, # -k
148
- # }
149
-
150
-
151
- class << self
152
- @@http_cfg = {}
153
- @@response = nil
154
- @@request = nil
155
- @@modules = []
156
-
157
- def curl name, secure: false, &block
158
- req = {
159
- 'use_ssl' => secure,
160
- }
161
-
162
- if @@http_cfg.key? name
163
- req.merge! @@http_cfg[name]
164
- raise "No `base_url' set for HTTP client '#{name}'. Check your HTTP config in your environment." if !req['base_url']
165
- else
166
- req['base_url'] = name
167
- end
168
-
169
- SpectreHttpRequest.new(req).instance_eval(&block) if block_given?
170
-
171
- invoke(req)
172
- end
173
-
174
- def curl_request
175
- raise 'No request has been invoked yet' unless @@request
176
- @@request
177
- end
178
-
179
- def curl_response
180
- raise 'There is no response. No request has been invoked yet.' unless @@response
181
- @@response
182
- end
183
-
184
- def register mod
185
- raise 'Module must not be nil' unless mod
186
- @@modules << mod
187
- end
188
-
189
- private
190
-
191
- def try_format_json str, pretty: false
192
- return str unless str or str.empty?
193
-
194
- begin
195
- json = JSON.parse str
196
-
197
- if pretty
198
- str = JSON.pretty_generate(json)
199
- else
200
- str = JSON.dump(json)
201
- end
202
- rescue
203
- # do nothing
204
- end
205
-
206
- str
207
- end
208
-
209
- def invoke req
210
- cmd = [@@curl_path]
211
-
212
- if req['cert'] or req['use_ssl']
213
- scheme = 'https'
214
- else
215
- scheme = 'http'
216
- end
217
-
218
- uri = req['base_url']
219
-
220
- if not uri.match /http(?:s)?:\/\//
221
- uri = scheme + '://' + uri
222
- end
223
-
224
- if req['path']
225
- uri += '/' if !uri.end_with? '/'
226
- uri += req['path']
227
- end
228
-
229
- if req['query']
230
- uri += '?'
231
- uri += req['query']
232
- .map { |x| x.join '='}
233
- .join '&'
234
- end
235
-
236
- cmd.append '"' + uri + '"'
237
- cmd.append '-X', req['method'] unless req['method'] == 'GET' or (req['body'] and req['method'] == 'POST')
238
-
239
- # Call all registered modules
240
- @@modules.each do |mod|
241
- mod.on_req(req, cmd) if mod.respond_to? :on_req
242
- end
243
-
244
- # Add headers to curl command
245
- req['headers'].each do |header|
246
- cmd.append '-H', '"' + header.join(':') + '"'
247
- end if req['headers']
248
-
249
- # Add request body
250
- if req['body'] != nil and not req['body'].empty?
251
- req_body = try_format_json(req['body']).gsub(/"/, '\\"')
252
- cmd.append '-d', '"' + req_body + '"'
253
- elsif ['POST', 'PUT', 'PATCH'].include? req['method'].upcase
254
- cmd.append '-d', '"\n"'
255
- end
256
-
257
- # Add certificate path if one if given
258
- if req['cert']
259
- raise "Certificate '#{req['cert']}' does not exist" unless File.exists? req['cert']
260
- cmd.append '--cacert', req['cert']
261
- elsif req['use_ssl'] or uri.start_with? 'https'
262
- cmd.append '-k'
263
- end
264
-
265
- cmd.append '-i'
266
- cmd.append '-v'
267
-
268
- @@request = OpenStruct.new req
269
-
270
- sys_cmd = cmd.join ' '
271
-
272
- @@logger.debug sys_cmd
273
-
274
- req_id = SecureRandom.uuid()[0..5]
275
-
276
- req_log = "[>] #{req_id} #{req['method']} #{uri}\n"
277
- req['headers'].each do |header|
278
- req_log += "#{header[0].to_s.ljust(30, '.')}: #{header[1].to_s}\n"
279
- end if req['headers']
280
- req_log += req['body'] if req['body'] != nil and not req['body'].empty?
281
-
282
- @@logger.info req_log
283
-
284
- start_time = Time.now
285
-
286
- stdin, stdout, stderr, wait_thr = Open3.popen3(sys_cmd)
287
-
288
- end_time = Time.now
289
-
290
- output = stdout.gets(nil)
291
- stdout.close
292
-
293
- debug_log = stderr.gets(nil)
294
- stderr.close
295
-
296
- # debug_log.lines.each { |x| @@logger.debug x unless x.empty? }
297
-
298
- raise "Unable to request #{uri}. Please check if this service is reachable." unless output
299
-
300
- @@logger.debug "[<] #{req_id} stdout:\n#{output}"
301
-
302
- header, body = output.split /\r?\n\r?\n/
303
-
304
- result = header.lines.first
305
-
306
- exit_code = wait_thr.value.exitstatus
307
-
308
- raise Exception.new "An error occured while executing curl:\n#{debug_log.lines.map { |x| not x.empty? }}" unless exit_code == 0
309
-
310
- # Parse protocol, version, status code and status message from response
311
- match = /^(?<protocol>[A-Za-z0-9]+)\/(?<version>\d+\.?\d*) (?<code>\d+) (?<message>.*)/.match result
312
-
313
- raise "Unexpected result from curl request:\n#{result}" unless match
314
-
315
- res_headers = header.lines[1..-1]
316
- .map { |x| /^(?<key>[A-Za-z0-9-]+):\s*(?<value>.*)$/.match x }
317
- .select { |x| x != nil }
318
- .map { |x| [x[:key].downcase, x[:value]] }
319
-
320
- res = {
321
- protocol: match[:protocol],
322
- version: match[:version],
323
- code: match[:code].to_i,
324
- message: match[:message],
325
- headers: Hash[res_headers],
326
- body: body
327
- }
328
-
329
- # Call all registered modules
330
- @@modules.each do |mod|
331
- mod.on_res(res, output) if mod.respond_to? :on_res
332
- end
333
-
334
- res_log = "[<] #{req_id} #{res[:code]} #{res[:message]} (#{end_time - start_time}s)\n"
335
- res_headers.each do |header|
336
- res_log += "#{header[0].to_s.ljust(30, '.')}: #{header[1].to_s}\n"
337
- end
338
-
339
- if res[:body] != nil and not res[:body].empty?
340
- res_log += try_format_json(res[:body], pretty: true)
341
- end
342
-
343
- @@logger.info res_log
344
-
345
- @@response = SpectreHttpResponse.new res
346
-
347
- raise "Response did not indicate success: #{@@response.code} #{@@response.message}" if req['ensure_success'] and not @@response.success?
348
-
349
- @@response
350
- end
351
- end
352
-
353
- Spectre.register do |config|
354
- @@logger = ::Logger.new config['log_file'], progname: 'spectre/curl'
355
-
356
- @@curl_path = config['curl_path'] || 'curl'
357
-
358
- if config.key? 'http'
359
- @@http_cfg = {}
360
-
361
- config['http'].each do |name, cfg|
362
- @@http_cfg[name] = cfg
363
- end
364
- end
365
- end
366
-
367
- Spectre.delegate :curl, :curl_response, :curl_request, to: self
368
- end
1
+ require_relative '../spectre'
2
+
3
+ require 'open3'
4
+ require 'ostruct'
5
+
6
+
7
+ module Spectre::Curl
8
+ class SpectreHttpRequest < Spectre::DslClass
9
+ def initialize request
10
+ @__req = request
11
+ end
12
+
13
+ def method method_name
14
+ @__req['method'] = method_name.upcase
15
+ end
16
+
17
+ def url base_url
18
+ @__req['base_url'] = base_url
19
+ end
20
+
21
+ def path url_path
22
+ @__req['path'] = url_path
23
+ end
24
+
25
+ def header name, value
26
+ @__req['headers'] = [] unless @__req['headers']
27
+ @__req['headers'].append [name, value.to_s.strip]
28
+ end
29
+
30
+ def param name, value
31
+ @__req['query'] = [] unless @__req['query']
32
+ @__req['query'].append [name, value.to_s.strip]
33
+ end
34
+
35
+ def content_type media_type
36
+ @__req['headers'] = [] unless @__req['headers']
37
+ @__req['headers'].append ['Content-Type', media_type]
38
+ end
39
+
40
+ def json data
41
+ body JSON.pretty_generate(data)
42
+ content_type 'application/json'
43
+ end
44
+
45
+ def body body_content
46
+ @__req['body'] = body_content
47
+ end
48
+
49
+ def ensure_success!
50
+ @__req['ensure_success'] = true
51
+ end
52
+
53
+ def ensure_success?
54
+ @__req['ensure_success']
55
+ end
56
+
57
+ def authenticate method
58
+ @__req['auth'] = method
59
+ end
60
+
61
+ def certificate path
62
+ @__req['cert'] = path
63
+ use_ssl!
64
+ end
65
+
66
+ def use_ssl!
67
+ @__req['use_ssl'] = true
68
+ end
69
+
70
+ alias_method :auth, :authenticate
71
+ alias_method :cert, :certificate
72
+ alias_method :media_type, :content_type
73
+ end
74
+
75
+ class SpectreHttpHeader
76
+ def initialize headers
77
+ @headers = headers || {}
78
+ end
79
+
80
+ def [] key
81
+ @headers[key.downcase]
82
+ end
83
+ end
84
+
85
+ class SpectreHttpResponse
86
+ def initialize res
87
+ @res = res
88
+ @data = nil
89
+ end
90
+
91
+ def code
92
+ @res[:code]
93
+ end
94
+
95
+ def message
96
+ @res[:message]
97
+ end
98
+
99
+ def protocol
100
+ @res[:protocol]
101
+ end
102
+
103
+ def version
104
+ @res[:version]
105
+ end
106
+
107
+ def headers
108
+ SpectreHttpHeader.new @res[:headers]
109
+ end
110
+
111
+ def body
112
+ @res[:body]
113
+ end
114
+
115
+ def json
116
+ return nil unless @res[:body]
117
+
118
+ if @data == nil
119
+ begin
120
+ @data = JSON.parse(@res[:body], object_class: OpenStruct)
121
+ rescue
122
+ raise 'invalid json'
123
+ end
124
+ end
125
+
126
+ @data
127
+ end
128
+
129
+ def success?
130
+ @res[:code] < 400
131
+ end
132
+
133
+ def pretty
134
+ @res.pretty
135
+ end
136
+ end
137
+
138
+ # DEFAULT_HTTP_REQUEST = {
139
+ # 'method' => 'GET', # -X, --request <cmd>
140
+ # 'base_url' => nil,
141
+ # 'path' => nil,
142
+ # 'headers' => nil, # -H, --header <header/@file>
143
+ # 'query' => nil,
144
+ # 'body' => nil, # -d, --data <data>
145
+ # 'cert' => nil, # --cacert
146
+ # 'follow' => false, # -L, --location
147
+ # 'username' => nil, # -u, --user <user:password>
148
+ # 'password' => nil,
149
+ # 'use_ssl' => false, # -k
150
+ # }
151
+
152
+
153
+ class << self
154
+ @@http_cfg = {}
155
+ @@response = nil
156
+ @@request = nil
157
+ @@modules = []
158
+
159
+ def curl name, secure: false, &block
160
+ req = {
161
+ 'use_ssl' => secure,
162
+ }
163
+
164
+ if @@http_cfg.key? name
165
+ req.merge! @@http_cfg[name]
166
+ raise "No `base_url' set for HTTP client '#{name}'. Check your HTTP config in your environment." unless req['base_url']
167
+ else
168
+ req['base_url'] = name
169
+ end
170
+
171
+ SpectreHttpRequest.new(req)._evaluate(&block) if block_given?
172
+
173
+ invoke(req)
174
+ end
175
+
176
+ def curl_request
177
+ raise 'No request has been invoked yet' unless @@request
178
+
179
+ @@request
180
+ end
181
+
182
+ def curl_response
183
+ raise 'There is no response. No request has been invoked yet.' unless @@response
184
+
185
+ @@response
186
+ end
187
+
188
+ def register mod
189
+ raise 'Module must not be nil' unless mod
190
+
191
+ @@modules << mod
192
+ end
193
+
194
+ private
195
+
196
+ def try_format_json str, pretty: false
197
+ return str unless str or str.empty?
198
+
199
+ begin
200
+ json = JSON.parse(str)
201
+ json.obfuscate!(@@secure_keys) unless @@debug
202
+
203
+ if pretty
204
+ str = JSON.pretty_generate(json)
205
+ else
206
+ str = JSON.dump(json)
207
+ end
208
+ rescue
209
+ # do nothing
210
+ end
211
+
212
+ str
213
+ end
214
+
215
+ def secure? key
216
+ @@secure_keys.any? { |x| key.to_s.downcase.include? x.downcase }
217
+ end
218
+
219
+ def header_to_s headers
220
+ s = ''
221
+
222
+ return s unless headers
223
+
224
+ headers.each do |header|
225
+ key = header[0].to_s
226
+ value = header[1].to_s
227
+ value = '*****' if secure?(key) and not @@debug
228
+ s += "#{key.ljust(30, '.')}: #{value}\n"
229
+ end
230
+
231
+ s
232
+ end
233
+
234
+ def invoke req
235
+ cmd = [@@curl_path]
236
+
237
+ if req['cert'] or req['use_ssl']
238
+ scheme = 'https'
239
+ else
240
+ scheme = 'http'
241
+ end
242
+
243
+ uri = req['base_url']
244
+
245
+ unless uri.match /http(?:s)?:\/\//
246
+ uri = scheme + '://' + uri
247
+ end
248
+
249
+ if req['path']
250
+ uri += '/' unless uri.end_with? '/'
251
+ uri += req['path']
252
+ end
253
+
254
+ if req['query']
255
+ uri += '?'
256
+ uri += req['query']
257
+ .map { |x| x.join '='}
258
+ .join('&')
259
+ end
260
+
261
+ cmd.append('"' + uri + '"')
262
+ cmd.append('-X', req['method']) unless req['method'] == 'GET' or (req['body'] and req['method'] == 'POST')
263
+
264
+ # Call all registered modules
265
+ @@modules.each do |mod|
266
+ mod.on_req(req, cmd) if mod.respond_to? :on_req
267
+ end
268
+
269
+ # Add headers to curl command
270
+ req['headers'].each do |header|
271
+ cmd.append('-H', '"' + header.join(':') + '"')
272
+ end if req['headers']
273
+
274
+ # Add request body
275
+ if req['body'] != nil and not req['body'].empty?
276
+ req_body = try_format_json(req['body']).gsub(/"/, '\\"')
277
+ cmd.append('-d', '"' + req_body + '"')
278
+ elsif ['POST', 'PUT', 'PATCH'].include? req['method'].upcase
279
+ cmd.append('-d', '"\n"')
280
+ end
281
+
282
+ # Add certificate path if one if given
283
+ if req['cert']
284
+ raise "Certificate '#{req['cert']}' does not exist" unless File.exists? req['cert']
285
+
286
+ cmd.append('--cacert', req['cert'])
287
+ elsif req['use_ssl'] or uri.start_with? 'https'
288
+ cmd.append('-k')
289
+ end
290
+
291
+ cmd.append('-i')
292
+ cmd.append('-v')
293
+
294
+ @@request = OpenStruct.new(req)
295
+
296
+ sys_cmd = cmd.join(' ')
297
+
298
+ @@logger.debug(sys_cmd)
299
+
300
+ req_id = SecureRandom.uuid()[0..5]
301
+
302
+ req_log = "[>] #{req_id} #{req['method']} #{uri}\n"
303
+ req_log += header_to_s(req['headers'])
304
+ req_log += try_format_json(req['body'], pretty: true)
305
+
306
+ @@logger.info(req_log)
307
+
308
+ start_time = Time.now
309
+
310
+ stdin, stdout, stderr, wait_thr = Open3.popen3(sys_cmd)
311
+
312
+ end_time = Time.now
313
+
314
+ output = stdout.gets(nil)
315
+ stdout.close
316
+
317
+ debug_log = stderr.gets(nil)
318
+ stderr.close
319
+
320
+ # debug_log.lines.each { |x| @@logger.debug x unless x.empty? }
321
+
322
+ raise "Unable to request #{uri}. Please check if this URL is correctly configured and reachable." unless output
323
+
324
+ @@logger.debug("[<] #{req_id} stdout:\n#{output}")
325
+
326
+ header, body = output.split /\r?\n\r?\n/
327
+
328
+ result = header.lines.first
329
+
330
+ exit_code = wait_thr.value.exitstatus
331
+
332
+ raise Exception.new "An error occured while executing curl:\n#{debug_log.lines.map { |x| not x.empty? }}" unless exit_code == 0
333
+
334
+ # Parse protocol, version, status code and status message from response
335
+ match = /^(?<protocol>[A-Za-z0-9]+)\/(?<version>\d+\.?\d*) (?<code>\d+) (?<message>.*)/.match result
336
+
337
+ raise "Unexpected result from curl request:\n#{result}" unless match
338
+
339
+ res_headers = header.lines[1..-1]
340
+ .map { |x| /^(?<key>[A-Za-z0-9-]+):\s*(?<value>.*)$/.match x }
341
+ .select { |x| x != nil }
342
+ .map { |x| [x[:key].downcase, x[:value]] }
343
+
344
+ res = {
345
+ protocol: match[:protocol],
346
+ version: match[:version],
347
+ code: match[:code].to_i,
348
+ message: match[:message],
349
+ headers: Hash[res_headers],
350
+ body: body,
351
+ }
352
+
353
+ # Call all registered modules
354
+ @@modules.each do |mod|
355
+ mod.on_res(res, output) if mod.respond_to? :on_res
356
+ end
357
+
358
+ res_log = "[<] #{req_id} #{res[:code]} #{res[:message]} (#{end_time - start_time}s)\n"
359
+ res_headers.each do |header|
360
+ res_log += "#{header[0].to_s.ljust(30, '.')}: #{header[1].to_s}\n"
361
+ end
362
+
363
+ if res[:body] != nil and not res[:body].empty?
364
+ res_log += try_format_json(res[:body], pretty: true)
365
+ end
366
+
367
+ @@logger.info res_log
368
+
369
+ @@response = SpectreHttpResponse.new(res)
370
+
371
+ raise "Response did not indicate success: #{@@response.code} #{@@response.message}" if req['ensure_success'] and not @@response.success?
372
+
373
+ @@response
374
+ end
375
+ end
376
+
377
+ Spectre.register do |config|
378
+ @@debug = config['debug']
379
+
380
+ @@logger = ::Logger.new(config['log_file'], progname: 'spectre/curl')
381
+ @@logger.level = @@debug ? Logger::DEBUG : Logger::INFO
382
+
383
+ @@secure_keys = config['secure_keys'] || []
384
+
385
+ @@curl_path = config['curl_path'] || 'curl'
386
+
387
+ if config.key? 'http'
388
+ @@http_cfg = {}
389
+
390
+ config['http'].each do |name, cfg|
391
+ @@http_cfg[name] = cfg
392
+ end
393
+ end
394
+ end
395
+
396
+ Spectre.delegate :curl, :curl_response, :curl_request, to: self
397
+ end