aspera-cli 4.14.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.
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