spectre-core 1.8.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,29 @@
1
+ module Spectre
2
+ module Diagnostic
3
+ module Stopwatch
4
+ @@duration = 0.0
5
+
6
+ class << self
7
+ def start_watch
8
+ @@start_time = Time.now
9
+ end
10
+
11
+ def stop_watch
12
+ @@end_time = Time.now
13
+ end
14
+
15
+ def measure
16
+ start_watch
17
+ yield
18
+ stop_watch
19
+ end
20
+
21
+ def duration
22
+ @@end_time - @@start_time
23
+ end
24
+ end
25
+
26
+ Spectre.delegate :start_watch, :stop_watch, :duration, :measure, to: Stopwatch
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,26 @@
1
+ require 'ostruct'
2
+
3
+ def to_recursive_ostruct(hash)
4
+ OpenStruct.new(hash.each_with_object({}) do |(key, val), memo|
5
+ memo[key] = val.is_a?(Hash) ? to_recursive_ostruct(val) : val
6
+ end)
7
+ end
8
+
9
+ module Spectre
10
+ module Environment
11
+ class << self
12
+ @@environment = OpenStruct.new
13
+
14
+ def env
15
+ @@environment
16
+ end
17
+ end
18
+
19
+ Spectre.register do |config|
20
+ @@environment = to_recursive_ostruct(config)
21
+ @@environment.freeze
22
+ end
23
+
24
+ Spectre.delegate :env, to: Environment
25
+ end
26
+ end
@@ -0,0 +1,195 @@
1
+ require 'net/ftp'
2
+ require 'net/sftp'
3
+ require 'logger'
4
+ require 'json'
5
+
6
+
7
+ module Spectre
8
+ module FTP
9
+ @@cfg = {}
10
+
11
+ class FTPConnection < DslClass
12
+ def initialize host, username, password, opts, logger
13
+ @__logger = logger
14
+ @__session = nil
15
+
16
+ @__host = host
17
+ @__username = username
18
+ @__password = password
19
+ @__opts = opts
20
+ end
21
+
22
+ def connect!
23
+ return unless @__session == nil or @__session.closed?
24
+ @__logger.info "Connecting to '#{@__host}' with user '#{@__username}'"
25
+ @__session = Net::FTP.new(@__host, @__opts)
26
+ @__session.login @__username, @__password
27
+ end
28
+
29
+ def close
30
+ return unless @__session and not @__session.closed?
31
+ @__session.close
32
+ end
33
+
34
+ def can_connect?
35
+ begin
36
+ connect!
37
+ return true
38
+ rescue
39
+ return false
40
+ end
41
+ end
42
+
43
+ def download remotefile, to: File.basename(remotefile)
44
+ connect!
45
+ @__logger.info "Downloading '#{@__username}@#{@__host}:#{File.join @__session.pwd, remotefile}' to '#{File.expand_path to}'"
46
+ @__session.getbinaryfile(remotefile, to)
47
+ end
48
+
49
+ def upload localfile, to: File.basename(localfile)
50
+ connect!
51
+ @__logger.info "Uploading '#{File.expand_path localfile}' to '#{@__username}@#{@__host}:#{File.join @__session.pwd, to}'"
52
+ @__session.putbinaryfile(localfile, to)
53
+ end
54
+
55
+ def list
56
+ connect!
57
+ file_list = @__session.list
58
+ @__logger.info "Listing file in #{@__session.pwd}\n#{file_list}"
59
+ file_list
60
+ end
61
+ end
62
+
63
+
64
+ class SFTPConnection < DslClass
65
+ def initialize host, username, opts, logger
66
+ opts[:non_interactive] = true
67
+
68
+ @__logger = logger
69
+ @__session = nil
70
+ @__host = host
71
+ @__username = username
72
+ @__opts = opts
73
+ end
74
+
75
+ def connect!
76
+ return unless @__session == nil or @__session.closed?
77
+ @__logger.info "Connecting to '#{@__host}' with user '#{@__username}'"
78
+ @__session = Net::SFTP.start(@__host, @__username, @__opts)
79
+ @__session.connect!
80
+ end
81
+
82
+ def close
83
+ return unless @__session and not @__session.closed?
84
+ # @__session.close!
85
+ end
86
+
87
+ def can_connect?
88
+ begin
89
+ connect!
90
+ return true
91
+ rescue
92
+ return false
93
+ end
94
+ end
95
+
96
+ def download remotefile, to: File.basename(remotefile)
97
+ @__logger.info "Downloading '#{@__username}@#{@__host}:#{remotefile}' to '#{File.expand_path to}'"
98
+ connect!
99
+ @__session.download!(remotefile, to)
100
+ end
101
+
102
+ def upload localfile, to: File.basename(localfile)
103
+ @__logger.info "Uploading '#{File.expand_path localfile}' to '#{@__username}@#{@__host}:#{to}'"
104
+ connect!
105
+ @__session.upload!(localfile, to)
106
+ end
107
+
108
+ def stat path
109
+ connect!
110
+ file_info = @__session.stat! path
111
+ @__logger.info "Stat '#{path}'\n#{JSON.pretty_generate file_info.attributes}"
112
+ file_info.attributes
113
+ end
114
+
115
+ def exists path
116
+ begin
117
+ file_info = @__session.stat! path
118
+
119
+ rescue Net::SFTP::StatusException => e
120
+ return false if e.description == 'no such file'
121
+ raise e
122
+ end
123
+
124
+ return true
125
+ end
126
+ end
127
+
128
+
129
+ class << self
130
+ def ftp name, config={}, &block
131
+ raise "FTP connection '#{name}' not configured" unless @@cfg.has_key?(name) or config.count > 0
132
+ cfg = @@cfg[name] || {}
133
+
134
+ host = config[:host] || cfg['host'] || name
135
+ username = config[:username] || cfg['username']
136
+ password = config[:password] || cfg['password']
137
+
138
+ opts = {}
139
+ opts[:ssl] = config[:ssl]
140
+ opts[:port] = config[:port] || cfg['port'] || 21
141
+
142
+ @@logger.info "Connecting to #{host} with user #{username}"
143
+
144
+ ftp_conn = FTPConnection.new(host, username, password, opts, @@logger)
145
+
146
+ begin
147
+ ftp_conn.instance_eval &block
148
+ ensure
149
+ ftp_conn.close
150
+ end
151
+ end
152
+
153
+ def sftp name, config={}, &block
154
+ raise "FTP connection '#{name}' not configured" unless @@cfg.has_key?(name) or config.count > 0
155
+
156
+ cfg = @@cfg[name] || {}
157
+
158
+ host = config[:host] || cfg['host'] || name
159
+ username = config[:username] || cfg['username']
160
+ password = config[:password] || cfg['password']
161
+
162
+ opts = {}
163
+ opts[:password] = password
164
+ opts[:port] = config[:port] || cfg['port'] || 22
165
+ opts[:keys] = [cfg['key']] if cfg.has_key? 'key'
166
+ opts[:passphrase] = cfg['passphrase'] if cfg.has_key? 'passphrase'
167
+
168
+ opts[:auth_methods] = []
169
+ opts[:auth_methods].push 'publickey' if opts[:keys]
170
+ opts[:auth_methods].push 'password' if opts[:password]
171
+
172
+ sftp_con = SFTPConnection.new(host, username, opts, @@logger)
173
+
174
+ begin
175
+ sftp_con.instance_eval &block
176
+ ensure
177
+ sftp_con.close
178
+ end
179
+ end
180
+ end
181
+
182
+ Spectre.register do |config|
183
+ @@logger = ::Logger.new config['log_file'], progname: 'spectre/ftp'
184
+
185
+ if config.has_key? 'ftp'
186
+
187
+ config['ftp'].each do |name, cfg|
188
+ @@cfg[name] = cfg
189
+ end
190
+ end
191
+ end
192
+
193
+ Spectre.delegate :ftp, :sftp, to: self
194
+ end
195
+ end
@@ -0,0 +1,64 @@
1
+ require 'securerandom'
2
+ require 'json'
3
+ require 'date'
4
+ require 'ostruct'
5
+
6
+ class ::String
7
+ def as_json
8
+ JSON.parse(self, object_class: OpenStruct)
9
+ end
10
+
11
+ def as_date
12
+ DateTime.parse(self)
13
+ end
14
+
15
+ def content with: nil
16
+ fail "'#{self}' is not a file path, or the file does not exist." if !File.exists? self
17
+ file_content = File.read(self)
18
+
19
+ if with
20
+ with.each do |key, value|
21
+ file_content = file_content.gsub '#{' + key.to_s + '}', value.to_s
22
+ end
23
+ end
24
+
25
+ file_content
26
+ end
27
+
28
+ def exists?
29
+ File.exists? self
30
+ end
31
+
32
+ def remove!
33
+ fail "'#{self}' is not a file path, or the file does not exist." if !File.exists? self
34
+
35
+ File.delete self
36
+ end
37
+
38
+ def trim count=50
39
+ if (self.length + 3) > count
40
+ return self[0..count] + '...'
41
+ end
42
+
43
+ self
44
+ end
45
+ end
46
+
47
+
48
+ class ::OpenStruct
49
+ def to_json *args, **kwargs
50
+ self.to_h.inject({}) { |memo, (k,v)| memo[k] = v.is_a?(OpenStruct) ? v.to_h : v; memo }.to_json(*args, **kwargs)
51
+ end
52
+ end
53
+
54
+
55
+ class ::Hash
56
+ def symbolize_keys
57
+ self.inject({}) { |memo, (k,v)| memo[k.to_sym] = v; memo }
58
+ end
59
+ end
60
+
61
+
62
+ def uuid length = 5
63
+ SecureRandom.uuid().gsub('-', '')[0..length]
64
+ end
@@ -0,0 +1,343 @@
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
+ raise 'Body value must be a string' if not body_content.is_a? String
66
+ @__req['body'] = body_content
67
+ end
68
+
69
+ def ensure_success!
70
+ @__req['ensure_success'] = true
71
+ end
72
+
73
+ def ensure_success?
74
+ @__req['ensure_success']
75
+ end
76
+
77
+ def authenticate method
78
+ @__req['auth'] = method
79
+ end
80
+
81
+ def certificate path
82
+ @__req['cert'] = path
83
+ use_ssl!
84
+ end
85
+
86
+ def use_ssl!
87
+ @__req['use_ssl'] = true
88
+ end
89
+
90
+ def to_s
91
+ @__req.to_s
92
+ end
93
+
94
+ alias_method :auth, :authenticate
95
+ alias_method :cert, :certificate
96
+ alias_method :media_type, :content_type
97
+ end
98
+
99
+ class SpectreHttpHeader
100
+ def initialize headers
101
+ @headers = headers || {}
102
+ end
103
+
104
+ def [] key
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
+
170
+ def https name, &block
171
+ http(name, secure: true, &block)
172
+ end
173
+
174
+ def http name, secure: nil, &block
175
+ req = {}
176
+
177
+ if @@http_cfg.has_key? name
178
+ req.merge! @@http_cfg[name]
179
+ raise "No `base_url' set for HTTP client '#{name}'. Check your HTTP config in your environment." if !req['base_url']
180
+ else
181
+ req['base_url'] = name
182
+ end
183
+
184
+ req['user_ssl'] = secure if secure != nil
185
+
186
+ SpectreHttpRequest.new(req).instance_eval(&block) if block_given?
187
+
188
+ invoke(req)
189
+ end
190
+
191
+ def request
192
+ raise 'No request has been invoked yet' unless @@request
193
+ @@request
194
+ end
195
+
196
+ def response
197
+ raise 'There is no response. No request has been invoked yet.' unless @@response
198
+ @@response
199
+ end
200
+
201
+ def register mod
202
+ raise 'Module must not be nil' unless mod
203
+ @@modules << mod
204
+ end
205
+
206
+ private
207
+
208
+ def try_format_json str, pretty: false
209
+ return str unless str or str.empty?
210
+
211
+ begin
212
+ json = JSON.parse str
213
+
214
+ if pretty
215
+ str = JSON.pretty_generate(json)
216
+ else
217
+ str = JSON.dump(json)
218
+ end
219
+ rescue
220
+ # do nothing
221
+ end
222
+
223
+ str
224
+ end
225
+
226
+ def invoke req
227
+ @@request = nil
228
+
229
+ if req['cert'] or req['use_ssl']
230
+ scheme = 'https'
231
+ else
232
+ scheme = 'http'
233
+ end
234
+
235
+ base_url = req['base_url']
236
+
237
+ if not base_url.match /http(?:s)?:\/\//
238
+ base_url = scheme + '://' + base_url
239
+ end
240
+
241
+ if req['path']
242
+ base_url = base_url + '/' if not base_url.end_with? '/'
243
+ base_url += req['path']
244
+ end
245
+
246
+ uri = URI(base_url)
247
+
248
+ raise "'#{uri}' is not a valid uri" if not uri.host
249
+
250
+ uri.query = URI.encode_www_form(req['query']) unless not req['query'] or req['query'].empty?
251
+
252
+ net_http = Net::HTTP.new(uri.host, uri.port)
253
+
254
+ if uri.scheme == 'https'
255
+ net_http.use_ssl = true
256
+
257
+ if req.has_key? 'cert'
258
+ raise "Certificate '#{req['cert']}' does not exist" unless File.exists? req['cert']
259
+ net_http.verify_mode = OpenSSL::SSL::VERIFY_PEER
260
+ net_http.ca_file = req['cert']
261
+ else
262
+ net_http.verify_mode = OpenSSL::SSL::VERIFY_NONE
263
+ end
264
+ end
265
+
266
+ net_req = Net::HTTPGenericRequest.new(req['method'], true, true, uri)
267
+ net_req.body = req['body']
268
+ net_req.content_type = req['content_type'] if req['content_type'] and not req['content_type'].empty?
269
+
270
+ if req['headers']
271
+ req['headers'].each do |header|
272
+ net_req[header[0]] = header[1]
273
+ end
274
+ end
275
+
276
+ req_id = SecureRandom.uuid()[0..5]
277
+
278
+ # Log request
279
+
280
+ req_log = "[>] #{req_id} #{req['method']} #{uri}"
281
+ req['headers'].each do |header|
282
+ req_log += "\n#{header[0].to_s.ljust(30, '.')}: #{header[1].to_s}"
283
+ end if req['headers']
284
+ req_log += "\n" + try_format_json(req['body'], pretty: true) if req['body'] != nil and not req['body'].empty?
285
+
286
+ @@logger.info req_log
287
+
288
+ # Request
289
+
290
+ start_time = Time.now
291
+
292
+ @@modules.each do |mod|
293
+ mod.on_req(net_http, net_req, req) if mod.respond_to? :on_req
294
+ end
295
+
296
+ net_res = net_http.request(net_req)
297
+
298
+ end_time = Time.now
299
+
300
+ @@modules.each do |mod|
301
+ mod.on_res(net_http, net_res, req) if mod.respond_to? :on_res
302
+ end
303
+
304
+ # Log response
305
+
306
+ res_log = "[<] #{req_id} #{net_res.code} #{net_res.message} (#{end_time - start_time}s)\n"
307
+ net_res.each_header do |header, value|
308
+ res_log += "#{header.to_s.ljust(30, '.')}: #{value}\n"
309
+ end
310
+ res_log += try_format_json(net_res.body, pretty: true) if net_res.body != nil and !net_res.body.empty?
311
+
312
+ @@logger.info(res_log)
313
+
314
+ if req['ensure_success']
315
+ code = Integer(net_res.code)
316
+ fail "Response code of #{req_id} did not indicate success: #{net_res.code} #{net_res.message}" if code >= 400
317
+ end
318
+
319
+ @@request = OpenStruct.new(req)
320
+ @@response = SpectreHttpResponse.new({
321
+ code: net_res.code.to_i,
322
+ message: net_res.message,
323
+ headers: net_res.to_hash,
324
+ body: net_res.body
325
+ })
326
+ end
327
+ end
328
+
329
+ Spectre.register do |config|
330
+ @@logger = ::Logger.new config['log_file'], progname: 'spectre/http'
331
+
332
+ if config.has_key? 'http'
333
+ @@http_cfg = {}
334
+
335
+ config['http'].each do |name, cfg|
336
+ @@http_cfg[name] = cfg
337
+ end
338
+ end
339
+ end
340
+
341
+ Spectre.delegate :http, :https, :request, :response, to: self
342
+ end
343
+ end