aspera-cli 4.15.0 → 4.17.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 (108) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/BUGS.md +29 -3
  4. data/CHANGELOG.md +375 -280
  5. data/CONTRIBUTING.md +71 -18
  6. data/README.md +1978 -1656
  7. data/bin/ascli +13 -31
  8. data/bin/asession +32 -22
  9. data/examples/dascli +2 -2
  10. data/lib/aspera/agent/alpha.rb +117 -0
  11. data/lib/aspera/agent/base.rb +61 -0
  12. data/lib/aspera/{fasp/agent_connect.rb → agent/connect.rb} +13 -11
  13. data/lib/aspera/{fasp/agent_direct.rb → agent/direct.rb} +116 -116
  14. data/lib/aspera/{fasp/agent_httpgw.rb → agent/httpgw.rb} +21 -19
  15. data/lib/aspera/{fasp/agent_node.rb → agent/node.rb} +21 -33
  16. data/lib/aspera/agent/trsdk.rb +188 -0
  17. data/lib/aspera/api/aoc.rb +586 -0
  18. data/lib/aspera/api/ats.rb +46 -0
  19. data/lib/aspera/api/cos_node.rb +95 -0
  20. data/lib/aspera/api/node.rb +344 -0
  21. data/lib/aspera/ascmd.rb +47 -14
  22. data/lib/aspera/{fasp → ascp}/installation.rb +54 -15
  23. data/lib/aspera/{fasp → ascp}/management.rb +14 -14
  24. data/lib/aspera/{fasp → ascp}/products.rb +1 -1
  25. data/lib/aspera/assert.rb +45 -0
  26. data/lib/aspera/cli/basic_auth_plugin.rb +11 -10
  27. data/lib/aspera/cli/extended_value.rb +5 -5
  28. data/lib/aspera/cli/formatter.rb +27 -14
  29. data/lib/aspera/cli/hints.rb +7 -6
  30. data/lib/aspera/cli/main.rb +49 -29
  31. data/lib/aspera/cli/manager.rb +46 -36
  32. data/lib/aspera/cli/plugin.rb +34 -20
  33. data/lib/aspera/cli/plugin_factory.rb +61 -0
  34. data/lib/aspera/cli/plugins/alee.rb +7 -7
  35. data/lib/aspera/cli/plugins/aoc.rb +168 -132
  36. data/lib/aspera/cli/plugins/ats.rb +33 -33
  37. data/lib/aspera/cli/plugins/bss.rb +3 -4
  38. data/lib/aspera/cli/plugins/config.rb +250 -272
  39. data/lib/aspera/cli/plugins/console.rb +8 -6
  40. data/lib/aspera/cli/plugins/cos.rb +20 -19
  41. data/lib/aspera/cli/plugins/faspex.rb +71 -60
  42. data/lib/aspera/cli/plugins/faspex5.rb +212 -133
  43. data/lib/aspera/cli/plugins/node.rb +83 -75
  44. data/lib/aspera/cli/plugins/orchestrator.rb +36 -44
  45. data/lib/aspera/cli/plugins/preview.rb +33 -31
  46. data/lib/aspera/cli/plugins/server.rb +33 -32
  47. data/lib/aspera/cli/plugins/shares.rb +39 -33
  48. data/lib/aspera/cli/sync_actions.rb +9 -9
  49. data/lib/aspera/cli/transfer_agent.rb +45 -25
  50. data/lib/aspera/cli/transfer_progress.rb +2 -3
  51. data/lib/aspera/cli/version.rb +1 -1
  52. data/lib/aspera/colors.rb +5 -0
  53. data/lib/aspera/command_line_builder.rb +16 -14
  54. data/lib/aspera/coverage.rb +21 -0
  55. data/lib/aspera/data_repository.rb +33 -2
  56. data/lib/aspera/environment.rb +5 -4
  57. data/lib/aspera/faspex_gw.rb +13 -11
  58. data/lib/aspera/faspex_postproc.rb +6 -5
  59. data/lib/aspera/id_generator.rb +4 -2
  60. data/lib/aspera/json_rpc.rb +10 -8
  61. data/lib/aspera/keychain/encrypted_hash.rb +46 -11
  62. data/lib/aspera/keychain/macos_security.rb +29 -22
  63. data/lib/aspera/log.rb +5 -4
  64. data/lib/aspera/nagios.rb +7 -2
  65. data/lib/aspera/node_simulator.rb +213 -0
  66. data/lib/aspera/oauth/base.rb +143 -0
  67. data/lib/aspera/oauth/factory.rb +124 -0
  68. data/lib/aspera/oauth/generic.rb +34 -0
  69. data/lib/aspera/oauth/jwt.rb +51 -0
  70. data/lib/aspera/oauth/url_json.rb +31 -0
  71. data/lib/aspera/oauth/web.rb +50 -0
  72. data/lib/aspera/oauth.rb +5 -328
  73. data/lib/aspera/open_application.rb +7 -7
  74. data/lib/aspera/persistency_action_once.rb +13 -14
  75. data/lib/aspera/persistency_folder.rb +3 -2
  76. data/lib/aspera/preview/file_types.rb +53 -267
  77. data/lib/aspera/preview/generator.rb +7 -5
  78. data/lib/aspera/preview/terminal.rb +17 -7
  79. data/lib/aspera/preview/utils.rb +8 -7
  80. data/lib/aspera/proxy_auto_config.rb +6 -3
  81. data/lib/aspera/rest.rb +187 -140
  82. data/lib/aspera/rest_error_analyzer.rb +1 -0
  83. data/lib/aspera/rest_errors_aspera.rb +5 -3
  84. data/lib/aspera/resumer.rb +77 -0
  85. data/lib/aspera/secret_hider.rb +5 -2
  86. data/lib/aspera/ssh.rb +15 -8
  87. data/lib/aspera/temp_file_manager.rb +1 -1
  88. data/lib/aspera/{fasp → transfer}/error.rb +3 -3
  89. data/lib/aspera/{fasp → transfer}/error_info.rb +1 -1
  90. data/lib/aspera/{fasp → transfer}/faux_file.rb +1 -1
  91. data/lib/aspera/{fasp → transfer}/parameters.rb +95 -120
  92. data/lib/aspera/{fasp/transfer_spec.rb → transfer/spec.rb} +23 -19
  93. data/lib/aspera/{fasp/parameters.yaml → transfer/spec.yaml} +4 -99
  94. data/lib/aspera/transfer/sync.rb +273 -0
  95. data/lib/aspera/{fasp → transfer}/uri.rb +10 -9
  96. data/lib/aspera/web_server_simple.rb +12 -3
  97. data.tar.gz.sig +0 -0
  98. metadata +92 -68
  99. metadata.gz.sig +0 -0
  100. data/lib/aspera/aoc.rb +0 -606
  101. data/lib/aspera/ats_api.rb +0 -47
  102. data/lib/aspera/cos_node.rb +0 -93
  103. data/lib/aspera/fasp/agent_aspera.rb +0 -126
  104. data/lib/aspera/fasp/agent_base.rb +0 -48
  105. data/lib/aspera/fasp/agent_trsdk.rb +0 -146
  106. data/lib/aspera/fasp/resume_policy.rb +0 -77
  107. data/lib/aspera/node.rb +0 -338
  108. data/lib/aspera/sync.rb +0 -219
data/lib/aspera/rest.rb CHANGED
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'aspera/log'
4
+ require 'aspera/assert'
4
5
  require 'aspera/oauth'
5
6
  require 'aspera/rest_error_analyzer'
6
7
  require 'aspera/hash_ext'
@@ -55,18 +56,19 @@ module Aspera
55
56
  return values.first.eql?(ARRAY_PARAMS)
56
57
  end
57
58
 
58
- # build URI from URL and parameters and check it is http or https
59
- def build_uri(url, params=nil)
59
+ # build URI from URL and parameters and check it is http or https, encode array [] parameters
60
+ def build_uri(url, query_hash=nil)
60
61
  uri = URI.parse(url)
61
- raise "REST endpoint shall be http/s not #{uri.scheme}" unless %w[http https].include?(uri.scheme)
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)
62
+ Aspera.assert(%w[http https].include?(uri.scheme)){"REST endpoint shall be http/s not #{uri.scheme}"}
63
+ return uri if query_hash.nil?
64
+ Log.log.debug{Log.dump('query', query_hash)}
65
+ Aspera.assert_type(query_hash, Hash)
66
+ return uri if query_hash.empty?
65
67
  query = []
66
- params.each do |k, v|
68
+ query_hash.each do |k, v|
67
69
  case v
68
70
  when Array
69
- # support array url params, there is no standard. Either p[]=1&p[]=2, or p=1&p=2
71
+ # support array for query parameter, there is no standard. Either p[]=1&p[]=2, or p=1&p=2
70
72
  suffix = array_params?(v) ? v.shift : ''
71
73
  v.each do |e|
72
74
  query.push(["#{k}#{suffix}", e])
@@ -84,7 +86,7 @@ module Aspera
84
86
  URI.decode_www_form(query).each_with_object({}){|v, h|h[v.first] = v.last }
85
87
  end
86
88
 
87
- # start a HTTP/S session, also used for web sockets
89
+ # Start a HTTP/S session, also used for web sockets
88
90
  # @param base_url [String] base url of HTTP/S session
89
91
  # @return [Net::HTTP] a started HTTP session
90
92
  def start_http_session(base_url)
@@ -103,17 +105,34 @@ module Aspera
103
105
  # little hack, handy because HTTP debug, proxy, etc... will be available
104
106
  # used implement web sockets after `start_http_session`
105
107
  def io_http_session(http_session)
106
- raise "wring type #{http_session.class}" unless http_session.is_a?(Net::HTTP)
108
+ Aspera.assert_type(http_session, Net::HTTP)
107
109
  # Net::BufferedIO in net/protocol.rb
108
110
  result = http_session.instance_variable_get(:@socket)
109
- raise "no socket for #{http_session}" if result.nil?
111
+ Aspera.assert(!result.nil?){"no socket for #{http_session}"}
112
+ return result
113
+ end
114
+
115
+ # @return [String] PEM certificates of remote server
116
+ def remote_certificate_chain(url, as_string: true)
117
+ result = []
118
+ # initiate a session to retrieve remote certificate
119
+ http_session = Rest.start_http_session(url)
120
+ begin
121
+ # retrieve underlying openssl socket
122
+ result = Rest.io_http_session(http_session).io.peer_cert_chain
123
+ rescue
124
+ result = http_session.peer_cert
125
+ ensure
126
+ http_session.finish
127
+ end
128
+ result = result.map(&:to_pem).join("\n") if as_string
110
129
  return result
111
130
  end
112
131
 
113
132
  # set global parameters
114
133
  def set_parameters(**options)
115
134
  options.each do |key, value|
116
- raise "ERROR: unknown Rest option #{key}" unless @@global.key?(key)
135
+ Aspera.assert(@@global.key?(key)){"Unknown Rest option #{key}"}
117
136
  @@global[key] = value
118
137
  end
119
138
  end
@@ -129,135 +148,169 @@ module Aspera
129
148
  # create and start keep alive connection on demand
130
149
  def http_session
131
150
  if @http_session.nil?
132
- @http_session = self.class.start_http_session(@params[:base_url])
151
+ @http_session = self.class.start_http_session(@base_url)
133
152
  end
134
153
  return @http_session
135
154
  end
136
155
 
137
- public
138
-
139
- attr_reader :params
140
-
141
- def oauth
142
- if @oauth.nil?
143
- raise 'ERROR: no OAuth defined' unless @params[:auth][:type].eql?(:oauth2)
144
- @oauth = Oauth.new(@params[:auth])
145
- end
146
- return @oauth
147
- end
148
-
149
- # @param a_rest_params [Hash] default call parameters (merged at call)
150
- def initialize(a_rest_params)
151
- raise 'ERROR: expecting Hash' unless a_rest_params.is_a?(Hash)
152
- raise 'ERROR: expecting base_url' unless a_rest_params[:base_url].is_a?(String)
153
- @params = a_rest_params.clone
154
- Log.log.debug{Log.dump('REST params', @params)}
155
- # base url without trailing slashes (note: string may be frozen)
156
- @params[:base_url] = @params[:base_url].gsub(%r{/+$}, '')
157
- @http_session = nil
158
- # default is no auth
159
- @params[:auth] ||= {type: :none}
160
- @params[:not_auth_codes] ||= ['401']
161
- @oauth = nil
162
- Log.log.debug{Log.dump('REST params(2)', @params)}
163
- end
164
-
165
- def oauth_token(force_refresh: false)
166
- raise "ERROR: expecting boolean, have #{force_refresh}" unless [true, false].include?(force_refresh)
167
- return oauth.get_authorization(use_refresh_token: force_refresh)
168
- end
169
-
170
- def build_request(call_data)
156
+ def build_request(
157
+ operation:,
158
+ subpath:,
159
+ url_params:,
160
+ json_params:,
161
+ www_body_params:,
162
+ text_body_params:,
163
+ headers:
164
+ )
171
165
  # TODO: shall we percent encode subpath (spaces) test with access key delete with space in id
172
166
  # URI.escape()
173
- uri = self.class.build_uri("#{call_data[:base_url]}#{['', '/'].include?(call_data[:subpath]) ? '' : '/'}#{call_data[:subpath]}", call_data[:url_params])
167
+ separator = !['', '/'].include?(subpath) || @base_url.end_with?('/') ? '/' : ''
168
+ uri = self.class.build_uri("#{@base_url}#{separator}#{subpath}", url_params)
174
169
  Log.log.debug{"URI=#{uri}"}
175
170
  begin
176
171
  # instantiate request object based on string name
177
- req = Net::HTTP.const_get(call_data[:operation].capitalize).new(uri)
172
+ req = Net::HTTP.const_get(operation.capitalize).new(uri)
178
173
  rescue NameError
179
- raise "unsupported operation : #{call_data[:operation]}"
174
+ raise "unsupported operation : #{operation}"
180
175
  end
181
- if call_data.key?(:json_params) && !call_data[:json_params].nil?
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])}
176
+ if !json_params.nil?
177
+ req.body = JSON.generate(json_params) # , ascii_only: true
184
178
  req['Content-Type'] = 'application/json'
185
- # call_data[:headers]['Accept']='application/json'
186
179
  end
187
- if call_data.key?(:www_body_params)
188
- req.body = URI.encode_www_form(call_data[:www_body_params])
189
- Log.log.debug{"body www data=#{req.body.chomp}"}
180
+ if !www_body_params.nil?
181
+ req.body = URI.encode_www_form(www_body_params)
190
182
  req['Content-Type'] = 'application/x-www-form-urlencoded'
191
183
  end
192
- if call_data.key?(:text_body_params)
193
- req.body = call_data[:text_body_params]
194
- Log.log.debug{"body data=#{req.body.chomp}"}
184
+ if !text_body_params.nil?
185
+ req.body = text_body_params
195
186
  end
196
187
  # set headers
197
- if call_data.key?(:headers)
198
- call_data[:headers].each_key do |key|
199
- req[key] = call_data[:headers][key]
200
- end
188
+ headers.each do |key, value|
189
+ req[key] = value
201
190
  end
202
191
  # :type = :basic
203
- req.basic_auth(call_data[:auth][:username], call_data[:auth][:password]) if call_data[:auth][:type].eql?(:basic)
192
+ req.basic_auth(@auth_params[:username], @auth_params[:password]) if @auth_params[:type].eql?(:basic)
204
193
  Log.log.debug{Log.dump(:req_body, req.body)}
205
194
  return req
206
195
  end
207
196
 
197
+ public
198
+
199
+ attr_reader :auth_params
200
+ attr_reader :base_url
201
+
202
+ def params
203
+ return {
204
+ base_url: @base_url,
205
+ auth: @auth_params,
206
+ not_auth_codes: @not_auth_codes,
207
+ redirect_max: @redirect_max,
208
+ headers: @headers
209
+ }
210
+ end
211
+
212
+ # @param base_url [String] base URL of REST API
213
+ # @param auth [Hash] authentication parameters:
214
+ # :type (:none, :basic, :url, :oauth2)
215
+ # :username [:basic]
216
+ # :password [:basic]
217
+ # :url_query [:url] a hash
218
+ # :* [:oauth2] see OAuth::Factory class
219
+ # @param not_auth_codes [Array] codes that trigger a refresh/regeneration of bearer token
220
+ # @param redirect_max [int] max redirections allowed
221
+ def initialize(
222
+ base_url:,
223
+ auth: nil,
224
+ not_auth_codes: nil,
225
+ redirect_max: 0,
226
+ headers: nil
227
+ )
228
+ Aspera.assert_type(base_url, String)
229
+ # base url with max one trailing slashes (note: string may be frozen)
230
+ @base_url = base_url.gsub(%r{//+$}, '/')
231
+ # default is no auth
232
+ @auth_params = auth.nil? ? {type: :none} : auth
233
+ Aspera.assert_type(@auth_params, Hash)
234
+ Aspera.assert(@auth_params.key?(:type)){'no auth type defined'}
235
+ @not_auth_codes = not_auth_codes.nil? ? ['401'] : not_auth_codes
236
+ Aspera.assert_type(@not_auth_codes, Array)
237
+ # persistent session
238
+ @http_session = nil
239
+ # OAuth object (created on demand)
240
+ @oauth = nil
241
+ @redirect_max = redirect_max
242
+ @headers = headers.nil? ? {} : headers
243
+ Aspera.assert_type(@headers, Hash)
244
+ @headers['User-Agent'] ||= @@global[:user_agent]
245
+ end
246
+
247
+ # @return the OAuth object (create, or cached if already created)
248
+ def oauth
249
+ if @oauth.nil?
250
+ Aspera.assert(@auth_params[:type].eql?(:oauth2)){'no OAuth defined'}
251
+ oauth_parameters = @auth_params.reject { |k, _v| k.eql?(:type) }
252
+ Log.log.debug{Log.dump('oauth parameters', oauth_parameters)}
253
+ @oauth = OAuth::Factory.instance.create(**oauth_parameters)
254
+ end
255
+ return @oauth
256
+ end
257
+
258
+ def oauth_token(force_refresh: false)
259
+ Aspera.assert_values(force_refresh, [true, false])
260
+ return oauth.get_authorization(use_refresh_token: force_refresh)
261
+ end
262
+
208
263
  # HTTP/S REST call
209
- # call_data has keys:
210
- # :auth
211
- # :operation
212
- # :subpath
213
- # :headers
214
- # :json_params
215
- # :url_params
216
- # :www_body_params
217
- # :text_body_params
218
- # :save_to_file (filepath) default: nil
219
- # :return_error (bool) default: nil
220
- # :redirect_max (int) default: 0
221
- # :not_auth_codes (array) codes that trigger a refresh/regeneration of bearer token
222
- # ----
223
- # authentication (:auth) :
224
- # :type (:none, :basic, :oauth2, :url)
225
- # :username [:basic]
226
- # :password [:basic]
227
- # :url_query [:url] a hash
228
- # :* [:oauth2] see Oauth class
229
- def call(call_data)
230
- raise "Hash call parameter is required (#{call_data.class})" unless call_data.is_a?(Hash)
231
- call_data[:subpath] = '' if call_data[:subpath].nil?
232
- Log.log.debug{"accessing #{call_data[:subpath]}".red.bold.bg_green}
233
- call_data[:headers] ||= {}
234
- call_data[:headers]['User-Agent'] ||= @@global[:user_agent]
235
- # defaults from @params are overridden by call data
236
- call_data = @params.deep_merge(call_data)
237
- case call_data[:auth][:type]
264
+ # @param save_to_file (filepath)
265
+ # @param return_error (bool)
266
+ def call(
267
+ operation:,
268
+ subpath: nil,
269
+ json_params: nil,
270
+ url_params: nil,
271
+ www_body_params: nil,
272
+ text_body_params: nil,
273
+ save_to_file: nil,
274
+ return_error: false,
275
+ headers: nil
276
+ )
277
+ subpath = subpath.to_s if subpath.is_a?(Symbol)
278
+ subpath = '' if subpath.nil?
279
+ Aspera.assert_type(subpath, String)
280
+ if headers.nil?
281
+ headers = @headers.clone
282
+ else
283
+ h = headers
284
+ headers = @headers.clone
285
+ headers.merge!(h)
286
+ end
287
+ Aspera.assert_type(headers, Hash)
288
+ Log.log.debug{"#{operation} [#{subpath}]".red.bold.bg_green}
289
+ case @auth_params[:type]
238
290
  when :none
239
291
  # no auth
240
292
  when :basic
241
293
  Log.log.debug('using Basic auth')
242
294
  # done in build_req
243
295
  when :oauth2
244
- call_data[:headers]['Authorization'] = oauth_token unless call_data[:headers].key?('Authorization')
296
+ headers['Authorization'] = oauth_token unless headers.key?('Authorization')
245
297
  when :url
246
- call_data[:url_params] ||= {}
247
- call_data[:auth][:url_query].each do |key, value|
248
- call_data[:url_params][key] = value
298
+ url_params ||= {}
299
+ @auth_params[:url_query].each do |key, value|
300
+ url_params[key] = value
249
301
  end
250
- else raise "unsupported auth type: [#{call_data[:auth][:type]}]"
302
+ else Aspera.error_unexpected_value(@auth_params[:type])
251
303
  end
252
- req = build_request(call_data)
253
- Log.log.debug{"call_data = #{call_data}"}
254
304
  result = {http: nil}
255
305
  # start a block to be able to retry the actual HTTP request
256
306
  begin
307
+ req = build_request(
308
+ operation: operation, subpath: subpath, json_params: json_params, url_params: url_params, www_body_params: www_body_params,
309
+ text_body_params: text_body_params, headers: headers)
257
310
  # we try the call, and will retry only if oauth, as we can, first with refresh, and then re-auth if refresh is bad
258
311
  oauth_tries ||= 2
259
312
  # initialize with number of initial retries allowed, nil gives zero
260
- tries_remain_redirect = call_data[:redirect_max].to_i if tries_remain_redirect.nil?
313
+ tries_remain_redirect = @redirect_max.to_i if tries_remain_redirect.nil?
261
314
  Log.log.debug("send request (retries=#{tries_remain_redirect})")
262
315
  result_mime = nil
263
316
  file_saved = false
@@ -266,13 +319,13 @@ module Aspera
266
319
  result[:http] = response
267
320
  result_mime = (result[:http]['Content-Type'] || 'text/plain').split(';').first.downcase
268
321
  # JSON data needs to be parsed, in case it contains an error code
269
- if !call_data[:save_to_file].nil? &&
322
+ if !save_to_file.nil? &&
270
323
  result[:http].code.to_s.start_with?('2') &&
271
324
  !result[:http]['Content-Length'].nil? &&
272
325
  !JSON_DECODE.include?(result_mime)
273
326
  total_size = result[:http]['Content-Length'].to_i
274
327
  Log.log.debug('before write file')
275
- target_file = call_data[:save_to_file]
328
+ target_file = save_to_file
276
329
  # override user's path to path in header
277
330
  if !response['Content-Disposition'].nil? && (m = response['Content-Disposition'].match(/filename="([^"]+)"/))
278
331
  target_file = File.join(File.dirname(target_file), m[1])
@@ -305,13 +358,13 @@ module Aspera
305
358
  else # when 'text/plain'
306
359
  result[:http].body
307
360
  end
308
- Log.log.debug{Log.dump("result: parsed: #{result_mime}", result[:data])}
309
- Log.log.debug{"result: code=#{result[:http].code}"}
361
+ Log.log.debug{"result: code=#{result[:http].code} mime=#{result_mime}"}
362
+ Log.log.debug{Log.dump('data', result[:data])}
310
363
  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?
364
+ File.write(save_to_file, result[:http].body) unless file_saved || save_to_file.nil?
312
365
  rescue RestCallError => e
313
366
  # not authorized: oauth token expired
314
- if call_data[:not_auth_codes].include?(result[:http].code.to_s) && call_data[:auth][:type].eql?(:oauth2)
367
+ if @not_auth_codes.include?(result[:http].code.to_s) && @auth_params[:type].eql?(:oauth2)
315
368
  begin
316
369
  # try to use refresh token
317
370
  req['Authorization'] = oauth_token(force_refresh: true)
@@ -321,35 +374,28 @@ module Aspera
321
374
  # regenerate a brand new token
322
375
  req['Authorization'] = oauth_token(force_refresh: true)
323
376
  end
324
- Log.log.debug{"using new token=#{call_data[:headers]['Authorization']}"}
325
- retry unless (oauth_tries -= 1).zero?
377
+ Log.log.debug{"using new token=#{headers['Authorization']}"}
378
+ retry if (oauth_tries -= 1).nonzero?
326
379
  end # if oauth
327
380
  # redirect ? (any code beginning with 3)
328
- if tries_remain_redirect.positive? && e.response.is_a?(Net::HTTPRedirection)
381
+ if e.response.is_a?(Net::HTTPRedirection) && tries_remain_redirect.positive?
329
382
  tries_remain_redirect -= 1
330
- current_uri = URI.parse(call_data[:base_url])
383
+ current_uri = URI.parse(@base_url)
331
384
  new_url = e.response['location']
332
385
  # special case: relative redirect
333
386
  if URI.parse(new_url).host.nil?
334
387
  # 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
338
- Log.log.info{"URL is moved: #{new_url}"}
339
- redirection_uri = URI.parse(new_url)
340
- call_data[:base_url] = new_url
341
- call_data[:subpath] = ''
342
- if current_uri.host.eql?(redirection_uri.host) && current_uri.port.eql?(redirection_uri.port)
343
- req = build_request(call_data)
344
- retry
345
- else
346
- # change host
347
- Log.log.info{"Redirect changes host: #{current_uri.host} -> #{redirection_uri.host}"}
348
- return self.class.new(call_data).call(call_data)
388
+ Aspera.assert(new_url.start_with?('/')){"redirect location is relative: #{new_url}, but does not start with /."}
389
+ new_url = "#{current_uri.scheme}://#{current_uri.host}#{new_url}"
349
390
  end
391
+ # forwards the request to the new location
392
+ return self.class.new(base_url: new_url, redirect_max: tries_remain_redirect).call(
393
+ operation: operation, json_params: json_params,
394
+ url_params: url_params, www_body_params: www_body_params, text_body_params: text_body_params,
395
+ save_to_file: save_to_file, return_error: return_error, headers: headers)
350
396
  end
351
397
  # raise exception if could not retry and not return error in result
352
- raise e unless call_data[:return_error]
398
+ raise e unless return_error
353
399
  end # begin request
354
400
  Log.log.debug{"result=#{result}"}
355
401
  return result
@@ -361,36 +407,37 @@ module Aspera
361
407
 
362
408
  # @param encoding : one of: :json_params, :url_params
363
409
  def create(subpath, params, encoding=:json_params)
364
- return call({operation: 'POST', subpath: subpath, headers: {'Accept' => 'application/json'}, encoding => params})
410
+ return call(operation: 'POST', subpath: subpath, headers: {'Accept' => 'application/json'}, encoding => params)
365
411
  end
366
412
 
367
- def read(subpath, options=nil)
368
- return call({operation: 'GET', subpath: subpath, headers: {'Accept' => 'application/json'}, url_params: options})
413
+ def read(subpath, query=nil)
414
+ return call(operation: 'GET', subpath: subpath, headers: {'Accept' => 'application/json'}, url_params: query)
369
415
  end
370
416
 
371
417
  def update(subpath, params)
372
- return call({operation: 'PUT', subpath: subpath, headers: {'Accept' => 'application/json'}, json_params: params})
418
+ return call(operation: 'PUT', subpath: subpath, headers: {'Accept' => 'application/json'}, json_params: params)
373
419
  end
374
420
 
375
421
  def delete(subpath, params=nil)
376
- return call({operation: 'DELETE', subpath: subpath, headers: {'Accept' => 'application/json'}, url_params: params})
422
+ return call(operation: 'DELETE', subpath: subpath, headers: {'Accept' => 'application/json'}, url_params: params)
377
423
  end
378
424
 
379
425
  def cancel(subpath)
380
- return call({operation: 'CANCEL', subpath: subpath, headers: {'Accept' => 'application/json'}})
426
+ return call(operation: 'CANCEL', subpath: subpath, headers: {'Accept' => 'application/json'})
381
427
  end
382
428
 
383
- # Query by name and returns a single result, else it throws an exception (no or multiple results)
429
+ # Query entity by general search (read with parameter `q`)
430
+ # TODO: not generic enough ? move somewhere ? inheritance ?
384
431
  # @param subpath path of entity in API
385
432
  # @param search_name name of searched entity
386
- # @param options additional search options
387
- def lookup_by_name(subpath, search_name, options={})
388
- # returns entities whose name contains value (case insensitive)
389
- matching_items = read(subpath, options.merge({'q' => search_name}))[:data]
433
+ # @param query additional search query parameters
434
+ # @returns [Hash] A single entity matching the search, or an exception if not found or multiple found
435
+ def lookup_by_name(subpath, search_name, query={})
436
+ # returns entities matching the query (it matches against several fields in case insensitive way)
437
+ matching_items = read(subpath, query.merge({'q' => search_name}))[:data]
390
438
  # API style: {totalcount:, ...} cspell: disable-line
391
- # TODO: not generic enough ? move somewhere ? inheritance ?
392
439
  matching_items = matching_items[subpath] if matching_items.is_a?(Hash)
393
- raise "Internal error: expecting array, have #{matching_items.class}" unless matching_items.is_a?(Array)
440
+ Aspera.assert_type(matching_items, Array)
394
441
  case matching_items.length
395
442
  when 1 then return matching_items.first
396
443
  when 0 then raise %Q{#{ENTITY_NOT_FOUND} #{subpath}: "#{search_name}"}
@@ -90,6 +90,7 @@ module Aspera
90
90
  # @param msg one error message to add to list
91
91
  def add_error(call_context, type, msg)
92
92
  call_context[:messages].push(msg)
93
+ Log.log.trace1{"Found error: #{type}: #{msg}"}
93
94
  log_file = instance.log_file
94
95
  # log error for further analysis (file must exist to activate)
95
96
  return if log_file.nil? || !File.exist?(log_file)
@@ -15,12 +15,14 @@ module Aspera
15
15
  RestErrorAnalyzer.instance.add_simple_handler(name: 'Type 1: error:user_message', path: %w[error user_message], always: true)
16
16
  RestErrorAnalyzer.instance.add_simple_handler(name: 'Type 2: error:description', path: %w[error description])
17
17
  RestErrorAnalyzer.instance.add_simple_handler(name: 'Type 3: error:internal_message', path: %w[error internal_message])
18
- # AoC Automation
19
- RestErrorAnalyzer.instance.add_simple_handler(name: 'AoC Automation', path: ['error'])
20
18
  RestErrorAnalyzer.instance.add_simple_handler(name: 'Type 5', path: ['error_description'])
21
19
  RestErrorAnalyzer.instance.add_simple_handler(name: 'Type 6', path: ['message'])
20
+ # AoC Automation
21
+ RestErrorAnalyzer.instance.add_simple_handler(name: 'AoC Automation', path: ['error'])
22
22
  RestErrorAnalyzer.instance.add_handler('Type 7: errors[]') do |type, call_context|
23
23
  next unless call_context[:data].is_a?(Hash) && call_context[:data]['errors'].is_a?(Hash)
24
+ # special for Shares: false positive ? (update global transfer_settings)
25
+ next if call_context[:data].key?('min_connect_version')
24
26
  call_context[:data]['errors'].each do |k, v|
25
27
  RestErrorAnalyzer.add_error(call_context, type, "#{k}: #{v}")
26
28
  end
@@ -31,7 +33,7 @@ module Aspera
31
33
  d_t_s = call_context[:data]['transfer_specs']
32
34
  next unless d_t_s.is_a?(Array)
33
35
  d_t_s.each do |res|
34
- r_err = res.dig(*%w[transfer_spec error])
36
+ r_err = res.dig(*%w[transfer_spec error]) || res['error']
35
37
  next unless r_err.is_a?(Hash)
36
38
  RestErrorAnalyzer.add_error(call_context, type, "#{r_err['code']}: #{r_err['reason']}: #{r_err['user_message']}")
37
39
  end
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'singleton'
4
+ require 'aspera/log'
5
+ require 'aspera/assert'
6
+
7
+ module Aspera
8
+ # implements a simple resume policy
9
+ class Resumer
10
+ # list of supported parameters and default values
11
+ DEFAULTS = {
12
+ iter_max: 7,
13
+ sleep_initial: 2,
14
+ sleep_factor: 2,
15
+ sleep_max: 60
16
+ }.freeze
17
+
18
+ # @param params see DEFAULTS
19
+ def initialize(params=nil)
20
+ @parameters = DEFAULTS.dup
21
+ if !params.nil?
22
+ Aspera.assert_type(params, Hash)
23
+ params.each do |k, v|
24
+ Aspera.assert_values(k, DEFAULTS.keys){'resume parameter'}
25
+ Aspera.assert_type(v, Integer){k}
26
+ @parameters[k] = v
27
+ end
28
+ end
29
+ Log.log.debug{"resume params=#{@parameters}"}
30
+ end
31
+
32
+ # calls block a number of times (resumes) until success or limit reached
33
+ # this is re-entrant, one resumer can handle multiple transfers in //
34
+ def execute_with_resume
35
+ Aspera.assert(block_given?)
36
+ # maximum of retry
37
+ remaining_resumes = @parameters[:iter_max]
38
+ sleep_seconds = @parameters[:sleep_initial]
39
+ Log.log.debug{"retries=#{remaining_resumes}"}
40
+ # try to send the file until ascp is successful
41
+ loop do
42
+ Log.log.debug('transfer starting')
43
+ begin
44
+ # call provided block
45
+ yield
46
+ # exit retry loop if success
47
+ break
48
+ rescue Transfer::Error => e
49
+ Log.log.warn{"An error occurred during transfer: #{e.message}"}
50
+ # failure in ascp
51
+ if e.retryable?
52
+ # exit if we exceed the max number of retry
53
+ raise Transfer::Error, "Maximum number of retry reached (#{@parameters[:iter_max]})" if remaining_resumes <= 0
54
+ else
55
+ # give one chance only to non retryable errors
56
+ unless remaining_resumes.eql?(@parameters[:iter_max])
57
+ Log.log.error('non-retryable error'.red.blink)
58
+ raise e
59
+ end
60
+ end
61
+ end
62
+
63
+ # take this retry in account
64
+ remaining_resumes -= 1
65
+ Log.log.warn{"Resuming in #{sleep_seconds} seconds (retry left:#{remaining_resumes})"}
66
+
67
+ # wait a bit before retrying, maybe network condition will be better
68
+ sleep(sleep_seconds)
69
+
70
+ # increase retry period
71
+ sleep_seconds *= @parameters[:sleep_factor]
72
+ # cap value
73
+ sleep_seconds = @parameters[:sleep_max] if sleep_seconds > @parameters[:sleep_max]
74
+ end # loop
75
+ end
76
+ end
77
+ end
@@ -6,6 +6,8 @@ require 'logger'
6
6
  module Aspera
7
7
  # remove secret from logs and output
8
8
  class SecretHider
9
+ # configurable:
10
+ ADDITIONAL_KEYS_TO_HIDE = []
9
11
  # display string for hidden secrets
10
12
  HIDDEN_PASSWORD = '🔑'
11
13
  # env vars for ascp with secrets
@@ -14,7 +16,7 @@ module Aspera
14
16
  KEY_SECRETS = %w[password secret passphrase _key apikey crn token].freeze
15
17
  HTTP_SECRETS = %w[Authorization].freeze
16
18
  ALL_SECRETS = [ASCP_ENV_SECRETS, KEY_SECRETS, HTTP_SECRETS].flatten.freeze
17
- KEY_FALSE_POSITIVES = [/^access_key$/].freeze
19
+ KEY_FALSE_POSITIVES = [/^access_key$/, /^fallback_private_key$/].freeze
18
20
  # regex that define named captures :begin and :end
19
21
  REGEX_LOG_REPLACES = [
20
22
  # CLI manager get/set options
@@ -32,7 +34,7 @@ module Aspera
32
34
  # cred in http dump
33
35
  /(?<begin>(?:#{HTTP_SECRETS.join('|')}): )[^\\]+(?<end>\\)/i
34
36
  ].freeze
35
- private_constant :HIDDEN_PASSWORD, :ASCP_ENV_SECRETS, :KEY_SECRETS, :ALL_SECRETS, :REGEX_LOG_REPLACES
37
+ private_constant :HIDDEN_PASSWORD, :ASCP_ENV_SECRETS, :KEY_SECRETS, :HTTP_SECRETS, :ALL_SECRETS, :KEY_FALSE_POSITIVES, :REGEX_LOG_REPLACES
36
38
  @log_secrets = false
37
39
  class << self
38
40
  attr_accessor :log_secrets
@@ -56,6 +58,7 @@ module Aspera
56
58
  return false unless keyword.is_a?(String) && value.is_a?(String)
57
59
  # those are not secrets
58
60
  return false if KEY_FALSE_POSITIVES.any?{|f|f.match?(keyword)}
61
+ return true if ADDITIONAL_KEYS_TO_HIDE.include?(keyword)
59
62
  # check if keyword (name) contains an element that designate it as a secret
60
63
  ALL_SECRETS.any?{|kw|keyword.include?(kw)}
61
64
  end
data/lib/aspera/ssh.rb CHANGED
@@ -2,15 +2,22 @@
2
2
 
3
3
  require 'net/ssh'
4
4
 
5
- # HACK: deactivate ed25519 and ecdsa private keys from ssh identities, as it usually cause problems
6
- old_verbose = $VERBOSE
7
- $VERBOSE = nil
8
- begin
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
10
- rescue StandardError
11
- # ignore errors
5
+ if ENV.fetch('ASCLI_ENABLE_ED25519', 'false').eql?('false')
6
+ # HACK: deactivate ed25519 and ecdsa private keys from SSH identities, as it usually causes problems
7
+ old_verbose = $VERBOSE
8
+ $VERBOSE = nil
9
+ begin
10
+ 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
11
+ rescue StandardError
12
+ # ignore errors
13
+ end
14
+ $VERBOSE = old_verbose
15
+ end
16
+
17
+ if RUBY_ENGINE == 'jruby' && ENV.fetch('ASCLI_ENABLE_ECDSHA2', 'false').eql?('false')
18
+ Net::SSH::Transport::Algorithms::ALGORITHMS.each_value { |a| a.reject! { |a| a =~ /^ecd(sa|h)-sha2/ } }
19
+ Net::SSH::KnownHosts::SUPPORTED_TYPE.reject! { |t| t =~ /^ecd(sa|h)-sha2/ }
12
20
  end
13
- $VERBOSE = old_verbose
14
21
 
15
22
  module Aspera
16
23
  # A simple wrapper around Net::SSH