aspera-cli 4.14.0 → 4.16.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (104) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/BUGS.md +29 -3
  4. data/CHANGELOG.md +300 -185
  5. data/CONTRIBUTING.md +74 -23
  6. data/README.md +2346 -1619
  7. data/bin/ascli +16 -25
  8. data/bin/asession +15 -15
  9. data/examples/dascli +2 -2
  10. data/examples/proxy.pac +1 -1
  11. data/lib/aspera/aoc.rb +216 -150
  12. data/lib/aspera/ascmd.rb +25 -18
  13. data/lib/aspera/assert.rb +45 -0
  14. data/lib/aspera/cli/basic_auth_plugin.rb +9 -6
  15. data/lib/aspera/cli/error.rb +17 -0
  16. data/lib/aspera/cli/extended_value.rb +51 -16
  17. data/lib/aspera/cli/formatter.rb +276 -174
  18. data/lib/aspera/cli/hints.rb +81 -0
  19. data/lib/aspera/cli/main.rb +114 -147
  20. data/lib/aspera/cli/manager.rb +181 -136
  21. data/lib/aspera/cli/plugin.rb +82 -64
  22. data/lib/aspera/cli/plugins/alee.rb +0 -1
  23. data/lib/aspera/cli/plugins/aoc.rb +327 -331
  24. data/lib/aspera/cli/plugins/ats.rb +12 -8
  25. data/lib/aspera/cli/plugins/bss.rb +2 -2
  26. data/lib/aspera/cli/plugins/config.rb +575 -439
  27. data/lib/aspera/cli/plugins/console.rb +40 -0
  28. data/lib/aspera/cli/plugins/cos.rb +4 -5
  29. data/lib/aspera/cli/plugins/faspex.rb +111 -92
  30. data/lib/aspera/cli/plugins/faspex5.rb +245 -182
  31. data/lib/aspera/cli/plugins/node.rb +239 -160
  32. data/lib/aspera/cli/plugins/orchestrator.rb +56 -19
  33. data/lib/aspera/cli/plugins/preview.rb +54 -38
  34. data/lib/aspera/cli/plugins/server.rb +63 -20
  35. data/lib/aspera/cli/plugins/shares.rb +64 -38
  36. data/lib/aspera/cli/sync_actions.rb +68 -0
  37. data/lib/aspera/cli/transfer_agent.rb +64 -67
  38. data/lib/aspera/cli/transfer_progress.rb +73 -0
  39. data/lib/aspera/cli/version.rb +1 -1
  40. data/lib/aspera/colors.rb +3 -1
  41. data/lib/aspera/command_line_builder.rb +27 -22
  42. data/lib/aspera/cos_node.rb +6 -4
  43. data/lib/aspera/coverage.rb +22 -0
  44. data/lib/aspera/data_repository.rb +33 -2
  45. data/lib/aspera/environment.rb +21 -8
  46. data/lib/aspera/fasp/agent_alpha.rb +116 -0
  47. data/lib/aspera/fasp/agent_base.rb +40 -76
  48. data/lib/aspera/fasp/agent_connect.rb +21 -22
  49. data/lib/aspera/fasp/agent_direct.rb +169 -179
  50. data/lib/aspera/fasp/agent_httpgw.rb +200 -195
  51. data/lib/aspera/fasp/agent_node.rb +43 -35
  52. data/lib/aspera/fasp/agent_trsdk.rb +124 -41
  53. data/lib/aspera/fasp/error_info.rb +2 -2
  54. data/lib/aspera/fasp/faux_file.rb +52 -0
  55. data/lib/aspera/fasp/installation.rb +89 -191
  56. data/lib/aspera/fasp/management.rb +249 -0
  57. data/lib/aspera/fasp/parameters.rb +86 -47
  58. data/lib/aspera/fasp/parameters.yaml +75 -8
  59. data/lib/aspera/fasp/products.rb +162 -0
  60. data/lib/aspera/fasp/resume_policy.rb +7 -5
  61. data/lib/aspera/fasp/sync.rb +273 -0
  62. data/lib/aspera/fasp/transfer_spec.rb +10 -8
  63. data/lib/aspera/fasp/uri.rb +6 -6
  64. data/lib/aspera/faspex_gw.rb +11 -8
  65. data/lib/aspera/faspex_postproc.rb +8 -7
  66. data/lib/aspera/hash_ext.rb +2 -2
  67. data/lib/aspera/id_generator.rb +3 -1
  68. data/lib/aspera/json_rpc.rb +51 -0
  69. data/lib/aspera/keychain/encrypted_hash.rb +46 -11
  70. data/lib/aspera/keychain/macos_security.rb +15 -13
  71. data/lib/aspera/line_logger.rb +23 -0
  72. data/lib/aspera/log.rb +61 -19
  73. data/lib/aspera/nagios.rb +7 -2
  74. data/lib/aspera/node.rb +105 -21
  75. data/lib/aspera/node_simulator.rb +214 -0
  76. data/lib/aspera/oauth.rb +57 -36
  77. data/lib/aspera/open_application.rb +4 -4
  78. data/lib/aspera/persistency_action_once.rb +13 -14
  79. data/lib/aspera/persistency_folder.rb +5 -4
  80. data/lib/aspera/preview/file_types.rb +56 -268
  81. data/lib/aspera/preview/generator.rb +28 -39
  82. data/lib/aspera/preview/options.rb +2 -0
  83. data/lib/aspera/preview/terminal.rb +36 -16
  84. data/lib/aspera/preview/utils.rb +23 -29
  85. data/lib/aspera/proxy_auto_config.rb +6 -3
  86. data/lib/aspera/rest.rb +127 -80
  87. data/lib/aspera/rest_call_error.rb +1 -1
  88. data/lib/aspera/rest_error_analyzer.rb +16 -14
  89. data/lib/aspera/rest_errors_aspera.rb +39 -34
  90. data/lib/aspera/secret_hider.rb +18 -17
  91. data/lib/aspera/ssh.rb +10 -5
  92. data/lib/aspera/temp_file_manager.rb +11 -4
  93. data/lib/aspera/web_auth.rb +10 -7
  94. data/lib/aspera/web_server_simple.rb +11 -5
  95. data.tar.gz.sig +0 -0
  96. metadata +108 -39
  97. metadata.gz.sig +0 -0
  98. data/lib/aspera/cli/listener/line_dump.rb +0 -19
  99. data/lib/aspera/cli/listener/logger.rb +0 -22
  100. data/lib/aspera/cli/listener/progress.rb +0 -50
  101. data/lib/aspera/cli/listener/progress_multi.rb +0 -84
  102. data/lib/aspera/cli/plugins/sync.rb +0 -44
  103. data/lib/aspera/fasp/listener.rb +0 -13
  104. data/lib/aspera/sync.rb +0 -213
@@ -2,6 +2,7 @@
2
2
 
3
3
  require 'singleton'
4
4
  require 'aspera/log'
5
+ require 'aspera/assert'
5
6
 
6
7
  module Aspera
7
8
  module Fasp
@@ -19,10 +20,10 @@ module Aspera
19
20
  def initialize(params=nil)
20
21
  @parameters = DEFAULTS.dup
21
22
  if !params.nil?
22
- raise "expecting Hash (or nil), but have #{params.class}" unless params.is_a?(Hash)
23
+ assert_type(params, Hash)
23
24
  params.each do |k, v|
24
- raise "unknown resume parameter: #{k}, expect one of #{DEFAULTS.keys.map(&:to_s).join(',')}" unless DEFAULTS.key?(k)
25
- raise "#{k} must be Integer" unless v.is_a?(Integer)
25
+ assert_values(k, DEFAULTS.keys){'resume parameter'}
26
+ assert_type(v, Integer){k}
26
27
  @parameters[k] = v
27
28
  end
28
29
  end
@@ -32,7 +33,7 @@ module Aspera
32
33
  # calls block a number of times (resumes) until success or limit reached
33
34
  # this is re-entrant, one resumer can handle multiple transfers in //
34
35
  def execute_with_resume
35
- raise 'block mandatory' unless block_given?
36
+ assert(block_given?)
36
37
  # maximum of retry
37
38
  remaining_resumes = @parameters[:iter_max]
38
39
  sleep_seconds = @parameters[:sleep_initial]
@@ -43,9 +44,10 @@ module Aspera
43
44
  begin
44
45
  # call provided block
45
46
  yield
47
+ # exit retry loop if success
46
48
  break
47
49
  rescue Fasp::Error => e
48
- Log.log.warn{"An error occurred: #{e.message}"}
50
+ Log.log.warn{"An error occurred during transfer: #{e.message}"}
49
51
  # failure in ascp
50
52
  if e.retryable?
51
53
  # exit if we exceed the max number of retry
@@ -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
- raise 'transfer spec must be a Hash' unless tspec.is_a?(Hash)
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 raise 'Error: upload or download only'
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
- raise 'transfer spec must be a Hash' unless tspec.is_a?(Hash)
41
- return case tspec['direction']
42
- when DIRECTION_SEND then :upload
43
- when DIRECTION_RECEIVE then :download
44
- else raise 'Error: upload or download only'
45
- end
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
@@ -1,17 +1,19 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # cspell:words httpport targetrate minrate bwcap createpath lockpolicy lockminrate
3
+ # cspell:words httpport targetrate minrate bwcap createpath lockpolicy lockminrate faspe
4
4
 
5
5
  require 'aspera/log'
6
+ require 'aspera/rest'
6
7
  require 'aspera/command_line_builder'
7
8
 
8
9
  module Aspera
9
10
  module Fasp
10
- # translates a "faspe:" URI (used in Faspex 4) into transfer spec hash
11
+ # translates a "faspe:" URI (used in Faspex 4) into transfer spec (Hash)
11
12
  class Uri
13
+ SCHEME = 'faspe'
12
14
  def initialize(fasp_link)
13
15
  @fasp_uri = URI.parse(fasp_link.gsub(' ', '%20'))
14
- # TODO: check scheme is faspe
16
+ # TODO: check scheme is 'faspe'
15
17
  end
16
18
 
17
19
  def transfer_spec
@@ -23,9 +25,7 @@ module Aspera
23
25
  # faspex does not encode trailing base64 padding, fix that to be able to decode properly
24
26
  fixed_query = @fasp_uri.query.gsub(/(=+)$/){|x|'%3D' * x.length}
25
27
 
26
- URI.decode_www_form(fixed_query).each do |i|
27
- name = i[0]
28
- value = i[1]
28
+ Rest.decode_query(fixed_query).each do |name, value|
29
29
  case name
30
30
  when 'cookie' then result_ts['cookie'] = value
31
31
  when 'token' then result_ts['token'] = value
@@ -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
- # this class answers the Faspex /send API and creates a package on Aspera on Cloud
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
- if @app_api.is_a?(Aspera::AoC)
74
+ case @app_api.class.name
75
+ when 'Aspera::AoC'
72
76
  faspex4_send_to_aoc(faspex_pkg_parameters)
73
- elsif @app_api.is_a?(Aspera::Rest)
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: 'Bad request'}.to_json
95
+ response.body = {error: 'Unsupported endpoint'}.to_json
93
96
  end
94
97
  end
95
98
  end # Faspex4GWServlet
96
- end # AsperaLm
99
+ end # Aspera
@@ -1,19 +1,20 @@
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
- raise 'parameters must be Hash' unless parameters.is_a?(Hash)
15
+ assert_type(parameters, Hash)
15
16
  @parameters = parameters.symbolize_keys
16
- Log.dump(:post_proc_parameters, @parameters)
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)}
18
19
  @parameters[:script_folder] ||= '.'
19
20
  @parameters[:fail_on_error] ||= false
@@ -44,7 +45,7 @@ module Aspera
44
45
  script_path = File.join(@parameters[:script_folder], script_file)
45
46
  Log.log.debug{"script=#{script_path}"}
46
47
  webhook_parameters = JSON.parse(request.body)
47
- Log.dump(:webhook_parameters, webhook_parameters)
48
+ Log.log.debug{Log.dump(:webhook_parameters, webhook_parameters)}
48
49
  # env expects only strings
49
50
  environment = webhook_parameters.each_with_object({}) { |(k, v), h| h[k] = v.to_s }
50
51
  post_proc_pid = Process.spawn(environment, [script_path, script_path])
@@ -74,4 +75,4 @@ module Aspera
74
75
  end
75
76
  end
76
77
  end # Faspex4PostProcServlet
77
- end # AsperaLm
78
+ end # Aspera
@@ -24,8 +24,8 @@ end
24
24
  unless Hash.method_defined?(:transform_keys)
25
25
  class Hash
26
26
  def transform_keys
27
- return each_with_object({}){|(k, v), memo|memo[yield(k)] = v} if block_given?
28
- raise 'missing block'
27
+ raise 'missing block' unless block_given?
28
+ return each_with_object({}){|(k, v), memo|memo[yield(k)] = v}
29
29
  end
30
30
  end
31
31
  end
@@ -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
- raise 'id must be a String' unless object_id.is_a?(String)
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)
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ # cspell:ignore blankslate
4
+
5
+ require 'aspera/rest_error_analyzer'
6
+ require 'aspera/assert'
7
+ require 'blankslate'
8
+
9
+ Aspera::RestErrorAnalyzer.instance.add_simple_handler(name: 'JSON RPC', path: %w[error message], always: true)
10
+
11
+ module Aspera
12
+ # a very simple JSON RPC client
13
+ class JsonRpcClient < BlankSlate
14
+ JSON_RPC_VERSION = '2.0'
15
+ reveal :instance_variable_get
16
+ reveal :inspect
17
+ reveal :to_s
18
+
19
+ def initialize(api, namespace = nil)
20
+ super()
21
+ @api = api
22
+ @namespace = namespace
23
+ @request_id = 0
24
+ end
25
+
26
+ def respond_to_missing?(sym, include_private = false)
27
+ true
28
+ end
29
+
30
+ def method_missing(method, *args, &block)
31
+ args = args.first if args.size == 1 && args.first.is_a?(Hash)
32
+ data = @api.create('', {
33
+ jsonrpc: JSON_RPC_VERSION,
34
+ method: "#{@namespace}#{method}",
35
+ params: args,
36
+ id: @request_id += 1
37
+ })[:data]
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(
43
+ !data.key?('error') ||
44
+ data['error'].is_a?(Hash) &&
45
+ data['error']['code'].is_a?(Integer) &&
46
+ data['error']['message'].is_a?(String)
47
+ ){'bad error response'}
48
+ return data['result']
49
+ end
50
+ end
51
+ 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
- CIPHER_NAME = 'aes-256-cbc'
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
- raise 'path to vault file shall be String' unless @path.is_a?(String)
18
- @all_secrets = File.exist?(@path) ? YAML.load_stream(@cipher.decrypt(File.read(@path))).first : {}
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 = CIPHER_NAME.split('-')[1].to_i / Environment::BITS_PER_BYTE
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.debug{"key=[#{key}],#{key.length}"}
27
- SymmetricEncryption.cipher = @cipher = SymmetricEncryption::Cipher.new(cipher_name: CIPHER_NAME, key: key, encoding: :none)
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
- File.write(@path, @cipher.encrypt(YAML.dump(@all_secrets)), encoding: 'BINARY')
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
- raise 'options shall be Hash' unless options.is_a?(Hash)
68
+ assert_type(options, Hash){'options'}
36
69
  unsupported = options.keys - CONTENT_KEYS
37
- options.each_value {|v| raise 'value must be String' unless v.is_a?(String)}
38
- raise "unsupported options: #{unsupported}" unless unsupported.empty?
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
- raise "Label not found: #{label}" unless @all_secrets.key?(label) || !exception
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