aspera-cli 4.14.0 → 4.15.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (90) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/CHANGELOG.md +54 -3
  4. data/CONTRIBUTING.md +7 -7
  5. data/README.md +1457 -880
  6. data/bin/ascli +18 -9
  7. data/bin/asession +12 -14
  8. data/examples/proxy.pac +1 -1
  9. data/lib/aspera/aoc.rb +198 -127
  10. data/lib/aspera/ascmd.rb +24 -14
  11. data/lib/aspera/cli/basic_auth_plugin.rb +9 -6
  12. data/lib/aspera/cli/error.rb +17 -0
  13. data/lib/aspera/cli/extended_value.rb +47 -12
  14. data/lib/aspera/cli/formatter.rb +260 -171
  15. data/lib/aspera/cli/hints.rb +80 -0
  16. data/lib/aspera/cli/main.rb +101 -147
  17. data/lib/aspera/cli/manager.rb +160 -124
  18. data/lib/aspera/cli/plugin.rb +70 -59
  19. data/lib/aspera/cli/plugins/alee.rb +0 -1
  20. data/lib/aspera/cli/plugins/aoc.rb +239 -273
  21. data/lib/aspera/cli/plugins/ats.rb +8 -5
  22. data/lib/aspera/cli/plugins/bss.rb +2 -2
  23. data/lib/aspera/cli/plugins/config.rb +516 -375
  24. data/lib/aspera/cli/plugins/console.rb +40 -0
  25. data/lib/aspera/cli/plugins/cos.rb +4 -5
  26. data/lib/aspera/cli/plugins/faspex.rb +99 -84
  27. data/lib/aspera/cli/plugins/faspex5.rb +179 -148
  28. data/lib/aspera/cli/plugins/node.rb +219 -153
  29. data/lib/aspera/cli/plugins/orchestrator.rb +52 -17
  30. data/lib/aspera/cli/plugins/preview.rb +46 -32
  31. data/lib/aspera/cli/plugins/server.rb +57 -17
  32. data/lib/aspera/cli/plugins/shares.rb +34 -12
  33. data/lib/aspera/cli/sync_actions.rb +68 -0
  34. data/lib/aspera/cli/transfer_agent.rb +45 -55
  35. data/lib/aspera/cli/transfer_progress.rb +74 -0
  36. data/lib/aspera/cli/version.rb +1 -1
  37. data/lib/aspera/colors.rb +3 -1
  38. data/lib/aspera/command_line_builder.rb +14 -11
  39. data/lib/aspera/cos_node.rb +3 -2
  40. data/lib/aspera/environment.rb +17 -6
  41. data/lib/aspera/fasp/agent_aspera.rb +126 -0
  42. data/lib/aspera/fasp/agent_base.rb +31 -77
  43. data/lib/aspera/fasp/agent_connect.rb +21 -22
  44. data/lib/aspera/fasp/agent_direct.rb +88 -102
  45. data/lib/aspera/fasp/agent_httpgw.rb +196 -192
  46. data/lib/aspera/fasp/agent_node.rb +41 -34
  47. data/lib/aspera/fasp/agent_trsdk.rb +75 -34
  48. data/lib/aspera/fasp/error_info.rb +2 -2
  49. data/lib/aspera/fasp/faux_file.rb +52 -0
  50. data/lib/aspera/fasp/installation.rb +43 -184
  51. data/lib/aspera/fasp/management.rb +244 -0
  52. data/lib/aspera/fasp/parameters.rb +59 -26
  53. data/lib/aspera/fasp/parameters.yaml +75 -8
  54. data/lib/aspera/fasp/products.rb +162 -0
  55. data/lib/aspera/fasp/transfer_spec.rb +1 -1
  56. data/lib/aspera/fasp/uri.rb +4 -4
  57. data/lib/aspera/faspex_gw.rb +2 -2
  58. data/lib/aspera/faspex_postproc.rb +2 -2
  59. data/lib/aspera/hash_ext.rb +2 -2
  60. data/lib/aspera/json_rpc.rb +49 -0
  61. data/lib/aspera/line_logger.rb +23 -0
  62. data/lib/aspera/log.rb +57 -16
  63. data/lib/aspera/node.rb +97 -14
  64. data/lib/aspera/oauth.rb +36 -18
  65. data/lib/aspera/open_application.rb +4 -4
  66. data/lib/aspera/persistency_folder.rb +2 -2
  67. data/lib/aspera/preview/file_types.rb +4 -2
  68. data/lib/aspera/preview/generator.rb +22 -35
  69. data/lib/aspera/preview/options.rb +2 -0
  70. data/lib/aspera/preview/terminal.rb +24 -13
  71. data/lib/aspera/preview/utils.rb +19 -26
  72. data/lib/aspera/rest.rb +103 -72
  73. data/lib/aspera/rest_call_error.rb +1 -1
  74. data/lib/aspera/rest_error_analyzer.rb +15 -14
  75. data/lib/aspera/rest_errors_aspera.rb +37 -34
  76. data/lib/aspera/secret_hider.rb +14 -16
  77. data/lib/aspera/ssh.rb +4 -1
  78. data/lib/aspera/sync.rb +128 -122
  79. data/lib/aspera/temp_file_manager.rb +10 -3
  80. data/lib/aspera/web_auth.rb +10 -7
  81. data/lib/aspera/web_server_simple.rb +9 -4
  82. data.tar.gz.sig +0 -0
  83. metadata +33 -15
  84. metadata.gz.sig +0 -0
  85. data/lib/aspera/cli/listener/line_dump.rb +0 -19
  86. data/lib/aspera/cli/listener/logger.rb +0 -22
  87. data/lib/aspera/cli/listener/progress.rb +0 -50
  88. data/lib/aspera/cli/listener/progress_multi.rb +0 -84
  89. data/lib/aspera/cli/plugins/sync.rb +0 -44
  90. data/lib/aspera/fasp/listener.rb +0 -13
data/lib/aspera/rest.rb CHANGED
@@ -10,7 +10,6 @@ require 'net/https'
10
10
  require 'json'
11
11
  require 'base64'
12
12
  require 'cgi'
13
- require 'ruby-progressbar'
14
13
 
15
14
  # add cancel method to http
16
15
  class Net::HTTP::Cancel < Net::HTTPRequest # rubocop:disable Style/ClassAndModuleChildren
@@ -26,34 +25,25 @@ module Aspera
26
25
  class Rest
27
26
  # global settings also valid for any subclass
28
27
  @@global = { # rubocop:disable Style/ClassVars
29
- debug: false,
30
- # true if https ignore certificate
31
- user_agent: 'Ruby',
32
- download_partial_suffix: '.http_partial',
33
- # a lambda which takes the Net::HTTP as arg, use this to change parameters
34
- session_cb: nil,
35
- proxy_user: nil,
36
- proxy_pass: nil
28
+ user_agent: 'Ruby', # goes to HTTP request header: 'User-Agent'
29
+ download_partial_suffix: '.http_partial', # suffix for partial download
30
+ session_cb: nil, # a lambda which takes the Net::HTTP as arg, use this to change parameters
31
+ progress_bar: nil # progress bar object
37
32
  }
38
33
 
34
+ # flag for array parameters prefixed with []
39
35
  ARRAY_PARAMS = '[]'
40
36
 
41
37
  private_constant :ARRAY_PARAMS
42
38
 
43
- # error message when entity not found
39
+ # error message when entity not found (TODO: use specific exception)
44
40
  ENTITY_NOT_FOUND = 'No such'
45
41
 
46
- class << self
47
- # define accessors
48
- @@global.each_key do |p|
49
- define_method(p){@@global[p]}
50
- define_method("#{p}=") do |val|
51
- Log.log.debug{"#{p} => #{val}".red}
52
- @@global[p] = val
53
- end
54
- end
42
+ # Content-Type that are JSON
43
+ JSON_DECODE = ['application/json', 'application/vnd.api+json', 'application/x-javascript'].freeze
55
44
 
56
- def basic_creds(user, pass); return "Basic #{Base64.strict_encode64("#{user}:#{pass}")}"; end
45
+ class << self
46
+ def basic_token(user, pass); return "Basic #{Base64.strict_encode64("#{user}:#{pass}")}"; end
57
47
 
58
48
  # used to build a parameter list prefixed with "[]"
59
49
  # @param values [Array] list of values
@@ -61,47 +51,77 @@ module Aspera
61
51
  return [ARRAY_PARAMS].concat(values)
62
52
  end
63
53
 
54
+ def array_params?(values)
55
+ return values.first.eql?(ARRAY_PARAMS)
56
+ end
57
+
64
58
  # build URI from URL and parameters and check it is http or https
65
59
  def build_uri(url, params=nil)
66
60
  uri = URI.parse(url)
67
61
  raise "REST endpoint shall be http/s not #{uri.scheme}" unless %w[http https].include?(uri.scheme)
68
- if !params.nil?
69
- # support array url params, there is no standard. Either p[]=1&p[]=2, or p=1&p=2
70
- if params.is_a?(Hash)
71
- orig = params
72
- params = []
73
- orig.each do |k, v|
74
- case v
75
- when Array
76
- suffix = v.first.eql?(ARRAY_PARAMS) ? v.shift : ''
77
- v.each do |e|
78
- params.push([k.to_s + suffix, e])
79
- end
80
- else
81
- params.push([k, v])
82
- end
62
+ return uri if params.nil?
63
+ Log.log.debug{Log.dump('params', params)}
64
+ raise 'Internal Error: param must be Hash' unless params.is_a?(Hash)
65
+ query = []
66
+ params.each do |k, v|
67
+ case v
68
+ when Array
69
+ # support array url params, there is no standard. Either p[]=1&p[]=2, or p=1&p=2
70
+ suffix = array_params?(v) ? v.shift : ''
71
+ v.each do |e|
72
+ query.push(["#{k}#{suffix}", e])
83
73
  end
74
+ else
75
+ query.push([k, v])
84
76
  end
85
- # CGI.unescape to transform back %5D into []
86
- uri.query = CGI.unescape(URI.encode_www_form(params))
87
77
  end
78
+ # [] is allowed in url parameters
79
+ uri.query = URI.encode_www_form(query).gsub('%5B%5D=', '[]=')
88
80
  return uri
89
81
  end
90
82
 
83
+ def decode_query(query)
84
+ URI.decode_www_form(query).each_with_object({}){|v, h|h[v.first] = v.last }
85
+ end
86
+
87
+ # start a HTTP/S session, also used for web sockets
88
+ # @param base_url [String] base url of HTTP/S session
89
+ # @return [Net::HTTP] a started HTTP session
91
90
  def start_http_session(base_url)
92
91
  uri = build_uri(base_url)
93
92
  # this honors http_proxy env var
94
93
  http_session = Net::HTTP.new(uri.host, uri.port)
95
- http_session.proxy_user = proxy_user
96
- http_session.proxy_pass = proxy_pass
97
94
  http_session.use_ssl = uri.scheme.eql?('https')
98
- http_session.set_debug_output($stdout) if debug
99
95
  # set http options in callback, such as timeout and cert. verification
100
- session_cb&.call(http_session)
96
+ @@global[:session_cb]&.call(http_session)
101
97
  # manually start session for keep alive (if supported by server, else, session is closed every time)
102
98
  http_session.start
103
99
  return http_session
104
100
  end
101
+
102
+ # get Net::HTTP underlying socket i/o
103
+ # little hack, handy because HTTP debug, proxy, etc... will be available
104
+ # used implement web sockets after `start_http_session`
105
+ def io_http_session(http_session)
106
+ raise "wring type #{http_session.class}" unless http_session.is_a?(Net::HTTP)
107
+ # Net::BufferedIO in net/protocol.rb
108
+ result = http_session.instance_variable_get(:@socket)
109
+ raise "no socket for #{http_session}" if result.nil?
110
+ return result
111
+ end
112
+
113
+ # set global parameters
114
+ def set_parameters(**options)
115
+ options.each do |key, value|
116
+ raise "ERROR: unknown Rest option #{key}" unless @@global.key?(key)
117
+ @@global[key] = value
118
+ end
119
+ end
120
+
121
+ # @return [String] HTTP agent name
122
+ def user_agent
123
+ return @@global[:user_agent]
124
+ end
105
125
  end
106
126
 
107
127
  private
@@ -131,7 +151,7 @@ module Aspera
131
151
  raise 'ERROR: expecting Hash' unless a_rest_params.is_a?(Hash)
132
152
  raise 'ERROR: expecting base_url' unless a_rest_params[:base_url].is_a?(String)
133
153
  @params = a_rest_params.clone
134
- Log.dump('REST params', @params)
154
+ Log.log.debug{Log.dump('REST params', @params)}
135
155
  # base url without trailing slashes (note: string may be frozen)
136
156
  @params[:base_url] = @params[:base_url].gsub(%r{/+$}, '')
137
157
  @http_session = nil
@@ -139,7 +159,7 @@ module Aspera
139
159
  @params[:auth] ||= {type: :none}
140
160
  @params[:not_auth_codes] ||= ['401']
141
161
  @oauth = nil
142
- Log.dump('REST params(2)', @params)
162
+ Log.log.debug{Log.dump('REST params(2)', @params)}
143
163
  end
144
164
 
145
165
  def oauth_token(force_refresh: false)
@@ -159,8 +179,8 @@ module Aspera
159
179
  raise "unsupported operation : #{call_data[:operation]}"
160
180
  end
161
181
  if call_data.key?(:json_params) && !call_data[:json_params].nil?
162
- req.body = JSON.generate(call_data[:json_params])
163
- Log.dump('body JSON data', call_data[:json_params])
182
+ req.body = JSON.generate(call_data[:json_params]) # , ascii_only: true
183
+ Log.log.debug{Log.dump('body JSON data', call_data[:json_params])}
164
184
  req['Content-Type'] = 'application/json'
165
185
  # call_data[:headers]['Accept']='application/json'
166
186
  end
@@ -181,6 +201,7 @@ module Aspera
181
201
  end
182
202
  # :type = :basic
183
203
  req.basic_auth(call_data[:auth][:username], call_data[:auth][:password]) if call_data[:auth][:type].eql?(:basic)
204
+ Log.log.debug{Log.dump(:req_body, req.body)}
184
205
  return req
185
206
  end
186
207
 
@@ -203,14 +224,14 @@ module Aspera
203
224
  # :type (:none, :basic, :oauth2, :url)
204
225
  # :username [:basic]
205
226
  # :password [:basic]
206
- # :url_creds [:url] a hash
227
+ # :url_query [:url] a hash
207
228
  # :* [:oauth2] see Oauth class
208
229
  def call(call_data)
209
230
  raise "Hash call parameter is required (#{call_data.class})" unless call_data.is_a?(Hash)
210
231
  call_data[:subpath] = '' if call_data[:subpath].nil?
211
232
  Log.log.debug{"accessing #{call_data[:subpath]}".red.bold.bg_green}
212
233
  call_data[:headers] ||= {}
213
- call_data[:headers]['User-Agent'] ||= self.class.user_agent
234
+ call_data[:headers]['User-Agent'] ||= @@global[:user_agent]
214
235
  # defaults from @params are overridden by call data
215
236
  call_data = @params.deep_merge(call_data)
216
237
  case call_data[:auth][:type]
@@ -223,7 +244,7 @@ module Aspera
223
244
  call_data[:headers]['Authorization'] = oauth_token unless call_data[:headers].key?('Authorization')
224
245
  when :url
225
246
  call_data[:url_params] ||= {}
226
- call_data[:auth][:url_creds].each do |key, value|
247
+ call_data[:auth][:url_query].each do |key, value|
227
248
  call_data[:url_params][key] = value
228
249
  end
229
250
  else raise "unsupported auth type: [#{call_data[:auth][:type]}]"
@@ -238,16 +259,18 @@ module Aspera
238
259
  # initialize with number of initial retries allowed, nil gives zero
239
260
  tries_remain_redirect = call_data[:redirect_max].to_i if tries_remain_redirect.nil?
240
261
  Log.log.debug("send request (retries=#{tries_remain_redirect})")
262
+ result_mime = nil
263
+ file_saved = false
241
264
  # make http request (pipelined)
242
265
  http_session.request(req) do |response|
243
266
  result[:http] = response
244
- if !call_data[:save_to_file].nil? && result[:http].code.to_s.start_with?('2')
267
+ result_mime = (result[:http]['Content-Type'] || 'text/plain').split(';').first.downcase
268
+ # JSON data needs to be parsed, in case it contains an error code
269
+ if !call_data[:save_to_file].nil? &&
270
+ result[:http].code.to_s.start_with?('2') &&
271
+ !result[:http]['Content-Length'].nil? &&
272
+ !JSON_DECODE.include?(result_mime)
245
273
  total_size = result[:http]['Content-Length'].to_i
246
- progress = ProgressBar.create(
247
- format: '%a %B %p%% %r KB/sec %e',
248
- rate_scale: lambda{|rate|rate / 1024},
249
- title: 'progress',
250
- total: total_size)
251
274
  Log.log.debug('before write file')
252
275
  target_file = call_data[:save_to_file]
253
276
  # override user's path to path in header
@@ -255,34 +278,37 @@ module Aspera
255
278
  target_file = File.join(File.dirname(target_file), m[1])
256
279
  end
257
280
  # download with temp filename
258
- target_file_tmp = "#{target_file}#{self.class.download_partial_suffix}"
281
+ target_file_tmp = "#{target_file}#{@@global[:download_partial_suffix]}"
259
282
  Log.log.debug{"saving to: #{target_file}"}
283
+ written_size = 0
284
+ @@global[:progress_bar]&.event(session_id: 1, type: :session_start)
285
+ @@global[:progress_bar]&.event(session_id: 1, type: :session_size, info: total_size)
260
286
  File.open(target_file_tmp, 'wb') do |file|
261
287
  result[:http].read_body do |fragment|
262
288
  file.write(fragment)
263
- new_process = progress.progress + fragment.length
264
- new_process = total_size if new_process > total_size
265
- progress.progress = new_process
289
+ written_size += fragment.length
290
+ @@global[:progress_bar]&.event(session_id: 1, type: :transfer, info: written_size)
266
291
  end
267
292
  end
293
+ @@global[:progress_bar]&.event(session_id: 1, type: :end)
268
294
  # rename at the end
269
295
  File.rename(target_file_tmp, target_file)
270
- progress = nil
296
+ file_saved = true
271
297
  end # save_to_file
272
298
  end
273
- # sometimes there is a UTF8 char (e.g. (c) )
274
- result[:http].body.force_encoding('UTF-8') if result[:http].body.is_a?(String)
275
- Log.log.debug{"result: body=#{result[:http].body}"}
276
- result_mime = (result[:http]['Content-Type'] || 'text/plain').split(';').first
299
+ # sometimes there is a UTF8 char (e.g. (c) ), TODO : related to mime type encoding ?
300
+ # result[:http].body.force_encoding('UTF-8') if result[:http].body.is_a?(String)
301
+ # Log.log.debug{"result: body=#{result[:http].body}"}
277
302
  result[:data] = case result_mime
278
- when 'application/json', 'application/vnd.api+json'
279
- JSON.parse(result[:http].body) rescue nil
303
+ when *JSON_DECODE
304
+ JSON.parse(result[:http].body) rescue result[:http].body
280
305
  else # when 'text/plain'
281
306
  result[:http].body
282
307
  end
283
- Log.dump("result: parsed: #{result_mime}", result[:data])
308
+ Log.log.debug{Log.dump("result: parsed: #{result_mime}", result[:data])}
284
309
  Log.log.debug{"result: code=#{result[:http].code}"}
285
310
  RestErrorAnalyzer.instance.raise_on_error(req, result)
311
+ File.write(call_data[:save_to_file], result[:http].body) unless file_saved || call_data[:save_to_file].nil?
286
312
  rescue RestCallError => e
287
313
  # not authorized: oauth token expired
288
314
  if call_data[:not_auth_codes].include?(result[:http].code.to_s) && call_data[:auth][:type].eql?(:oauth2)
@@ -303,7 +329,12 @@ module Aspera
303
329
  tries_remain_redirect -= 1
304
330
  current_uri = URI.parse(call_data[:base_url])
305
331
  new_url = e.response['location']
306
- new_url = "#{current_uri.scheme}:#{new_url}" unless new_url.start_with?('http')
332
+ # special case: relative redirect
333
+ if URI.parse(new_url).host.nil?
334
+ # we don't manage relative redirects with non-absolute path
335
+ raise "Error: redirect location is relative: #{new_url}, but does not start with /." unless new_url.start_with?('/')
336
+ new_url = current_uri.scheme + '://' + current_uri.host + new_url
337
+ end
307
338
  Log.log.info{"URL is moved: #{new_url}"}
308
339
  redirection_uri = URI.parse(new_url)
309
340
  call_data[:base_url] = new_url
@@ -333,16 +364,16 @@ module Aspera
333
364
  return call({operation: 'POST', subpath: subpath, headers: {'Accept' => 'application/json'}, encoding => params})
334
365
  end
335
366
 
336
- def read(subpath, args=nil)
337
- return call({operation: 'GET', subpath: subpath, headers: {'Accept' => 'application/json'}, url_params: args})
367
+ def read(subpath, options=nil)
368
+ return call({operation: 'GET', subpath: subpath, headers: {'Accept' => 'application/json'}, url_params: options})
338
369
  end
339
370
 
340
371
  def update(subpath, params)
341
372
  return call({operation: 'PUT', subpath: subpath, headers: {'Accept' => 'application/json'}, json_params: params})
342
373
  end
343
374
 
344
- def delete(subpath, args=nil)
345
- return call({operation: 'DELETE', subpath: subpath, headers: {'Accept' => 'application/json'}, url_params: args})
375
+ def delete(subpath, params=nil)
376
+ return call({operation: 'DELETE', subpath: subpath, headers: {'Accept' => 'application/json'}, url_params: params})
346
377
  end
347
378
 
348
379
  def cancel(subpath)
@@ -355,7 +386,7 @@ module Aspera
355
386
  # @param options additional search options
356
387
  def lookup_by_name(subpath, search_name, options={})
357
388
  # returns entities whose name contains value (case insensitive)
358
- matching_items = read(subpath, options.merge({'q' => CGI.escape(search_name)}))[:data]
389
+ matching_items = read(subpath, options.merge({'q' => search_name}))[:data]
359
390
  # API style: {totalcount:, ...} cspell: disable-line
360
391
  # TODO: not generic enough ? move somewhere ? inheritance ?
361
392
  matching_items = matching_items[subpath] if matching_items.is_a?(Hash)
@@ -8,7 +8,7 @@ module Aspera
8
8
  # @param req HTTP Request object
9
9
  # @param resp HTTP Response object
10
10
  # @param msg Error message
11
- def initialize(req, resp, msg)
11
+ def initialize(msg, req=nil, resp=nil)
12
12
  @request = req
13
13
  @response = resp
14
14
  super(msg)
@@ -27,6 +27,7 @@ module Aspera
27
27
  # Analyzes REST call response and raises a RestCallError exception
28
28
  # if HTTP result code is not 2XX
29
29
  def raise_on_error(req, res)
30
+ Log.log.debug{"raise_on_error #{req.method} #{req.path} #{res[:http].code}"}
30
31
  call_context = {
31
32
  messages: [],
32
33
  request: req,
@@ -44,7 +45,7 @@ module Aspera
44
45
  Log.log.error{"ERROR in handler:\n#{e.message}\n#{e.backtrace}"}
45
46
  end
46
47
  end
47
- raise RestCallError.new(call_context[:request], call_context[:response], call_context[:messages].join("\n")) unless call_context[:messages].empty?
48
+ raise RestCallError.new(call_context[:messages].join("\n"), call_context[:request], call_context[:response]) unless call_context[:messages].empty?
48
49
  end
49
50
 
50
51
  # add a new error handler (done at application initialization)
@@ -59,21 +60,21 @@ module Aspera
59
60
  # add a simple error handler
60
61
  # check that key exists and is string under specified path (hash)
61
62
  # adds other keys as secondary information
62
- def add_simple_handler(name, *args)
63
+ # @param name [String] name of error handler (for logs)
64
+ # @param always [boolean] if true, always add error message, even if response code is 2XX
65
+ # @param path [Array] path to error message in response
66
+ def add_simple_handler(name:, always: false, path:)
67
+ path.freeze
63
68
  add_handler(name) do |type, call_context|
64
- # need to clone because we modify and same array is used subsequently
65
- path = args.clone
66
- # Log.log.debug{"path=#{path}"}
67
- # if last in path is boolean it tells if the error is only with http error code or always
68
- always = [true, false].include?(path.last) ? path.pop : false
69
69
  if call_context[:data].is_a?(Hash) && (!call_context[:response].code.start_with?('2') || always)
70
- msg_key = path.pop
71
- # dig and find sub entry corresponding to path in deep hash
72
- error_struct = path.inject(call_context[:data]) { |sub_hash, key| sub_hash.respond_to?(:keys) ? sub_hash[key] : nil }
73
- if error_struct.is_a?(Hash) && error_struct[msg_key].is_a?(String)
74
- RestErrorAnalyzer.add_error(call_context, type, error_struct[msg_key])
70
+ # Log.log.debug{"simple_handler: #{type} #{path} #{path.last}"}
71
+ # dig and find hash containing error message
72
+ error_struct = path.length.eql?(1) ? call_context[:data] : call_context[:data].dig(*path[0..-2])
73
+ # Log.log.debug{"found: #{error_struct.class} #{error_struct}"}
74
+ if error_struct.is_a?(Hash) && error_struct[path.last].is_a?(String)
75
+ RestErrorAnalyzer.add_error(call_context, type, error_struct[path.last])
75
76
  error_struct.each do |k, v|
76
- next if k.eql?(msg_key)
77
+ next if k.eql?(path.last)
77
78
  RestErrorAnalyzer.add_error(call_context, "#{type}(sub)", "#{k}: #{v}") if [String, Integer].include?(v.class)
78
79
  end
79
80
  end
@@ -93,7 +94,7 @@ module Aspera
93
94
  # log error for further analysis (file must exist to activate)
94
95
  return if log_file.nil? || !File.exist?(log_file)
95
96
  File.open(log_file, 'a+') do |f|
96
- f.write("\n=#{type}=====\n#{call_context[:request].method} #{call_context[:request].path}\n#{call_context[:response].code}\n"\
97
+ f.write("\n=#{type}=====\n#{call_context[:request].method} #{call_context[:request].path}\n#{call_context[:response].code}\n" \
97
98
  "#{JSON.generate(call_context[:data])}\n#{call_context[:messages].join("\n")}")
98
99
  end
99
100
  end
@@ -12,48 +12,51 @@ module Aspera
12
12
  Log.log.debug('registering Aspera REST error handlers')
13
13
  # Faspex 4: both user_message and internal_message, and code 200
14
14
  # example: missing meta data on package creation
15
- RestErrorAnalyzer.instance.add_simple_handler('Type 1: error:user_message', 'error', 'user_message', true)
16
- RestErrorAnalyzer.instance.add_simple_handler('Type 2: error:description', 'error', 'description')
17
- RestErrorAnalyzer.instance.add_simple_handler('Type 3: error:internal_message', 'error', 'internal_message')
15
+ RestErrorAnalyzer.instance.add_simple_handler(name: 'Type 1: error:user_message', path: %w[error user_message], always: true)
16
+ RestErrorAnalyzer.instance.add_simple_handler(name: 'Type 2: error:description', path: %w[error description])
17
+ RestErrorAnalyzer.instance.add_simple_handler(name: 'Type 3: error:internal_message', path: %w[error internal_message])
18
18
  # AoC Automation
19
- RestErrorAnalyzer.instance.add_simple_handler('AoC Automation', 'error')
20
- RestErrorAnalyzer.instance.add_simple_handler('Type 5', 'error_description')
21
- RestErrorAnalyzer.instance.add_simple_handler('Type 6', 'message')
22
- RestErrorAnalyzer.instance.add_handler('Type 7: errors[]') do |name, call_context|
23
- if call_context[:data].is_a?(Hash) && call_context[:data]['errors'].is_a?(Hash)
24
- call_context[:data]['errors'].each do |k, v|
25
- RestErrorAnalyzer.add_error(call_context, name, "#{k}: #{v}")
26
- end
19
+ RestErrorAnalyzer.instance.add_simple_handler(name: 'AoC Automation', path: ['error'])
20
+ RestErrorAnalyzer.instance.add_simple_handler(name: 'Type 5', path: ['error_description'])
21
+ RestErrorAnalyzer.instance.add_simple_handler(name: 'Type 6', path: ['message'])
22
+ RestErrorAnalyzer.instance.add_handler('Type 7: errors[]') do |type, call_context|
23
+ next unless call_context[:data].is_a?(Hash) && call_context[:data]['errors'].is_a?(Hash)
24
+ call_context[:data]['errors'].each do |k, v|
25
+ RestErrorAnalyzer.add_error(call_context, type, "#{k}: #{v}")
27
26
  end
28
27
  end
29
28
  # call to upload_setup and download_setup of node api
30
29
  RestErrorAnalyzer.instance.add_handler('T8:node: *_setup') do |type, call_context|
31
- if call_context[:data].is_a?(Hash)
32
- d_t_s = call_context[:data]['transfer_specs']
33
- if d_t_s.is_a?(Array)
34
- d_t_s.each do |res|
35
- # r_err=res['transfer_spec']['error']
36
- r_err = res['error']
37
- if r_err.is_a?(Hash)
38
- RestErrorAnalyzer.add_error(call_context, type, "#{r_err['code']}: #{r_err['reason']}: #{r_err['user_message']}")
39
- end
40
- end
41
- end
30
+ next unless call_context[:data].is_a?(Hash)
31
+ d_t_s = call_context[:data]['transfer_specs']
32
+ next unless d_t_s.is_a?(Array)
33
+ d_t_s.each do |res|
34
+ r_err = res.dig(*%w[transfer_spec error])
35
+ next unless r_err.is_a?(Hash)
36
+ RestErrorAnalyzer.add_error(call_context, type, "#{r_err['code']}: #{r_err['reason']}: #{r_err['user_message']}")
42
37
  end
43
38
  end
44
- RestErrorAnalyzer.instance.add_simple_handler('T9:IBM cloud IAM', 'errorMessage')
45
- RestErrorAnalyzer.instance.add_simple_handler('T10:faspex v4', 'user_message')
39
+ RestErrorAnalyzer.instance.add_simple_handler(name: 'T9:IBM cloud IAM', path: ['errorMessage'])
40
+ RestErrorAnalyzer.instance.add_simple_handler(name: 'T10:faspex v4', path: ['user_message'])
46
41
  RestErrorAnalyzer.instance.add_handler('bss graphql') do |type, call_context|
47
- if call_context[:data].is_a?(Hash)
48
- d_t_s = call_context[:data]['errors']
49
- if d_t_s.is_a?(Array)
50
- d_t_s.each do |res|
51
- r_err = res['message']
52
- if r_err.is_a?(String)
53
- RestErrorAnalyzer.add_error(call_context, type, r_err)
54
- end
55
- end
56
- end
42
+ next unless call_context[:data].is_a?(Hash)
43
+ d_t_s = call_context[:data]['errors']
44
+ next unless d_t_s.is_a?(Array)
45
+ d_t_s.each do |res|
46
+ r_err = res['message']
47
+ next unless r_err.is_a?(String)
48
+ RestErrorAnalyzer.add_error(call_context, type, r_err)
49
+ end
50
+ end
51
+ RestErrorAnalyzer.instance.add_handler('Orchestrator') do |type, call_context|
52
+ next if call_context[:response].code.start_with?('2')
53
+ data = call_context[:data]
54
+ next unless data.is_a?(Hash)
55
+ work_order = data['work_order']
56
+ next unless work_order.is_a?(Hash)
57
+ RestErrorAnalyzer.add_error(call_context, type, work_order['statusDetails'])
58
+ data['missing_parameters']&.each do |param|
59
+ RestErrorAnalyzer.add_error(call_context, type, "missing parameter: #{param}")
57
60
  end
58
61
  end
59
62
  end # register_handlers
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # cspell:ignore FILEPASS
3
4
  require 'logger'
4
5
 
5
6
  module Aspera
@@ -11,22 +12,25 @@ module Aspera
11
12
  ASCP_ENV_SECRETS = %w[ASPERA_SCP_PASS ASPERA_SCP_KEY ASPERA_SCP_FILEPASS ASPERA_PROXY_PASS ASPERA_SCP_TOKEN].freeze
12
13
  # keys in hash that contain secrets
13
14
  KEY_SECRETS = %w[password secret passphrase _key apikey crn token].freeze
14
- ALL_SECRETS = [ASCP_ENV_SECRETS, KEY_SECRETS].flatten.freeze
15
- FALSE_POSITIVES = [/^access_key$/].freeze
15
+ HTTP_SECRETS = %w[Authorization].freeze
16
+ ALL_SECRETS = [ASCP_ENV_SECRETS, KEY_SECRETS, HTTP_SECRETS].flatten.freeze
17
+ KEY_FALSE_POSITIVES = [/^access_key$/].freeze
16
18
  # regex that define named captures :begin and :end
17
19
  REGEX_LOG_REPLACES = [
18
20
  # CLI manager get/set options
19
- /(?<begin>[sg]et (#{KEY_SECRETS.join('|')})=).*(?<end>)/,
21
+ /(?<begin>[sg]et (?:#{KEY_SECRETS.join('|')})=).*(?<end>)/,
20
22
  # env var ascp exec
21
- /(?<begin> (#{ASCP_ENV_SECRETS.join('|')})=)(\\.|[^ ])*(?<end> )/,
22
- # rendered JSON
23
- /(?<begin>["':][^"]*(#{ALL_SECRETS.join('|')})[^"]*["']?[=>: ]+")[^"]+(?<end>")/,
23
+ /(?<begin> (?:#{ASCP_ENV_SECRETS.join('|')})=)(\\.|[^ ])*(?<end> )/,
24
+ # rendered JSON or Ruby
25
+ /(?<begin>(?:(?<quote>["'])|:)[^"':=]*(?:#{ALL_SECRETS.join('|')})[^"':=]*\k<quote>?(?:=>|:) *")[^"]+(?<end>")/,
24
26
  # option "secret"
25
27
  /(?<begin>"[^"]*(secret)[^"]*"=>{)[^}]+(?<end>})/,
26
28
  # option "secrets"
27
29
  /(?<begin>(secrets)={)[^}]+(?<end>})/,
28
30
  # private key values
29
- /(?<begin>--+BEGIN .+ KEY--+)[[:ascii:]]+?(?<end>--+?END .+ KEY--+)/
31
+ /(?<begin>--+BEGIN [^-]+ KEY--+)[[:ascii:]]+?(?<end>--+?END [^-]+ KEY--+)/,
32
+ # cred in http dump
33
+ /(?<begin>(?:#{HTTP_SECRETS.join('|')}): )[^\\]+(?<end>\\)/i
30
34
  ].freeze
31
35
  private_constant :HIDDEN_PASSWORD, :ASCP_ENV_SECRETS, :KEY_SECRETS, :ALL_SECRETS, :REGEX_LOG_REPLACES
32
36
  @log_secrets = false
@@ -51,21 +55,15 @@ module Aspera
51
55
  # only Strings can be secrets, not booleans, or hash, arrays
52
56
  return false unless keyword.is_a?(String) && value.is_a?(String)
53
57
  # those are not secrets
54
- return false if FALSE_POSITIVES.any?{|f|f.match?(keyword)}
58
+ return false if KEY_FALSE_POSITIVES.any?{|f|f.match?(keyword)}
55
59
  # check if keyword (name) contains an element that designate it as a secret
56
60
  ALL_SECRETS.any?{|kw|keyword.include?(kw)}
57
61
  end
58
62
 
59
- def deep_remove_secret(obj, is_name_value: false)
63
+ def deep_remove_secret(obj)
60
64
  case obj
61
65
  when Array
62
- if is_name_value
63
- obj.each do |i|
64
- i['value'] = HIDDEN_PASSWORD if secret?(i['parameter'], i['value'])
65
- end
66
- else
67
- obj.each{|i|deep_remove_secret(i)}
68
- end
66
+ obj.each{|i|deep_remove_secret(i)}
69
67
  when Hash
70
68
  obj.each do |k, v|
71
69
  if secret?(k, v)
data/lib/aspera/ssh.rb CHANGED
@@ -2,12 +2,15 @@
2
2
 
3
3
  require 'net/ssh'
4
4
 
5
- # HACK: deactivate ed25519 and ecdsa private keys from ssh identities, as it usually hurts
5
+ # HACK: deactivate ed25519 and ecdsa private keys from ssh identities, as it usually cause problems
6
+ old_verbose = $VERBOSE
7
+ $VERBOSE = nil
6
8
  begin
7
9
  module Net; module SSH; module Authentication; class Session; private; def default_keys; %w[~/.ssh/id_dsa ~/.ssh/id_rsa ~/.ssh2/id_dsa ~/.ssh2/id_rsa]; end; end; end; end; end # rubocop:disable Layout/AccessModifierIndentation, Layout/EmptyLinesAroundAccessModifier, Layout/LineLength, Style/Semicolon
8
10
  rescue StandardError
9
11
  # ignore errors
10
12
  end
13
+ $VERBOSE = old_verbose
11
14
 
12
15
  module Aspera
13
16
  # A simple wrapper around Net::SSH