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.
- checksums.yaml +4 -4
- checksums.yaml.gz.sig +0 -0
- data/BUGS.md +29 -3
- data/CHANGELOG.md +375 -280
- data/CONTRIBUTING.md +71 -18
- data/README.md +1978 -1656
- data/bin/ascli +13 -31
- data/bin/asession +32 -22
- data/examples/dascli +2 -2
- data/lib/aspera/agent/alpha.rb +117 -0
- data/lib/aspera/agent/base.rb +61 -0
- data/lib/aspera/{fasp/agent_connect.rb → agent/connect.rb} +13 -11
- data/lib/aspera/{fasp/agent_direct.rb → agent/direct.rb} +116 -116
- data/lib/aspera/{fasp/agent_httpgw.rb → agent/httpgw.rb} +21 -19
- data/lib/aspera/{fasp/agent_node.rb → agent/node.rb} +21 -33
- data/lib/aspera/agent/trsdk.rb +188 -0
- data/lib/aspera/api/aoc.rb +586 -0
- data/lib/aspera/api/ats.rb +46 -0
- data/lib/aspera/api/cos_node.rb +95 -0
- data/lib/aspera/api/node.rb +344 -0
- data/lib/aspera/ascmd.rb +47 -14
- data/lib/aspera/{fasp → ascp}/installation.rb +54 -15
- data/lib/aspera/{fasp → ascp}/management.rb +14 -14
- data/lib/aspera/{fasp → ascp}/products.rb +1 -1
- data/lib/aspera/assert.rb +45 -0
- data/lib/aspera/cli/basic_auth_plugin.rb +11 -10
- data/lib/aspera/cli/extended_value.rb +5 -5
- data/lib/aspera/cli/formatter.rb +27 -14
- data/lib/aspera/cli/hints.rb +7 -6
- data/lib/aspera/cli/main.rb +49 -29
- data/lib/aspera/cli/manager.rb +46 -36
- data/lib/aspera/cli/plugin.rb +34 -20
- data/lib/aspera/cli/plugin_factory.rb +61 -0
- data/lib/aspera/cli/plugins/alee.rb +7 -7
- data/lib/aspera/cli/plugins/aoc.rb +168 -132
- data/lib/aspera/cli/plugins/ats.rb +33 -33
- data/lib/aspera/cli/plugins/bss.rb +3 -4
- data/lib/aspera/cli/plugins/config.rb +250 -272
- data/lib/aspera/cli/plugins/console.rb +8 -6
- data/lib/aspera/cli/plugins/cos.rb +20 -19
- data/lib/aspera/cli/plugins/faspex.rb +71 -60
- data/lib/aspera/cli/plugins/faspex5.rb +212 -133
- data/lib/aspera/cli/plugins/node.rb +83 -75
- data/lib/aspera/cli/plugins/orchestrator.rb +36 -44
- data/lib/aspera/cli/plugins/preview.rb +33 -31
- data/lib/aspera/cli/plugins/server.rb +33 -32
- data/lib/aspera/cli/plugins/shares.rb +39 -33
- data/lib/aspera/cli/sync_actions.rb +9 -9
- data/lib/aspera/cli/transfer_agent.rb +45 -25
- data/lib/aspera/cli/transfer_progress.rb +2 -3
- data/lib/aspera/cli/version.rb +1 -1
- data/lib/aspera/colors.rb +5 -0
- data/lib/aspera/command_line_builder.rb +16 -14
- data/lib/aspera/coverage.rb +21 -0
- data/lib/aspera/data_repository.rb +33 -2
- data/lib/aspera/environment.rb +5 -4
- data/lib/aspera/faspex_gw.rb +13 -11
- data/lib/aspera/faspex_postproc.rb +6 -5
- data/lib/aspera/id_generator.rb +4 -2
- data/lib/aspera/json_rpc.rb +10 -8
- data/lib/aspera/keychain/encrypted_hash.rb +46 -11
- data/lib/aspera/keychain/macos_security.rb +29 -22
- data/lib/aspera/log.rb +5 -4
- data/lib/aspera/nagios.rb +7 -2
- data/lib/aspera/node_simulator.rb +213 -0
- data/lib/aspera/oauth/base.rb +143 -0
- data/lib/aspera/oauth/factory.rb +124 -0
- data/lib/aspera/oauth/generic.rb +34 -0
- data/lib/aspera/oauth/jwt.rb +51 -0
- data/lib/aspera/oauth/url_json.rb +31 -0
- data/lib/aspera/oauth/web.rb +50 -0
- data/lib/aspera/oauth.rb +5 -328
- data/lib/aspera/open_application.rb +7 -7
- data/lib/aspera/persistency_action_once.rb +13 -14
- data/lib/aspera/persistency_folder.rb +3 -2
- data/lib/aspera/preview/file_types.rb +53 -267
- data/lib/aspera/preview/generator.rb +7 -5
- data/lib/aspera/preview/terminal.rb +17 -7
- data/lib/aspera/preview/utils.rb +8 -7
- data/lib/aspera/proxy_auto_config.rb +6 -3
- data/lib/aspera/rest.rb +187 -140
- data/lib/aspera/rest_error_analyzer.rb +1 -0
- data/lib/aspera/rest_errors_aspera.rb +5 -3
- data/lib/aspera/resumer.rb +77 -0
- data/lib/aspera/secret_hider.rb +5 -2
- data/lib/aspera/ssh.rb +15 -8
- data/lib/aspera/temp_file_manager.rb +1 -1
- data/lib/aspera/{fasp → transfer}/error.rb +3 -3
- data/lib/aspera/{fasp → transfer}/error_info.rb +1 -1
- data/lib/aspera/{fasp → transfer}/faux_file.rb +1 -1
- data/lib/aspera/{fasp → transfer}/parameters.rb +95 -120
- data/lib/aspera/{fasp/transfer_spec.rb → transfer/spec.rb} +23 -19
- data/lib/aspera/{fasp/parameters.yaml → transfer/spec.yaml} +4 -99
- data/lib/aspera/transfer/sync.rb +273 -0
- data/lib/aspera/{fasp → transfer}/uri.rb +10 -9
- data/lib/aspera/web_server_simple.rb +12 -3
- data.tar.gz.sig +0 -0
- metadata +92 -68
- metadata.gz.sig +0 -0
- data/lib/aspera/aoc.rb +0 -606
- data/lib/aspera/ats_api.rb +0 -47
- data/lib/aspera/cos_node.rb +0 -93
- data/lib/aspera/fasp/agent_aspera.rb +0 -126
- data/lib/aspera/fasp/agent_base.rb +0 -48
- data/lib/aspera/fasp/agent_trsdk.rb +0 -146
- data/lib/aspera/fasp/resume_policy.rb +0 -77
- data/lib/aspera/node.rb +0 -338
- 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,
|
|
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
|
-
|
|
62
|
-
return uri if
|
|
63
|
-
Log.log.debug{Log.dump('
|
|
64
|
-
|
|
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
|
-
|
|
68
|
+
query_hash.each do |k, v|
|
|
67
69
|
case v
|
|
68
70
|
when Array
|
|
69
|
-
# support array
|
|
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
|
-
#
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(@
|
|
151
|
+
@http_session = self.class.start_http_session(@base_url)
|
|
133
152
|
end
|
|
134
153
|
return @http_session
|
|
135
154
|
end
|
|
136
155
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
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
|
-
|
|
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(
|
|
172
|
+
req = Net::HTTP.const_get(operation.capitalize).new(uri)
|
|
178
173
|
rescue NameError
|
|
179
|
-
raise "unsupported operation : #{
|
|
174
|
+
raise "unsupported operation : #{operation}"
|
|
180
175
|
end
|
|
181
|
-
if
|
|
182
|
-
req.body = JSON.generate(
|
|
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
|
|
188
|
-
req.body = URI.encode_www_form(
|
|
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
|
|
193
|
-
req.body =
|
|
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
|
-
|
|
198
|
-
|
|
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(
|
|
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
|
-
#
|
|
210
|
-
#
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
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
|
-
|
|
296
|
+
headers['Authorization'] = oauth_token unless headers.key?('Authorization')
|
|
245
297
|
when :url
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
298
|
+
url_params ||= {}
|
|
299
|
+
@auth_params[:url_query].each do |key, value|
|
|
300
|
+
url_params[key] = value
|
|
249
301
|
end
|
|
250
|
-
else
|
|
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 =
|
|
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 !
|
|
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 =
|
|
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{
|
|
309
|
-
Log.log.debug{
|
|
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(
|
|
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
|
|
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=#{
|
|
325
|
-
retry
|
|
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
|
|
381
|
+
if e.response.is_a?(Net::HTTPRedirection) && tries_remain_redirect.positive?
|
|
329
382
|
tries_remain_redirect -= 1
|
|
330
|
-
current_uri = URI.parse(
|
|
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
|
-
|
|
336
|
-
new_url = current_uri.scheme
|
|
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
|
|
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(
|
|
410
|
+
return call(operation: 'POST', subpath: subpath, headers: {'Accept' => 'application/json'}, encoding => params)
|
|
365
411
|
end
|
|
366
412
|
|
|
367
|
-
def read(subpath,
|
|
368
|
-
return call(
|
|
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(
|
|
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(
|
|
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(
|
|
426
|
+
return call(operation: 'CANCEL', subpath: subpath, headers: {'Accept' => 'application/json'})
|
|
381
427
|
end
|
|
382
428
|
|
|
383
|
-
# Query by
|
|
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
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
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
|
-
|
|
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
|
data/lib/aspera/secret_hider.rb
CHANGED
|
@@ -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
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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
|