aspera-cli 4.15.0 → 4.16.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (80) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/BUGS.md +29 -3
  4. data/CHANGELOG.md +292 -228
  5. data/CONTRIBUTING.md +69 -18
  6. data/README.md +1102 -952
  7. data/bin/ascli +13 -31
  8. data/bin/asession +3 -1
  9. data/examples/dascli +2 -2
  10. data/lib/aspera/aoc.rb +28 -33
  11. data/lib/aspera/ascmd.rb +3 -6
  12. data/lib/aspera/assert.rb +45 -0
  13. data/lib/aspera/cli/extended_value.rb +5 -5
  14. data/lib/aspera/cli/formatter.rb +26 -13
  15. data/lib/aspera/cli/hints.rb +4 -3
  16. data/lib/aspera/cli/main.rb +16 -3
  17. data/lib/aspera/cli/manager.rb +45 -36
  18. data/lib/aspera/cli/plugin.rb +20 -13
  19. data/lib/aspera/cli/plugins/aoc.rb +103 -73
  20. data/lib/aspera/cli/plugins/ats.rb +4 -3
  21. data/lib/aspera/cli/plugins/config.rb +114 -119
  22. data/lib/aspera/cli/plugins/cos.rb +2 -2
  23. data/lib/aspera/cli/plugins/faspex.rb +23 -19
  24. data/lib/aspera/cli/plugins/faspex5.rb +75 -43
  25. data/lib/aspera/cli/plugins/node.rb +28 -15
  26. data/lib/aspera/cli/plugins/orchestrator.rb +4 -2
  27. data/lib/aspera/cli/plugins/preview.rb +9 -7
  28. data/lib/aspera/cli/plugins/server.rb +6 -3
  29. data/lib/aspera/cli/plugins/shares.rb +30 -26
  30. data/lib/aspera/cli/sync_actions.rb +9 -9
  31. data/lib/aspera/cli/transfer_agent.rb +21 -14
  32. data/lib/aspera/cli/transfer_progress.rb +2 -3
  33. data/lib/aspera/cli/version.rb +1 -1
  34. data/lib/aspera/command_line_builder.rb +13 -11
  35. data/lib/aspera/cos_node.rb +3 -2
  36. data/lib/aspera/coverage.rb +22 -0
  37. data/lib/aspera/data_repository.rb +33 -2
  38. data/lib/aspera/environment.rb +4 -2
  39. data/lib/aspera/fasp/{agent_aspera.rb → agent_alpha.rb} +29 -39
  40. data/lib/aspera/fasp/agent_base.rb +17 -7
  41. data/lib/aspera/fasp/agent_direct.rb +88 -84
  42. data/lib/aspera/fasp/agent_httpgw.rb +4 -3
  43. data/lib/aspera/fasp/agent_node.rb +3 -2
  44. data/lib/aspera/fasp/agent_trsdk.rb +79 -37
  45. data/lib/aspera/fasp/installation.rb +51 -12
  46. data/lib/aspera/fasp/management.rb +11 -6
  47. data/lib/aspera/fasp/parameters.rb +53 -47
  48. data/lib/aspera/fasp/resume_policy.rb +7 -5
  49. data/lib/aspera/fasp/sync.rb +273 -0
  50. data/lib/aspera/fasp/transfer_spec.rb +10 -8
  51. data/lib/aspera/fasp/uri.rb +2 -2
  52. data/lib/aspera/faspex_gw.rb +11 -8
  53. data/lib/aspera/faspex_postproc.rb +6 -5
  54. data/lib/aspera/id_generator.rb +3 -1
  55. data/lib/aspera/json_rpc.rb +10 -8
  56. data/lib/aspera/keychain/encrypted_hash.rb +46 -11
  57. data/lib/aspera/keychain/macos_security.rb +15 -13
  58. data/lib/aspera/log.rb +4 -3
  59. data/lib/aspera/nagios.rb +7 -2
  60. data/lib/aspera/node.rb +17 -16
  61. data/lib/aspera/node_simulator.rb +214 -0
  62. data/lib/aspera/oauth.rb +22 -19
  63. data/lib/aspera/persistency_action_once.rb +13 -14
  64. data/lib/aspera/persistency_folder.rb +3 -2
  65. data/lib/aspera/preview/file_types.rb +53 -267
  66. data/lib/aspera/preview/generator.rb +7 -5
  67. data/lib/aspera/preview/terminal.rb +14 -5
  68. data/lib/aspera/preview/utils.rb +8 -7
  69. data/lib/aspera/proxy_auto_config.rb +6 -3
  70. data/lib/aspera/rest.rb +29 -13
  71. data/lib/aspera/rest_error_analyzer.rb +1 -0
  72. data/lib/aspera/rest_errors_aspera.rb +2 -0
  73. data/lib/aspera/secret_hider.rb +5 -2
  74. data/lib/aspera/ssh.rb +10 -8
  75. data/lib/aspera/temp_file_manager.rb +1 -1
  76. data/lib/aspera/web_server_simple.rb +2 -1
  77. data.tar.gz.sig +0 -0
  78. metadata +96 -45
  79. metadata.gz.sig +0 -0
  80. 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
- 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, not #{tspec['direction']} (#{tspec['direction'].class})"
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
@@ -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 hash
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
@@ -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.class.name.eql?('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.class.name.eql?('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,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
- raise 'parameters must be Hash' unless parameters.is_a?(Hash)
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 # AsperaLm
78
+ end # Aspera
@@ -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)
@@ -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
- raise 'response shall be Hash' unless data.is_a?(Hash)
38
- raise 'bad version in response' unless data['jsonrpc'] == JSON_RPC_VERSION
39
- raise 'missing id in response' unless data.key?('id')
40
- raise 'both error and response' if data.key?('error') && data.key?('result')
41
- raise 'bad error response' unless
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
- data['error'].is_a?(Hash) &&
44
- data['error']['code'].is_a?(Integer) &&
45
- data['error']['message'].is_a?(String)
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
- 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
@@ -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
- raise 'only https' unless uri.scheme.eql?('https')
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
- raise "unknown option: #{k}" unless supported.key?(k)
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
- raise ArgumentError, "Invalid domain #{options[:domain]}, expected one of: #{DOMAINS}" unless options[:domain].nil? || DOMAINS.include?(options[:domain])
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
- raise "wrong operation: #{operation}" unless %i[add find delete].include?(operation)
93
- raise "wrong pass_type: #{pass_type}" unless %i[generic internet].include?(pass_type)
94
- raise 'options shall be Hash' unless options.is_a?(Hash)
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
- raise "missing options: #{missing}" unless missing.empty?
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
- raise 'options shall be Hash' unless options.is_a?(Hash)
132
+ assert_type(options, Hash){'options'}
131
133
  unsupported = options.keys - %i[label username password url description]
132
- raise "unsupported options: #{unsupported}" unless unsupported.empty?
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
- raise 'options shall be Hash' unless options.is_a?(Hash)
141
+ assert_type(options, Hash){'options'}
140
142
  unsupported = options.keys - %i[label]
141
- raise "unsupported options: #{unsupported}" unless unsupported.empty?
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
- raise 'options shall be Hash' unless options.is_a?(Hash)
158
+ assert_type(options, Hash){'options'}
157
159
  unsupported = options.keys - %i[label]
158
- raise "unsupported options: #{unsupported}" unless unsupported.empty?
160
+ assert(unsupported.empty?){"unsupported options: #{unsupported}"}
159
161
  raise 'delete not implemented, use macos keychain app'
160
162
  end
161
163
  end