aspera-cli 4.14.0 → 4.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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