aspera-cli 4.15.0 → 4.16.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 +292 -228
- data/CONTRIBUTING.md +69 -18
- data/README.md +1102 -952
- data/bin/ascli +13 -31
- data/bin/asession +3 -1
- data/examples/dascli +2 -2
- data/lib/aspera/aoc.rb +28 -33
- data/lib/aspera/ascmd.rb +3 -6
- data/lib/aspera/assert.rb +45 -0
- data/lib/aspera/cli/extended_value.rb +5 -5
- data/lib/aspera/cli/formatter.rb +26 -13
- data/lib/aspera/cli/hints.rb +4 -3
- data/lib/aspera/cli/main.rb +16 -3
- data/lib/aspera/cli/manager.rb +45 -36
- data/lib/aspera/cli/plugin.rb +20 -13
- data/lib/aspera/cli/plugins/aoc.rb +103 -73
- data/lib/aspera/cli/plugins/ats.rb +4 -3
- data/lib/aspera/cli/plugins/config.rb +114 -119
- data/lib/aspera/cli/plugins/cos.rb +2 -2
- data/lib/aspera/cli/plugins/faspex.rb +23 -19
- data/lib/aspera/cli/plugins/faspex5.rb +75 -43
- data/lib/aspera/cli/plugins/node.rb +28 -15
- data/lib/aspera/cli/plugins/orchestrator.rb +4 -2
- data/lib/aspera/cli/plugins/preview.rb +9 -7
- data/lib/aspera/cli/plugins/server.rb +6 -3
- data/lib/aspera/cli/plugins/shares.rb +30 -26
- data/lib/aspera/cli/sync_actions.rb +9 -9
- data/lib/aspera/cli/transfer_agent.rb +21 -14
- data/lib/aspera/cli/transfer_progress.rb +2 -3
- data/lib/aspera/cli/version.rb +1 -1
- data/lib/aspera/command_line_builder.rb +13 -11
- data/lib/aspera/cos_node.rb +3 -2
- data/lib/aspera/coverage.rb +22 -0
- data/lib/aspera/data_repository.rb +33 -2
- data/lib/aspera/environment.rb +4 -2
- data/lib/aspera/fasp/{agent_aspera.rb → agent_alpha.rb} +29 -39
- data/lib/aspera/fasp/agent_base.rb +17 -7
- data/lib/aspera/fasp/agent_direct.rb +88 -84
- data/lib/aspera/fasp/agent_httpgw.rb +4 -3
- data/lib/aspera/fasp/agent_node.rb +3 -2
- data/lib/aspera/fasp/agent_trsdk.rb +79 -37
- data/lib/aspera/fasp/installation.rb +51 -12
- data/lib/aspera/fasp/management.rb +11 -6
- data/lib/aspera/fasp/parameters.rb +53 -47
- data/lib/aspera/fasp/resume_policy.rb +7 -5
- data/lib/aspera/fasp/sync.rb +273 -0
- data/lib/aspera/fasp/transfer_spec.rb +10 -8
- data/lib/aspera/fasp/uri.rb +2 -2
- data/lib/aspera/faspex_gw.rb +11 -8
- data/lib/aspera/faspex_postproc.rb +6 -5
- data/lib/aspera/id_generator.rb +3 -1
- data/lib/aspera/json_rpc.rb +10 -8
- data/lib/aspera/keychain/encrypted_hash.rb +46 -11
- data/lib/aspera/keychain/macos_security.rb +15 -13
- data/lib/aspera/log.rb +4 -3
- data/lib/aspera/nagios.rb +7 -2
- data/lib/aspera/node.rb +17 -16
- data/lib/aspera/node_simulator.rb +214 -0
- data/lib/aspera/oauth.rb +22 -19
- 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 +14 -5
- data/lib/aspera/preview/utils.rb +8 -7
- data/lib/aspera/proxy_auto_config.rb +6 -3
- data/lib/aspera/rest.rb +29 -13
- data/lib/aspera/rest_error_analyzer.rb +1 -0
- data/lib/aspera/rest_errors_aspera.rb +2 -0
- data/lib/aspera/secret_hider.rb +5 -2
- data/lib/aspera/ssh.rb +10 -8
- data/lib/aspera/temp_file_manager.rb +1 -1
- data/lib/aspera/web_server_simple.rb +2 -1
- data.tar.gz.sig +0 -0
- metadata +96 -45
- metadata.gz.sig +0 -0
- data/lib/aspera/sync.rb +0 -219
@@ -0,0 +1,273 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# cspell:words logdir bidi watchd cooloff asyncadmin
|
4
|
+
|
5
|
+
require 'aspera/command_line_builder'
|
6
|
+
require 'aspera/fasp/installation'
|
7
|
+
require 'aspera/log'
|
8
|
+
require 'aspera/assert'
|
9
|
+
require 'json'
|
10
|
+
require 'base64'
|
11
|
+
require 'open3'
|
12
|
+
require 'English'
|
13
|
+
|
14
|
+
module Aspera
|
15
|
+
module Fasp
|
16
|
+
# builds command line arg for async
|
17
|
+
module Sync
|
18
|
+
# sync direction, default is push
|
19
|
+
DIRECTIONS = %i[push pull bidi].freeze
|
20
|
+
# custom JSON for async instance command line options
|
21
|
+
PARAMS_VX_INSTANCE =
|
22
|
+
{
|
23
|
+
'alt_logdir' => { cli: { type: :opt_with_arg}, accepted_types: :string},
|
24
|
+
'watchd' => { cli: { type: :opt_with_arg}, accepted_types: :string},
|
25
|
+
'apply_local_docroot' => { cli: { type: :opt_without_arg}},
|
26
|
+
'quiet' => { cli: { type: :opt_without_arg}},
|
27
|
+
'ws_connect' => { cli: { type: :opt_without_arg}}
|
28
|
+
}.freeze
|
29
|
+
|
30
|
+
# map sync session parameters to transfer spec: sync -> ts, true if same
|
31
|
+
PARAMS_VX_SESSION =
|
32
|
+
{
|
33
|
+
'name' => { cli: { type: :opt_with_arg}, accepted_types: :string},
|
34
|
+
'local_dir' => { cli: { type: :opt_with_arg}, accepted_types: :string},
|
35
|
+
'remote_dir' => { cli: { type: :opt_with_arg}, accepted_types: :string},
|
36
|
+
'local_db_dir' => { cli: { type: :opt_with_arg}, accepted_types: :string},
|
37
|
+
'remote_db_dir' => { cli: { type: :opt_with_arg}, accepted_types: :string},
|
38
|
+
'host' => { cli: { type: :opt_with_arg}, accepted_types: :string, ts: :remote_host},
|
39
|
+
'user' => { cli: { type: :opt_with_arg}, accepted_types: :string, ts: :remote_user},
|
40
|
+
'private_key_paths' => { cli: { type: :opt_with_arg, switch: '--private-key-path'}, accepted_types: :array},
|
41
|
+
'direction' => { cli: { type: :opt_with_arg}, accepted_types: :string},
|
42
|
+
'checksum' => { cli: { type: :opt_with_arg}, accepted_types: :string},
|
43
|
+
'tags' => { cli: { type: :opt_with_arg, switch: '--tags64', convert: 'Aspera::Fasp::Parameters.convert_json64'},
|
44
|
+
accepted_types: :hash, ts: true},
|
45
|
+
'tcp_port' => { cli: { type: :opt_with_arg}, accepted_types: :int, ts: :ssh_port},
|
46
|
+
'rate_policy' => { cli: { type: :opt_with_arg}, accepted_types: :string},
|
47
|
+
'target_rate' => { cli: { type: :opt_with_arg}, accepted_types: :string},
|
48
|
+
'cooloff' => { cli: { type: :opt_with_arg}, accepted_types: :int},
|
49
|
+
'pending_max' => { cli: { type: :opt_with_arg}, accepted_types: :int},
|
50
|
+
'scan_intensity' => { cli: { type: :opt_with_arg}, accepted_types: :string},
|
51
|
+
'cipher' => { cli: { type: :opt_with_arg, convert: 'Aspera::Fasp::Parameters.convert_remove_hyphen'}, accepted_types: :string, ts: true},
|
52
|
+
'transfer_threads' => { cli: { type: :opt_with_arg}, accepted_types: :int},
|
53
|
+
'preserve_time' => { cli: { type: :opt_without_arg}, ts: :preserve_times},
|
54
|
+
'preserve_access_time' => { cli: { type: :opt_without_arg}, ts: nil},
|
55
|
+
'preserve_modification_time' => { cli: { type: :opt_without_arg}, ts: nil},
|
56
|
+
'preserve_uid' => { cli: { type: :opt_without_arg}, ts: :preserve_file_owner_uid},
|
57
|
+
'preserve_gid' => { cli: { type: :opt_without_arg}, ts: :preserve_file_owner_gid},
|
58
|
+
'create_dir' => { cli: { type: :opt_without_arg}, ts: true},
|
59
|
+
'reset' => { cli: { type: :opt_without_arg}},
|
60
|
+
# NOTE: only one env var, but multiple sessions... could be a problem
|
61
|
+
'remote_password' => { cli: { type: :envvar, variable: 'ASPERA_SCP_PASS'}, ts: true},
|
62
|
+
'cookie' => { cli: { type: :envvar, variable: 'ASPERA_SCP_COOKIE'}, ts: true},
|
63
|
+
'token' => { cli: { type: :envvar, variable: 'ASPERA_SCP_TOKEN'}, ts: true},
|
64
|
+
'license' => { cli: { type: :envvar, variable: 'ASPERA_SCP_LICENSE'}}
|
65
|
+
}.freeze
|
66
|
+
|
67
|
+
Aspera::CommandLineBuilder.normalize_description(PARAMS_VX_INSTANCE)
|
68
|
+
Aspera::CommandLineBuilder.normalize_description(PARAMS_VX_SESSION)
|
69
|
+
|
70
|
+
PARAMS_VX_KEYS = %w[instance sessions].freeze
|
71
|
+
|
72
|
+
# Translation of transfer spec parameters to async v2 API (asyncs)
|
73
|
+
TS_TO_PARAMS_V2 = {
|
74
|
+
'remote_host' => 'remote.host',
|
75
|
+
'remote_user' => 'remote.user',
|
76
|
+
'remote_password' => 'remote.pass',
|
77
|
+
'sshfp' => 'remote.fingerprint',
|
78
|
+
'ssh_port' => 'remote.port',
|
79
|
+
'wss_port' => 'remote.ws_port',
|
80
|
+
'proxy' => 'remote.proxy',
|
81
|
+
'token' => 'remote.token',
|
82
|
+
'tags' => 'tags'
|
83
|
+
}.freeze
|
84
|
+
|
85
|
+
ASYNC_EXECUTABLE = 'async'
|
86
|
+
ASYNC_ADMIN_EXECUTABLE = 'asyncadmin'
|
87
|
+
|
88
|
+
private_constant :PARAMS_VX_INSTANCE, :PARAMS_VX_SESSION, :PARAMS_VX_KEYS, :TS_TO_PARAMS_V2, :ASYNC_EXECUTABLE, :ASYNC_ADMIN_EXECUTABLE
|
89
|
+
|
90
|
+
class << self
|
91
|
+
# Set remote_dir in sync parameters based on transfer spec
|
92
|
+
# @param params [Hash] sync parameters, old or new format
|
93
|
+
# @param remote_dir_key [String] key to update in above hash
|
94
|
+
# @param transfer_spec [Hash] transfer spec
|
95
|
+
def update_remote_dir(sync_params, remote_dir_key, transfer_spec)
|
96
|
+
if transfer_spec.dig(*%w[tags aspera node file_id])
|
97
|
+
# in AoC, use gen4
|
98
|
+
sync_params[remote_dir_key] = '/'
|
99
|
+
elsif transfer_spec['cookie']&.start_with?('aspera.shares2')
|
100
|
+
# TODO : something more generic, independent of Shares
|
101
|
+
# in Shares, the actual folder on remote end is not always the same as the name of the share
|
102
|
+
actual_remote = transfer_spec['paths']&.first&.[]('source')
|
103
|
+
sync_params[remote_dir_key] = actual_remote if actual_remote
|
104
|
+
end
|
105
|
+
nil
|
106
|
+
end
|
107
|
+
|
108
|
+
def remote_certificates(remote)
|
109
|
+
certificates_to_use = []
|
110
|
+
# use web socket secure for session ?
|
111
|
+
if remote['connect_mode']&.eql?('ws')
|
112
|
+
remote.delete('port')
|
113
|
+
remote.delete('fingerprint')
|
114
|
+
# ignore cert for wss ?
|
115
|
+
if false # @options[:check_ignore]&.call(remote['host'], remote['ws_port'])
|
116
|
+
wss_cert_file = TempFileManager.instance.new_file_path_global('wss_cert')
|
117
|
+
wss_url = "https://#{remote['host']}:#{remote['ws_port']}"
|
118
|
+
File.write(wss_cert_file, Rest.remote_certificates(wss_url))
|
119
|
+
certificates_to_use.push(wss_cert_file)
|
120
|
+
end
|
121
|
+
# set location for CA bundle to be the one of Ruby, see env var SSL_CERT_FILE / SSL_CERT_DIR
|
122
|
+
# certificates_to_use.concat(@options[:trusted_certs]) if @options[:trusted_certs]
|
123
|
+
else
|
124
|
+
# remove unused parameter (avoid warning)
|
125
|
+
remote.delete('ws_port')
|
126
|
+
# add SSH bypass keys when authentication is token and no auth is provided
|
127
|
+
if remote.key?('token') && !remote.key?('pass')
|
128
|
+
certificates_to_use.concat(Installation.instance.aspera_token_ssh_key_paths)
|
129
|
+
end
|
130
|
+
end
|
131
|
+
return certificates_to_use
|
132
|
+
end
|
133
|
+
|
134
|
+
# @param sync_params [Hash] sync parameters, old or new format
|
135
|
+
# @param block [nil, Proc] block to generate transfer spec, takes: direction (one of DIRECTIONS), local_dir, remote_dir
|
136
|
+
def start(sync_params, &block)
|
137
|
+
assert_type(sync_params, Hash)
|
138
|
+
env_args = {
|
139
|
+
args: [],
|
140
|
+
env: {}
|
141
|
+
}
|
142
|
+
if sync_params.key?('local')
|
143
|
+
remote = sync_params['remote']
|
144
|
+
# async native JSON format (v2)
|
145
|
+
assert_type(remote, Hash)
|
146
|
+
# get transfer spec if possible, and feed back to new structure
|
147
|
+
if block
|
148
|
+
transfer_spec = yield((sync_params['direction'] || 'push').to_sym, sync_params['local']['path'], remote['path'])
|
149
|
+
# async native JSON format
|
150
|
+
assert_type(sync_params['local'], Hash)
|
151
|
+
# translate transfer spec to async parameters
|
152
|
+
TS_TO_PARAMS_V2.each do |ts_param, sy_path|
|
153
|
+
next unless transfer_spec.key?(ts_param)
|
154
|
+
sy_dig = sy_path.split('.')
|
155
|
+
param = sy_dig.pop
|
156
|
+
hash = sy_dig.empty? ? sync_params : sync_params[sy_dig.first]
|
157
|
+
hash = sync_params[sy_dig.first] = {} if hash.nil?
|
158
|
+
hash[param] = transfer_spec[ts_param]
|
159
|
+
end
|
160
|
+
update_remote_dir(remote, 'path', transfer_spec)
|
161
|
+
end
|
162
|
+
remote['connect_mode'] ||= remote.key?('ws_port') ? 'ws' : 'ssh'
|
163
|
+
add_certificates = remote_certificates(remote)
|
164
|
+
if !add_certificates.empty?
|
165
|
+
remote['private_key_paths'] ||= []
|
166
|
+
remote['private_key_paths'].concat(add_certificates)
|
167
|
+
end
|
168
|
+
assert_type(sync_params, Hash)
|
169
|
+
env_args[:args] = ["--conf64=#{Base64.strict_encode64(JSON.generate(sync_params))}"]
|
170
|
+
elsif sync_params.key?('sessions')
|
171
|
+
# ascli JSON format (v1)
|
172
|
+
if block
|
173
|
+
sync_params['sessions'].each do |session|
|
174
|
+
transfer_spec = yield((session['direction'] || 'push').to_sym, session['local_dir'], session['remote_dir'])
|
175
|
+
PARAMS_VX_SESSION.each do |async_param, behavior|
|
176
|
+
if behavior.key?(:ts)
|
177
|
+
tspec_param = behavior[:ts].is_a?(TrueClass) ? async_param : behavior[:ts].to_s
|
178
|
+
session[async_param] ||= transfer_spec[tspec_param] if transfer_spec.key?(tspec_param)
|
179
|
+
end
|
180
|
+
end
|
181
|
+
session['private_key_paths'] = Fasp::Installation.instance.aspera_token_ssh_key_paths if transfer_spec.key?('token')
|
182
|
+
update_remote_dir(session, 'remote_dir', transfer_spec)
|
183
|
+
end
|
184
|
+
end
|
185
|
+
raise StandardError, "Only 'sessions', and optionally 'instance' keys are allowed" unless
|
186
|
+
sync_params.keys.push('instance').uniq.sort.eql?(PARAMS_VX_KEYS)
|
187
|
+
assert_type(sync_params['sessions'], Array)
|
188
|
+
assert_type(sync_params['sessions'].first, Hash)
|
189
|
+
if sync_params.key?('instance')
|
190
|
+
assert_type(sync_params['instance'], Hash)
|
191
|
+
instance_builder = Aspera::CommandLineBuilder.new(sync_params['instance'], PARAMS_VX_INSTANCE)
|
192
|
+
instance_builder.process_params
|
193
|
+
instance_builder.add_env_args(env_args)
|
194
|
+
end
|
195
|
+
|
196
|
+
sync_params['sessions'].each do |session_params|
|
197
|
+
assert_type(session_params, Hash)
|
198
|
+
assert(session_params.key?('name')){'session must contain at least name'}
|
199
|
+
session_builder = Aspera::CommandLineBuilder.new(session_params, PARAMS_VX_SESSION)
|
200
|
+
session_builder.process_params
|
201
|
+
session_builder.add_env_args(env_args)
|
202
|
+
end
|
203
|
+
else
|
204
|
+
raise 'At least one of `local` or `sessions` must be present in async parameters'
|
205
|
+
end
|
206
|
+
Log.log.debug{Log.dump(:sync_params, sync_params)}
|
207
|
+
Log.log.debug{"execute: #{env_args[:env].map{|k, v| "#{k}=\"#{v}\""}.join(' ')} \"#{ASYNC_EXECUTABLE}\" \"#{env_args[:args].join('" "')}\""}
|
208
|
+
res = system(env_args[:env], [ASYNC_EXECUTABLE, ASYNC_EXECUTABLE], *env_args[:args])
|
209
|
+
Log.log.debug{"result=#{res}"}
|
210
|
+
case res
|
211
|
+
when true then return nil
|
212
|
+
when false then raise "failed: #{$CHILD_STATUS}"
|
213
|
+
when nil then raise "not started: #{$CHILD_STATUS}"
|
214
|
+
else error_unexpected_value(res)
|
215
|
+
end
|
216
|
+
end
|
217
|
+
|
218
|
+
def parse_status(stdout)
|
219
|
+
Log.log.trace1{"stdout=#{stdout}"}
|
220
|
+
result = {}
|
221
|
+
ids = nil
|
222
|
+
stdout.split("\n").each do |line|
|
223
|
+
info = line.split(':', 2).map(&:lstrip)
|
224
|
+
if info[1].eql?('')
|
225
|
+
info[1] = ids = []
|
226
|
+
elsif info[1].nil?
|
227
|
+
ids.push(info[0])
|
228
|
+
next
|
229
|
+
end
|
230
|
+
result[info[0]] = info[1]
|
231
|
+
end
|
232
|
+
return result
|
233
|
+
end
|
234
|
+
|
235
|
+
def admin_status(sync_params, session_name)
|
236
|
+
command_line = [ASYNC_ADMIN_EXECUTABLE, '--quiet']
|
237
|
+
if sync_params.key?('local')
|
238
|
+
assert(!sync_params['name'].nil?){'Missing session name'}
|
239
|
+
assert(session_name.nil? || session_name.eql?(sync_params['name'])){'Session not found'}
|
240
|
+
command_line.push("--name=#{sync_params['name']}")
|
241
|
+
if sync_params.key?('local_db_dir')
|
242
|
+
command_line.push("--local-db-dir=#{sync_params['local_db_dir']}")
|
243
|
+
elsif sync_params.dig('local', 'path')
|
244
|
+
command_line.push("--local-dir=#{sync_params.dig('local', 'path')}")
|
245
|
+
else
|
246
|
+
raise 'Missing either local_db_dir or local.path'
|
247
|
+
end
|
248
|
+
elsif sync_params.key?('sessions')
|
249
|
+
session = session_name.nil? ? sync_params['sessions'].first : sync_params['sessions'].find{|s|s['name'].eql?(session_name)}
|
250
|
+
raise "Session #{session_name} not found in #{sync_params['sessions'].map{|s|s['name']}.join(',')}" if session.nil?
|
251
|
+
raise 'Missing session name' if session['name'].nil?
|
252
|
+
command_line.push("--name=#{session['name']}")
|
253
|
+
if session.key?('local_db_dir')
|
254
|
+
command_line.push("--local-db-dir=#{session['local_db_dir']}")
|
255
|
+
elsif session.key?('local_dir')
|
256
|
+
command_line.push("--local-dir=#{session['local_dir']}")
|
257
|
+
else
|
258
|
+
raise 'Missing either local_db_dir or local_dir'
|
259
|
+
end
|
260
|
+
else
|
261
|
+
raise 'At least one of `local` or `sessions` must be present in async parameters'
|
262
|
+
end
|
263
|
+
Log.log.debug{"execute: #{command_line.join(' ')}"}
|
264
|
+
stdout, stderr, status = Open3.capture3(*command_line)
|
265
|
+
Log.log.debug{"status=#{status}, stderr=#{stderr}"}
|
266
|
+
Log.log.trace1{"stdout=#{stdout}"}
|
267
|
+
raise "Sync failed: #{status.exitstatus} : #{stderr}" unless status.success?
|
268
|
+
return parse_status(stdout)
|
269
|
+
end
|
270
|
+
end # end self
|
271
|
+
end # end Sync
|
272
|
+
end # end Fasp
|
273
|
+
end # end Aspera
|
@@ -1,6 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require 'aspera/fasp/parameters'
|
4
|
+
require 'aspera/assert'
|
4
5
|
|
5
6
|
module Aspera
|
6
7
|
module Fasp
|
@@ -27,22 +28,23 @@ module Aspera
|
|
27
28
|
end
|
28
29
|
class << self
|
29
30
|
def action_to_direction(tspec, command)
|
30
|
-
|
31
|
+
assert_type(tspec, Hash){'transfer spec'}
|
31
32
|
tspec['direction'] = case command.to_sym
|
32
33
|
when :upload then DIRECTION_SEND
|
33
34
|
when :download then DIRECTION_RECEIVE
|
34
|
-
else
|
35
|
+
else error_unexpected_value(command.to_sym)
|
35
36
|
end
|
36
37
|
return tspec
|
37
38
|
end
|
38
39
|
|
39
40
|
def action(tspec)
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
41
|
+
assert_type(tspec, Hash){'transfer spec'}
|
42
|
+
assert_values(tspec['direction'], [DIRECTION_SEND, DIRECTION_RECEIVE]){'direction'}
|
43
|
+
case tspec['direction']
|
44
|
+
when DIRECTION_SEND then :upload
|
45
|
+
when DIRECTION_RECEIVE then :download
|
46
|
+
else error_unexpected_value(tspec['direction'])
|
47
|
+
end
|
46
48
|
end
|
47
49
|
end
|
48
50
|
end
|
data/lib/aspera/fasp/uri.rb
CHANGED
@@ -8,12 +8,12 @@ require 'aspera/command_line_builder'
|
|
8
8
|
|
9
9
|
module Aspera
|
10
10
|
module Fasp
|
11
|
-
# translates a "faspe:" URI (used in Faspex 4) into transfer spec
|
11
|
+
# translates a "faspe:" URI (used in Faspex 4) into transfer spec (Hash)
|
12
12
|
class Uri
|
13
13
|
SCHEME = 'faspe'
|
14
14
|
def initialize(fasp_link)
|
15
15
|
@fasp_uri = URI.parse(fasp_link.gsub(' ', '%20'))
|
16
|
-
# TODO: check scheme is faspe
|
16
|
+
# TODO: check scheme is 'faspe'
|
17
17
|
end
|
18
18
|
|
19
19
|
def transfer_spec
|
data/lib/aspera/faspex_gw.rb
CHANGED
@@ -1,15 +1,17 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require 'aspera/web_server_simple'
|
4
3
|
require 'aspera/log'
|
4
|
+
require 'aspera/assert'
|
5
|
+
require 'webrick'
|
5
6
|
require 'json'
|
6
7
|
|
7
8
|
module Aspera
|
8
|
-
#
|
9
|
+
# Simulate the Faspex 4 /send API and creates a package on Aspera on Cloud or Faspex 5
|
9
10
|
class Faspex4GWServlet < WEBrick::HTTPServlet::AbstractServlet
|
10
11
|
# @param app_api [Aspera::AoC]
|
11
12
|
# @param app_context [String]
|
12
13
|
def initialize(server, app_api, app_context)
|
14
|
+
assert_values(app_api.class.name, ['Aspera::AoC', 'Aspera::Rest'])
|
13
15
|
super(server)
|
14
16
|
# typed: Aspera::AoC
|
15
17
|
@app_api = app_api
|
@@ -67,13 +69,14 @@ module Aspera
|
|
67
69
|
raise 'no payload' if request.body.nil?
|
68
70
|
faspex_pkg_parameters = JSON.parse(request.body)
|
69
71
|
Log.log.debug{"faspex pkg create parameters=#{faspex_pkg_parameters}"}
|
72
|
+
# compare string, as class is not yet known here
|
70
73
|
faspex_package_create_result =
|
71
|
-
|
74
|
+
case @app_api.class.name
|
75
|
+
when 'Aspera::AoC'
|
72
76
|
faspex4_send_to_aoc(faspex_pkg_parameters)
|
73
|
-
|
77
|
+
when 'Aspera::Rest'
|
74
78
|
faspex4_send_to_faspex5(faspex_pkg_parameters)
|
75
|
-
else
|
76
|
-
raise "No such adapter: #{@app_api.class}"
|
79
|
+
else error_unexpected_value(@app_api.class.name)
|
77
80
|
end
|
78
81
|
Log.log.info{"faspex_package_create_result=#{faspex_package_create_result}"}
|
79
82
|
response.status = 200
|
@@ -89,8 +92,8 @@ module Aspera
|
|
89
92
|
else
|
90
93
|
response.status = 400
|
91
94
|
response['Content-Type'] = 'application/json'
|
92
|
-
response.body = {error: '
|
95
|
+
response.body = {error: 'Unsupported endpoint'}.to_json
|
93
96
|
end
|
94
97
|
end
|
95
98
|
end # Faspex4GWServlet
|
96
|
-
end #
|
99
|
+
end # Aspera
|
@@ -1,17 +1,18 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require 'English'
|
4
|
-
require 'aspera/web_server_simple'
|
5
|
-
require 'aspera/log'
|
6
3
|
require 'json'
|
7
4
|
require 'timeout'
|
5
|
+
require 'English'
|
6
|
+
require 'webrick'
|
7
|
+
require 'aspera/log'
|
8
|
+
require 'aspera/assert'
|
8
9
|
|
9
10
|
module Aspera
|
10
11
|
# this class answers the Faspex /send API and creates a package on Aspera on Cloud
|
11
12
|
class Faspex4PostProcServlet < WEBrick::HTTPServlet::AbstractServlet
|
12
13
|
ALLOWED_PARAMETERS = %i[root script_folder fail_on_error timeout_seconds].freeze
|
13
14
|
def initialize(server, parameters)
|
14
|
-
|
15
|
+
assert_type(parameters, Hash)
|
15
16
|
@parameters = parameters.symbolize_keys
|
16
17
|
Log.log.debug{Log.dump(:post_proc_parameters, @parameters)}
|
17
18
|
raise "unexpected key in parameters config: only: #{ALLOWED_PARAMETERS.join(', ')}" if @parameters.keys.any?{|k|!ALLOWED_PARAMETERS.include?(k)}
|
@@ -74,4 +75,4 @@ module Aspera
|
|
74
75
|
end
|
75
76
|
end
|
76
77
|
end # Faspex4PostProcServlet
|
77
|
-
end #
|
78
|
+
end # Aspera
|
data/lib/aspera/id_generator.rb
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require 'aspera/assert'
|
3
4
|
require 'uri'
|
4
5
|
|
5
6
|
module Aspera
|
@@ -11,11 +12,12 @@ module Aspera
|
|
11
12
|
class << self
|
12
13
|
def from_list(object_id)
|
13
14
|
if object_id.is_a?(Array)
|
15
|
+
# compact: remove nils
|
14
16
|
object_id = object_id.compact.map do |i|
|
15
17
|
i.is_a?(String) && i.start_with?('https://') ? URI.parse(i).host : i.to_s
|
16
18
|
end.join(ID_SEPARATOR)
|
17
19
|
end
|
18
|
-
|
20
|
+
assert_type(object_id, String)
|
19
21
|
return object_id
|
20
22
|
.gsub(WINDOWS_PROTECTED_CHAR, PROTECTED_CHAR_REPLACE) # remove windows forbidden chars
|
21
23
|
.gsub('.', PROTECTED_CHAR_REPLACE) # keep dot for extension only (nicer)
|
data/lib/aspera/json_rpc.rb
CHANGED
@@ -3,6 +3,7 @@
|
|
3
3
|
# cspell:ignore blankslate
|
4
4
|
|
5
5
|
require 'aspera/rest_error_analyzer'
|
6
|
+
require 'aspera/assert'
|
6
7
|
require 'blankslate'
|
7
8
|
|
8
9
|
Aspera::RestErrorAnalyzer.instance.add_simple_handler(name: 'JSON RPC', path: %w[error message], always: true)
|
@@ -34,15 +35,16 @@ module Aspera
|
|
34
35
|
params: args,
|
35
36
|
id: @request_id += 1
|
36
37
|
})[:data]
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
38
|
+
assert_type(data, Hash){'response'}
|
39
|
+
assert(data['jsonrpc'] == JSON_RPC_VERSION){'bad version in response'}
|
40
|
+
assert(data.key?('id')){'missing id in response'}
|
41
|
+
assert(!(data.key?('error') && data.key?('result'))){'both error and response'}
|
42
|
+
assert(
|
42
43
|
!data.key?('error') ||
|
43
|
-
|
44
|
-
|
45
|
-
|
44
|
+
data['error'].is_a?(Hash) &&
|
45
|
+
data['error']['code'].is_a?(Integer) &&
|
46
|
+
data['error']['message'].is_a?(String)
|
47
|
+
){'bad error response'}
|
46
48
|
return data['result']
|
47
49
|
end
|
48
50
|
end
|
@@ -2,6 +2,8 @@
|
|
2
2
|
|
3
3
|
require 'aspera/hash_ext'
|
4
4
|
require 'aspera/environment'
|
5
|
+
require 'aspera/log'
|
6
|
+
require 'aspera/assert'
|
5
7
|
require 'symmetric_encryption/core'
|
6
8
|
require 'yaml'
|
7
9
|
|
@@ -9,33 +11,66 @@ module Aspera
|
|
9
11
|
module Keychain
|
10
12
|
# Manage secrets in a simple Hash
|
11
13
|
class EncryptedHash
|
12
|
-
|
14
|
+
LEGACY_CIPHER_NAME = 'aes-256-cbc'
|
15
|
+
DEFAULT_CIPHER_NAME = 'aes-256-cbc'
|
16
|
+
FILE_TYPE = 'encrypted_hash_vault'
|
13
17
|
CONTENT_KEYS = %i[label username password url description].freeze
|
18
|
+
FILE_KEYS = %w[version type cipher data].sort.freeze
|
14
19
|
def initialize(path, current_password)
|
20
|
+
assert_type(path, String){'path to vault file'}
|
15
21
|
@path = path
|
22
|
+
@all_secrets = {}
|
23
|
+
vault_encrypted_data = nil
|
24
|
+
if File.exist?(@path)
|
25
|
+
vault_file = File.read(@path)
|
26
|
+
if vault_file.start_with?('---')
|
27
|
+
vault_info = YAML.parse(vault_file).to_ruby
|
28
|
+
assert(vault_info.keys.sort == FILE_KEYS){'Invalid vault file'}
|
29
|
+
@cipher_name = vault_info['cipher']
|
30
|
+
vault_encrypted_data = vault_info['data']
|
31
|
+
else
|
32
|
+
# legacy vault file
|
33
|
+
@cipher_name = LEGACY_CIPHER_NAME
|
34
|
+
vault_encrypted_data = File.read(@path, mode: 'rb')
|
35
|
+
end
|
36
|
+
end
|
37
|
+
# setting password also creates the cipher
|
16
38
|
self.password = current_password
|
17
|
-
|
18
|
-
|
39
|
+
if !vault_encrypted_data.nil?
|
40
|
+
@all_secrets = YAML.load_stream(@cipher.decrypt(vault_encrypted_data)).first
|
41
|
+
end
|
19
42
|
end
|
20
43
|
|
44
|
+
# set the password and cipher
|
21
45
|
def password=(new_password)
|
22
46
|
# number of bits in second position
|
23
|
-
key_bytes =
|
47
|
+
key_bytes = DEFAULT_CIPHER_NAME.split('-')[1].to_i / Environment::BITS_PER_BYTE
|
24
48
|
# derive key from passphrase, add trailing zeros
|
25
49
|
key = "#{new_password}#{"\x0" * key_bytes}"[0..(key_bytes - 1)]
|
26
|
-
Log.log.
|
27
|
-
|
50
|
+
Log.log.trace1{"secret=[#{key}],#{key.length}"}
|
51
|
+
@cipher = SymmetricEncryption.cipher = SymmetricEncryption::Cipher.new(cipher_name: DEFAULT_CIPHER_NAME, key: key, encoding: :none)
|
28
52
|
end
|
29
53
|
|
54
|
+
# save current data to file with format
|
30
55
|
def save
|
31
|
-
|
56
|
+
vault_info = {
|
57
|
+
'version' => '1.0.0',
|
58
|
+
'type' => FILE_TYPE,
|
59
|
+
'cipher' => @cipher_name,
|
60
|
+
'data' => @cipher.encrypt(YAML.dump(@all_secrets))
|
61
|
+
}
|
62
|
+
File.write(@path, YAML.dump(vault_info))
|
32
63
|
end
|
33
64
|
|
65
|
+
# set a secret
|
66
|
+
# @param options [Hash] with keys :label, :username, :password, :url, :description
|
34
67
|
def set(options)
|
35
|
-
|
68
|
+
assert_type(options, Hash){'options'}
|
36
69
|
unsupported = options.keys - CONTENT_KEYS
|
37
|
-
|
38
|
-
|
70
|
+
assert(unsupported.empty?){"unsupported options: #{unsupported}"}
|
71
|
+
options.each_pair do |k, v|
|
72
|
+
assert_type(v, String){k.to_s}
|
73
|
+
end
|
39
74
|
label = options.delete(:label)
|
40
75
|
raise "secret #{label} already exist, delete first" if @all_secrets.key?(label)
|
41
76
|
@all_secrets[label] = options.symbolize_keys
|
@@ -59,7 +94,7 @@ module Aspera
|
|
59
94
|
end
|
60
95
|
|
61
96
|
def get(label:, exception: true)
|
62
|
-
|
97
|
+
assert(@all_secrets.key?(label)){"Label not found: #{label}"} if exception
|
63
98
|
result = @all_secrets[label].clone
|
64
99
|
result[:label] = label if result.is_a?(Hash)
|
65
100
|
return result
|
@@ -2,6 +2,8 @@
|
|
2
2
|
|
3
3
|
# https://github.com/fastlane-community/security
|
4
4
|
require 'aspera/cli/info'
|
5
|
+
require 'aspera/log'
|
6
|
+
require 'aspera/assert'
|
5
7
|
|
6
8
|
# enhance the gem to support other key chains
|
7
9
|
module Aspera
|
@@ -36,7 +38,7 @@ module Aspera
|
|
36
38
|
url = options&.delete(:url)
|
37
39
|
if !url.nil?
|
38
40
|
uri = URI.parse(url)
|
39
|
-
|
41
|
+
assert(uri.scheme.eql?('https')){'only https'}
|
40
42
|
options[:protocol] = 'htps' # cspell: disable-line
|
41
43
|
raise 'host required in URL' if uri.host.nil?
|
42
44
|
options[:server] = uri.host
|
@@ -45,7 +47,7 @@ module Aspera
|
|
45
47
|
end
|
46
48
|
cmd = ['security', command]
|
47
49
|
options&.each do |k, v|
|
48
|
-
|
50
|
+
assert(supported.key?(k)){"unknown option: #{k}"}
|
49
51
|
next if v.nil?
|
50
52
|
cmd.push("-#{supported[k]}")
|
51
53
|
cmd.push(v.shellescape) unless v.empty?
|
@@ -70,7 +72,7 @@ module Aspera
|
|
70
72
|
end
|
71
73
|
|
72
74
|
def list(options={})
|
73
|
-
|
75
|
+
assert_values(options[:domain], DOMAINS, exception_class: ArgumentError){'domain'} unless options[:domain].nil?
|
74
76
|
key_chains(execute('list-key_chains', options, LIST_OPTIONS))
|
75
77
|
end
|
76
78
|
|
@@ -89,11 +91,11 @@ module Aspera
|
|
89
91
|
end
|
90
92
|
|
91
93
|
def password(operation, pass_type, options)
|
92
|
-
|
93
|
-
|
94
|
-
|
94
|
+
assert_values(operation, %i[add find delete]){'operation'}
|
95
|
+
assert_values(pass_type, %i[generic internet]){'pass_type'}
|
96
|
+
assert_type(options, Hash)
|
95
97
|
missing = (operation.eql?(:add) ? %i[account service password] : %i[label]) - options.keys
|
96
|
-
|
98
|
+
assert(missing.empty?){"missing options: #{missing}"}
|
97
99
|
options[:getpass] = '' if operation.eql?(:find)
|
98
100
|
output = self.class.execute("#{operation}-#{pass_type}-password", options, ADD_PASS_OPTIONS, @path)
|
99
101
|
raise output.gsub(/^.*: /, '') if output.start_with?('security: ')
|
@@ -127,18 +129,18 @@ module Aspera
|
|
127
129
|
end
|
128
130
|
|
129
131
|
def set(options)
|
130
|
-
|
132
|
+
assert_type(options, Hash){'options'}
|
131
133
|
unsupported = options.keys - %i[label username password url description]
|
132
|
-
|
134
|
+
assert(unsupported.empty?){"unsupported options: #{unsupported}"}
|
133
135
|
@keychain.password(
|
134
136
|
:add, :generic, service: options[:label],
|
135
137
|
account: options[:username] || 'none', password: options[:password], comment: options[:description])
|
136
138
|
end
|
137
139
|
|
138
140
|
def get(options)
|
139
|
-
|
141
|
+
assert_type(options, Hash){'options'}
|
140
142
|
unsupported = options.keys - %i[label]
|
141
|
-
|
143
|
+
assert(unsupported.empty?){"unsupported options: #{unsupported}"}
|
142
144
|
info = @keychain.password(:find, :generic, label: options[:label])
|
143
145
|
raise 'not found' if info.nil?
|
144
146
|
result = options.clone
|
@@ -153,9 +155,9 @@ module Aspera
|
|
153
155
|
end
|
154
156
|
|
155
157
|
def delete(options)
|
156
|
-
|
158
|
+
assert_type(options, Hash){'options'}
|
157
159
|
unsupported = options.keys - %i[label]
|
158
|
-
|
160
|
+
assert(unsupported.empty?){"unsupported options: #{unsupported}"}
|
159
161
|
raise 'delete not implemented, use macos keychain app'
|
160
162
|
end
|
161
163
|
end
|