aspera-cli 4.22.0 → 4.24.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 (114) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/CHANGELOG.md +405 -364
  4. data/CONTRIBUTING.md +86 -29
  5. data/README.md +1856 -961
  6. data/bin/ascli +2 -1
  7. data/bin/asession +4 -4
  8. data/lib/aspera/agent/base.rb +4 -0
  9. data/lib/aspera/agent/connect.rb +20 -18
  10. data/lib/aspera/agent/desktop.rb +14 -11
  11. data/lib/aspera/agent/direct.rb +39 -31
  12. data/lib/aspera/agent/httpgw.rb +2 -2
  13. data/lib/aspera/agent/node.rb +9 -11
  14. data/lib/aspera/agent/transferd.rb +18 -11
  15. data/lib/aspera/api/aoc.rb +53 -43
  16. data/lib/aspera/api/cos_node.rb +7 -5
  17. data/lib/aspera/api/httpgw.rb +23 -22
  18. data/lib/aspera/api/node.rb +104 -22
  19. data/lib/aspera/ascmd.rb +35 -21
  20. data/lib/aspera/ascp/installation.rb +43 -43
  21. data/lib/aspera/ascp/management.rb +5 -4
  22. data/lib/aspera/assert.rb +55 -24
  23. data/lib/aspera/cli/basic_auth_plugin.rb +8 -7
  24. data/lib/aspera/cli/error.rb +1 -1
  25. data/lib/aspera/cli/extended_value.rb +28 -29
  26. data/lib/aspera/cli/formatter.rb +191 -168
  27. data/lib/aspera/cli/hints.rb +38 -4
  28. data/lib/aspera/cli/main.rb +139 -108
  29. data/lib/aspera/cli/manager.rb +51 -31
  30. data/lib/aspera/cli/plugin.rb +149 -78
  31. data/lib/aspera/cli/plugin_factory.rb +2 -2
  32. data/lib/aspera/cli/plugins/aoc.rb +217 -88
  33. data/lib/aspera/cli/plugins/ats.rb +15 -13
  34. data/lib/aspera/cli/plugins/config.rb +105 -227
  35. data/lib/aspera/cli/plugins/console.rb +49 -18
  36. data/lib/aspera/cli/plugins/cos.rb +4 -4
  37. data/lib/aspera/cli/plugins/faspex.rb +45 -51
  38. data/lib/aspera/cli/plugins/faspex5.rb +162 -163
  39. data/lib/aspera/cli/plugins/faspio.rb +6 -5
  40. data/lib/aspera/cli/plugins/httpgw.rb +2 -2
  41. data/lib/aspera/cli/plugins/node.rb +233 -247
  42. data/lib/aspera/cli/plugins/orchestrator.rb +10 -14
  43. data/lib/aspera/cli/plugins/preview.rb +26 -29
  44. data/lib/aspera/cli/plugins/server.rb +29 -28
  45. data/lib/aspera/cli/plugins/shares.rb +40 -28
  46. data/lib/aspera/cli/sync_actions.rb +101 -80
  47. data/lib/aspera/cli/transfer_agent.rb +55 -58
  48. data/lib/aspera/cli/transfer_progress.rb +29 -20
  49. data/lib/aspera/cli/version.rb +1 -1
  50. data/lib/aspera/cli/wizard.rb +160 -0
  51. data/lib/aspera/colors.rb +13 -8
  52. data/lib/aspera/command_line_builder.rb +28 -22
  53. data/lib/aspera/command_line_converter.rb +31 -0
  54. data/lib/aspera/data_repository.rb +1 -0
  55. data/lib/aspera/environment.rb +144 -100
  56. data/lib/aspera/faspex_gw.rb +1 -1
  57. data/lib/aspera/faspex_postproc.rb +3 -2
  58. data/lib/aspera/hash_ext.rb +1 -1
  59. data/lib/aspera/id_generator.rb +10 -10
  60. data/lib/aspera/keychain/base.rb +18 -0
  61. data/lib/aspera/keychain/encrypted_hash.rb +6 -12
  62. data/lib/aspera/keychain/factory.rb +9 -3
  63. data/lib/aspera/keychain/hashicorp_vault.rb +9 -6
  64. data/lib/aspera/keychain/macos_security.rb +13 -13
  65. data/lib/aspera/log.rb +70 -20
  66. data/lib/aspera/nagios.rb +5 -6
  67. data/lib/aspera/node_simulator.rb +12 -7
  68. data/lib/aspera/oauth/base.rb +6 -2
  69. data/lib/aspera/oauth/factory.rb +25 -18
  70. data/lib/aspera/oauth/jwt.rb +13 -1
  71. data/lib/aspera/oauth/url_json.rb +3 -3
  72. data/lib/aspera/oauth/web.rb +5 -3
  73. data/lib/aspera/persistency_folder.rb +2 -2
  74. data/lib/aspera/preview/file_types.rb +43 -35
  75. data/lib/aspera/preview/generator.rb +26 -13
  76. data/lib/aspera/preview/terminal.rb +10 -7
  77. data/lib/aspera/preview/utils.rb +11 -9
  78. data/lib/aspera/products/connect.rb +2 -1
  79. data/lib/aspera/products/desktop.rb +1 -1
  80. data/lib/aspera/products/other.rb +2 -2
  81. data/lib/aspera/products/transferd.rb +8 -6
  82. data/lib/aspera/proxy_auto_config.rb +1 -1
  83. data/lib/aspera/rest.rb +46 -28
  84. data/lib/aspera/rest_call_error.rb +1 -1
  85. data/lib/aspera/rest_error_analyzer.rb +1 -0
  86. data/lib/aspera/resumer.rb +1 -1
  87. data/lib/aspera/secret_hider.rb +46 -40
  88. data/lib/aspera/ssh.rb +14 -4
  89. data/lib/aspera/sync/args.schema.yaml +102 -0
  90. data/lib/aspera/sync/conf.schema.yaml +701 -0
  91. data/lib/aspera/sync/database.rb +83 -0
  92. data/lib/aspera/{transfer/sync.rb → sync/operations.rb} +145 -68
  93. data/lib/aspera/temp_file_manager.rb +4 -2
  94. data/lib/aspera/timer_limiter.rb +7 -5
  95. data/lib/aspera/transfer/error.rb +1 -1
  96. data/lib/aspera/transfer/error_info.rb +1 -2
  97. data/lib/aspera/transfer/faux_file.rb +11 -10
  98. data/lib/aspera/transfer/parameters.rb +6 -5
  99. data/lib/aspera/transfer/spec.rb +15 -1
  100. data/lib/aspera/transfer/spec.schema.yaml +316 -293
  101. data/lib/aspera/transfer/spec_doc.rb +34 -16
  102. data/lib/aspera/transfer/uri.rb +5 -5
  103. data/lib/aspera/uri_reader.rb +14 -10
  104. data/lib/aspera/web_auth.rb +2 -2
  105. data/lib/aspera/web_server_simple.rb +2 -2
  106. data.tar.gz.sig +0 -0
  107. metadata +15 -15
  108. metadata.gz.sig +0 -0
  109. data/examples/dascli +0 -30
  110. data/examples/get_proto_file.rb +0 -8
  111. data/examples/proxy.pac +0 -60
  112. data/lib/aspera/transfer/convert.rb +0 -29
  113. data/lib/aspera/transfer/sync_instance.schema.yaml +0 -13
  114. data/lib/aspera/transfer/sync_session.schema.yaml +0 -79
@@ -8,6 +8,7 @@ require 'aspera/cli/version'
8
8
  require 'aspera/cli/formatter'
9
9
  require 'aspera/cli/info'
10
10
  require 'aspera/cli/transfer_progress'
11
+ require 'aspera/cli/wizard'
11
12
  require 'aspera/ascp/installation'
12
13
  require 'aspera/products/transferd'
13
14
  require 'aspera/transfer/error_info'
@@ -23,6 +24,7 @@ require 'aspera/persistency_folder'
23
24
  require 'aspera/data_repository'
24
25
  require 'aspera/line_logger'
25
26
  require 'aspera/rest'
27
+ require 'aspera/oauth/jwt'
26
28
  require 'aspera/log'
27
29
  require 'aspera/assert'
28
30
  require 'aspera/oauth'
@@ -68,10 +70,7 @@ module Aspera
68
70
  EXTEND_VAULT = :vault
69
71
  PRESET_DIG_SEPARATOR = '.'
70
72
  DEFAULT_CHECK_NEW_VERSION_DAYS = 7
71
- DEFAULT_PRIV_KEY_FILENAME = 'my_private_key.pem' # pragma: allowlist secret
72
- DEFAULT_PRIV_KEY_LENGTH = 4096
73
- COFFEE_IMAGE = 'https://enjoyjava.com/wp-content/uploads/2018/01/How-to-make-strong-coffee.jpg'
74
- WIZARD_RESULT_KEYS = %i[preset_value test_args].freeze
73
+ COFFEE_IMAGE_URL = 'https://enjoyjava.com/wp-content/uploads/2018/01/How-to-make-strong-coffee.jpg'
75
74
  GEM_CHECK_DATE_FMT = '%Y/%m/%d'
76
75
  # for testing only
77
76
  SELF_SIGNED_CERT = OpenSSL::SSL.const_get(:enon_yfirev.to_s.upcase.reverse) # cspell: disable-line
@@ -90,28 +89,15 @@ module Aspera
90
89
  :EXTEND_PRESET,
91
90
  :EXTEND_VAULT,
92
91
  :DEFAULT_CHECK_NEW_VERSION_DAYS,
93
- :DEFAULT_PRIV_KEY_FILENAME,
94
92
  :SERVER_COMMAND,
95
93
  :PRESET_DIG_SEPARATOR,
96
- :COFFEE_IMAGE,
97
- :WIZARD_RESULT_KEYS,
94
+ :COFFEE_IMAGE_URL,
98
95
  :SELF_SIGNED_CERT,
99
96
  :PERSISTENCY_FOLDER,
100
- :DEFAULT_PRIV_KEY_LENGTH,
101
97
  :CONF_OVERVIEW_KEYS,
102
98
  :SMTP_CONF_PARAMS
103
99
 
104
100
  class << self
105
- def generate_rsa_private_key(path:, length: DEFAULT_PRIV_KEY_LENGTH)
106
- require 'openssl'
107
- priv_key = OpenSSL::PKey::RSA.new(length)
108
- File.write(path, priv_key.to_s)
109
- File.write("#{path}.pub", priv_key.public_key.to_s)
110
- Environment.restrict_file_access(path)
111
- Environment.restrict_file_access("#{path}.pub")
112
- nil
113
- end
114
-
115
101
  # folder containing plugins in the gem's main folder
116
102
  def gem_plugins_folder
117
103
  File.dirname(File.expand_path(__FILE__))
@@ -132,7 +118,7 @@ module Aspera
132
118
  # return product family folder (~/.aspera)
133
119
  def module_family_folder
134
120
  user_home_folder = Dir.home
135
- Aspera.assert(Dir.exist?(user_home_folder), exception_class: Cli::Error){"Home folder does not exist: #{user_home_folder}. Check your user environment."}
121
+ Aspera.assert(Dir.exist?(user_home_folder), type: Cli::Error){"Home folder does not exist: #{user_home_folder}. Check your user environment."}
136
122
  return File.join(user_home_folder, ASPERA_HOME_FOLDER_NAME)
137
123
  end
138
124
 
@@ -171,11 +157,12 @@ module Aspera
171
157
  :home, 'Home folder for tool',
172
158
  handler: {o: self, m: :main_folder},
173
159
  types: String,
174
- default: self.class.default_app_main_folder(app_name: Info::CMD_NAME))
160
+ default: self.class.default_app_main_folder(app_name: Info::CMD_NAME)
161
+ )
175
162
  options.parse_options!
176
163
  Log.log.debug{"#{Info::CMD_NAME} folder: #{@main_folder}"}
177
164
  # data persistency manager, created by config plugin, set for global object
178
- @broker.persistency = PersistencyFolder.new(File.join(@main_folder, PERSISTENCY_FOLDER))
165
+ context.persistency = PersistencyFolder.new(File.join(@main_folder, PERSISTENCY_FOLDER))
179
166
  # set folders for plugin lookup
180
167
  PluginFactory.instance.add_lookup_folder(self.class.gem_plugins_folder)
181
168
  PluginFactory.instance.add_lookup_folder(File.join(@main_folder, ASPERA_PLUGINS_FOLDERNAME))
@@ -183,7 +170,8 @@ module Aspera
183
170
  options.declare(
184
171
  :config_file, 'Path to YAML file with preset configuration',
185
172
  handler: {o: self, m: :option_config_file},
186
- default: File.join(@main_folder, DEFAULT_CONFIG_FILENAME))
173
+ default: File.join(@main_folder, DEFAULT_CONFIG_FILENAME)
174
+ )
187
175
  options.parse_options!
188
176
  # read config file (set @config_presets)
189
177
  read_config_file
@@ -204,29 +192,26 @@ module Aspera
204
192
  options.declare(:preset, 'Load the named option preset from current config file', short: 'P', handler: {o: self, m: :option_preset})
205
193
  options.declare(:version_check_days, 'Period in days to check new version (zero to disable)', coerce: Integer, default: DEFAULT_CHECK_NEW_VERSION_DAYS)
206
194
  options.declare(:plugin_folder, 'Folder where to find additional plugins', handler: {o: self, m: :option_plugin_folder})
207
- # wizard options
208
- options.declare(:override, 'Wizard: override existing value', values: :bool, default: :no)
209
- options.declare(:default, 'Wizard: set as default configuration for specified plugin (also: update)', values: :bool, default: true)
210
- options.declare(:test_mode, 'Wizard: skip private key check step', values: :bool, default: false)
211
- options.declare(:key_path, 'Wizard: path to private key for JWT')
195
+ # declare wizard options
196
+ @wizard = Wizard.new(self, @main_folder)
212
197
  # Transfer SDK options
213
- options.declare(:ascp_path, 'Path to ascp', handler: {o: Ascp::Installation.instance, m: :ascp_path})
214
- options.declare(:use_product, 'Use ascp from specified product', handler: {o: self, m: :option_use_product})
215
- options.declare(:sdk_url, 'URL to get Aspera Transfer Daemon', default: SpecialValues::DEF)
216
- options.declare(:locations_url, 'URL to get locations of Aspera Transfer Daemon', handler: {o: Ascp::Installation.instance, m: :transferd_urls})
217
- options.declare(:sdk_folder, 'SDK folder path', handler: {o: Products::Transferd, m: :sdk_directory})
198
+ options.declare(:ascp_path, 'Ascp: Path to ascp', handler: {o: Ascp::Installation.instance, m: :ascp_path})
199
+ options.declare(:use_product, 'Ascp: Use ascp from specified product', handler: {o: self, m: :option_use_product})
200
+ options.declare(:sdk_url, 'Ascp: URL to get Aspera Transfer Executables', default: SpecialValues::DEF)
201
+ options.declare(:locations_url, 'Ascp: URL to get locations of Aspera Transfer Daemon', handler: {o: Ascp::Installation.instance, m: :transferd_urls})
202
+ options.declare(:sdk_folder, 'Ascp: SDK folder path', handler: {o: Products::Transferd, m: :sdk_directory})
218
203
  options.declare(:progress_bar, 'Display progress bar', values: :bool, default: Environment.terminal?)
219
204
  # email options
220
- options.declare(:smtp, 'SMTP configuration', types: Hash)
221
- options.declare(:notify_to, 'Email recipient for notification of transfers')
222
- options.declare(:notify_template, 'Email ERB template for notification of transfers')
205
+ options.declare(:smtp, 'Email: SMTP configuration', types: Hash)
206
+ options.declare(:notify_to, 'Email: Recipient for notification of transfers')
207
+ options.declare(:notify_template, 'Email: ERB template for notification of transfers')
223
208
  # HTTP options
224
- options.declare(:insecure, 'Do not validate any HTTPS certificate', values: :bool, handler: {o: self, m: :option_insecure}, default: :no)
225
- options.declare(:ignore_certificate, 'Do not validate HTTPS certificate for these URLs', types: Array, handler: {o: self, m: :option_ignore_cert_host_port})
226
- options.declare(:silent_insecure, 'Issue a warning if certificate is ignored', values: :bool, handler: {o: self, m: :option_warn_insecure_cert}, default: :yes)
227
- options.declare(:cert_stores, 'List of folder with trusted certificates', types: [Array, String], handler: {o: self, m: :trusted_cert_locations})
228
- options.declare(:http_options, 'Options for HTTP/S socket', types: Hash, handler: {o: self, m: :option_http_options}, default: {})
229
- options.declare(:http_proxy, 'URL for HTTP proxy with optional credentials', types: String, handler: {o: self, m: :option_http_proxy})
209
+ options.declare(:insecure, 'HTTP/S: Do not validate any certificate', values: :bool, handler: {o: self, m: :option_insecure}, default: :no)
210
+ options.declare(:ignore_certificate, 'HTTP/S: Do not validate certificate for these URLs', types: Array, handler: {o: self, m: :option_ignore_cert_host_port})
211
+ options.declare(:warn_insecure, 'HTTP/S: Issue a warning if certificate is ignored', values: :bool, handler: {o: self, m: :option_warn_insecure_cert}, default: :yes)
212
+ options.declare(:cert_stores, 'HTTP/S: List of folder with trusted certificates', types: [Array, String], handler: {o: self, m: :trusted_cert_locations})
213
+ options.declare(:http_options, 'HTTP/S: Options for HTTP/S socket', types: Hash, handler: {o: self, m: :option_http_options}, default: {})
214
+ options.declare(:http_proxy, 'HTTP/S: URL for proxy with optional credentials', types: String, handler: {o: self, m: :option_http_proxy})
230
215
  options.declare(:cache_tokens, 'Save and reuse OAuth tokens', values: :bool, handler: {o: self, m: :option_cache_tokens})
231
216
  options.declare(:fpac, 'Proxy auto configuration script')
232
217
  options.declare(:proxy_credentials, 'HTTP proxy credentials for fpac: user, password', types: Array)
@@ -256,7 +241,7 @@ module Aspera
256
241
  @pac_exec = ProxyAutoConfig.new(pac_script).register_uri_generic
257
242
  proxy_user_pass = options.get_option(:proxy_credentials)
258
243
  if !proxy_user_pass.nil?
259
- Aspera.assert(proxy_user_pass.length.eql?(2), exception_class: Cli::BadArgument){"proxy_credentials shall have two elements (#{proxy_user_pass.length})"}
244
+ Aspera.assert(proxy_user_pass.length.eql?(2), type: Cli::BadArgument){"proxy_credentials shall have two elements (#{proxy_user_pass.length})"}
260
245
  @pac_exec.proxy_user = proxy_user_pass[0]
261
246
  @pac_exec.proxy_pass = proxy_user_pass[1]
262
247
  end
@@ -362,7 +347,8 @@ module Aspera
362
347
  if !@ssl_warned_urls.include?(base_url)
363
348
  formatter.display_message(
364
349
  :error,
365
- "#{Formatter::WARNING_FLASH} Ignoring certificate for: #{base_url}. Do not deactivate certificate verification in production.")
350
+ "#{Formatter::WARNING_FLASH} Ignoring certificate for: #{base_url}. Do not deactivate certificate verification in production."
351
+ )
366
352
  @ssl_warned_urls.push(base_url)
367
353
  end
368
354
  end
@@ -378,13 +364,39 @@ module Aspera
378
364
  # Rest.io_http_session(http_session).debug_output = Log.log
379
365
  http_session.verify_mode = SELF_SIGNED_CERT if http_session.use_ssl? && ignore_cert?(http_session.address, http_session.port)
380
366
  http_session.cert_store = @certificate_store if @certificate_store
381
- Log.log.debug{"using cert store #{http_session.cert_store} (#{@certificate_store})"} unless http_session.cert_store.nil?
367
+ Log.log.debug{"Using cert store #{http_session.cert_store} (#{@certificate_store})"} unless http_session.cert_store.nil?
382
368
  @option_http_options.each do |k, v|
383
369
  method = "#{k}=".to_sym
384
370
  # check if accessor is a method of Net::HTTP
385
371
  # continue_timeout= read_timeout= write_timeout=
386
372
  if http_session.respond_to?(method)
387
373
  http_session.send(method, v)
374
+ elsif k.eql?('ssl_options')
375
+ # NOTE: here is a hack that allows setting SSLContext options
376
+ Aspera.assert_type(v, Array){'ssl_options'}
377
+ # more dynamic method, but more complex:
378
+ # Net::HTTP::SSL_ATTRIBUTES.push(:options) unless Net::HTTP::SSL_ATTRIBUTES.include?(:options)
379
+ # Net::HTTP::SSL_IVNAMES.push(:@options) unless Net::HTTP::SSL_IVNAMES.include?(:@options)
380
+ # Start with default options
381
+ ssl_options = OpenSSL::SSL::SSLContext::DEFAULT_PARAMS[:options]
382
+ v.each do |opt|
383
+ case opt
384
+ when Integer
385
+ ssl_options = opt
386
+ when String
387
+ name = "OP_#{opt.start_with?('-') ? opt[1..] : opt}".upcase
388
+ raise Cli::BadArgument, "No such ssl_option: #{name}, use one of: #{OpenSSL::SSL.constants.grep(/^OP_/).map{ |c| c.to_s.sub(/^OP_/, '')}.join(', ')}" if !OpenSSL::SSL.const_defined?(name)
389
+ if opt.start_with?('-')
390
+ ssl_options &= ~OpenSSL::SSL.const_get(name)
391
+ else
392
+ ssl_options |= OpenSSL::SSL.const_get(name)
393
+ end
394
+ else
395
+ Aspera.error_unexpected_value(opt.class.name){'Expected String or Integer in ssl_options'}
396
+ end
397
+ end
398
+ # http_session.instance_variable_set(:@options, ssl_options)
399
+ OpenSSL::SSL::SSLContext::DEFAULT_PARAMS[:options] = ssl_options
388
400
  else
389
401
  Log.log.error{"no such HTTP session attribute: #{k}"}
390
402
  end
@@ -423,7 +435,8 @@ module Aspera
423
435
  check_date_persist = PersistencyActionOnce.new(
424
436
  manager: persistency,
425
437
  data: last_check_array,
426
- id: 'version_last_check')
438
+ id: 'version_last_check'
439
+ )
427
440
  # get persisted date or nil
428
441
  current_date = Date.today
429
442
  last_check_days = (current_date - Date.strptime(last_check_array.first, GEM_CHECK_DATE_FMT)) rescue nil
@@ -447,7 +460,7 @@ module Aspera
447
460
  default_config_name = get_plugin_default_config_name(plugin_name_sym)
448
461
  Log.log.debug{"add_plugin_default_preset:#{plugin_name_sym}:#{default_config_name}"}
449
462
  options.add_option_preset(preset_by_name(default_config_name), 'default_plugin', override: false) unless default_config_name.nil?
450
- return nil
463
+ return
451
464
  end
452
465
 
453
466
  # get the default global preset, or set default one
@@ -495,12 +508,12 @@ module Aspera
495
508
  # @return the hash from name (also expands possible includes)
496
509
  # @param config_name name of the preset in config file
497
510
  # @param include_path used to detect and avoid include loops
498
- def preset_by_name(config_name, include_path=[])
511
+ def preset_by_name(config_name, include_path = [])
499
512
  raise Cli::Error, 'loop in include' if include_path.include?(config_name)
500
513
  include_path = include_path.clone # avoid messing up if there are multiple branches
501
514
  current = @config_presets
502
515
  config_name.split(PRESET_DIG_SEPARATOR).each do |name|
503
- Aspera.assert_type(current, Hash, exception_class: Cli::Error){"sub key: #{include_path}"}
516
+ Aspera.assert_type(current, Hash, type: Cli::Error){"sub key: #{include_path}"}
504
517
  include_path.push(name)
505
518
  current = current[name]
506
519
  raise Cli::Error, "No such config preset: #{include_path}" if current.nil?
@@ -537,7 +550,7 @@ module Aspera
537
550
  when String
538
551
  options.add_option_preset(preset_by_name(value), 'set_by_name')
539
552
  else
540
- raise 'Preset definition must be a String for preset name, or Hash for set of values'
553
+ raise BadArgument, 'Preset definition must be a String for preset name, or Hash for set of values'
541
554
  end
542
555
  end
543
556
 
@@ -563,12 +576,12 @@ module Aspera
563
576
  @config_checksum_on_disk = config_checksum
564
577
  end
565
578
  files_to_copy = []
566
- Log.log.trace1{Log.dump('Available_presets', @config_presets)}
579
+ Log.dump(:available_presets, @config_presets, level: :trace1)
567
580
  Aspera.assert_type(@config_presets, Hash){'config file YAML'}
568
581
  # check there is at least the config section
569
582
  Aspera.assert(@config_presets.key?(CONF_PRESET_CONFIG)){"Cannot find key: #{CONF_PRESET_CONFIG}"}
570
583
  version = @config_presets[CONF_PRESET_CONFIG][CONF_PRESET_VERSION]
571
- raise 'No version found in config section.' if version.nil?
584
+ raise Error, 'No version found in config section.' if version.nil?
572
585
  Log.log.debug{"conf version: #{version}"}
573
586
  # VVV if there are any conversion needed, those happen here.
574
587
  # fix bug in 4.4 (creating key "true" in "default" preset)
@@ -598,46 +611,6 @@ module Aspera
598
611
  raise Cli::Error, e.to_s
599
612
  end
600
613
 
601
- # Find a plugin, and issue the "require"
602
- # @return [Hash] plugin info: { product: , url:, version: }
603
- def identify_plugins_for_url
604
- app_url = options.get_next_argument('url', mandatory: true)
605
- check_only = options.get_next_argument('plugin name', mandatory: false)
606
- check_only = check_only.to_sym unless check_only.nil?
607
- found_apps = []
608
- my_self_plugin_sym = self.class.name.split('::').last.downcase.to_sym
609
- PluginFactory.instance.plugin_list.each do |plugin_name_sym|
610
- # no detection for internal plugin
611
- next if plugin_name_sym.eql?(my_self_plugin_sym)
612
- next if check_only && !check_only.eql?(plugin_name_sym)
613
- # load plugin class
614
- detect_plugin_class = PluginFactory.instance.plugin_class(plugin_name_sym)
615
- # requires detection method
616
- next unless detect_plugin_class.respond_to?(:detect)
617
- detection_info = nil
618
- begin
619
- Log.log.debug{"detecting #{plugin_name_sym} at #{app_url}"}
620
- formatter.long_operation_running("#{plugin_name_sym}\r")
621
- detection_info = detect_plugin_class.detect(app_url)
622
- rescue OpenSSL::SSL::SSLError => e
623
- Log.log.warn(e.message)
624
- Log.log.warn('Use option --insecure=yes to allow unchecked certificate') if e.message.include?('cert')
625
- rescue StandardError => e
626
- Log.log.debug{"detect error: [#{e.class}] #{e}"}
627
- next
628
- end
629
- next if detection_info.nil?
630
- Aspera.assert_type(detection_info, Hash)
631
- Aspera.assert_type(detection_info[:url], String) if detection_info.key?(:url)
632
- app_name = detect_plugin_class.respond_to?(:application_name) ? detect_plugin_class.application_name : detect_plugin_class.name.split('::').last
633
- # if there is a redirect, then the detector can override the url.
634
- found_apps.push({product: plugin_name_sym, name: app_name, url: app_url, version: 'unknown'}.merge(detection_info))
635
- end
636
- raise "No known application found at #{app_url}" if found_apps.empty?
637
- Aspera.assert(found_apps.all?{ |a| a.keys.all?(Symbol)})
638
- return found_apps
639
- end
640
-
641
614
  def execute_connect_action
642
615
  command = options.get_next_command(%i[list info version])
643
616
  if %i[info version].include?(command)
@@ -686,12 +659,12 @@ module Aspera
686
659
  set_global_default(:ascp_path, ascp_path)
687
660
  return Main.result_nothing
688
661
  when :show
689
- return Main.result_status(Ascp::Installation.instance.path(:ascp))
662
+ return Main.result_text(Ascp::Installation.instance.path(:ascp))
690
663
  when :info
691
664
  # collect info from ascp executable
692
665
  data = Ascp::Installation.instance.ascp_info
693
666
  # add command line transfer spec
694
- data['ts'] = transfer.updated_ts
667
+ data['ts'] = transfer.option_transfer_spec
695
668
  # add keys
696
669
  DataRepository::ELEMENTS.each_with_object(data){ |i, h| h[i.to_s] = DataRepository.instance.item(i)}
697
670
  # declare those as secrets
@@ -715,10 +688,8 @@ module Aspera
715
688
  n, v = Ascp::Installation.instance.install_sdk(url: options.get_option(:sdk_url, mandatory: true), version: version)
716
689
  return Main.result_status("Installed #{n} version #{v}")
717
690
  when :spec
718
- return Main.result_object_list(
719
- Transfer::SpecDoc.man_table(formatter, cli: false),
720
- fields: Transfer::SpecDoc::TABLE_COLUMNS.map(&:to_s)
721
- )
691
+ fields, data = Transfer::SpecDoc.man_table(Formatter)
692
+ return Main.result_object_list(data, fields: fields.map(&:to_s))
722
693
  when :schema
723
694
  schema = Transfer::Spec::SCHEMA.merge({'$comment'=>'DO NOT EDIT, this file was generated from the YAML.'})
724
695
  agent = options.get_next_argument('transfer agent name', mandatory: false)
@@ -772,7 +743,7 @@ module Aspera
772
743
  raise "no such preset: #{name}" if PRESET_EXIST_ACTIONS.include?(action) && !@config_presets.key?(name)
773
744
  case action
774
745
  when :list
775
- return Main.result_value_list(@config_presets.keys, 'name')
746
+ return Main.result_value_list(@config_presets.keys, name: 'name')
776
747
  when :overview
777
748
  # display process modifies the value (hide secrets): we do not want to save removed secrets
778
749
  data = self.class.deep_clone(@config_presets)
@@ -809,9 +780,7 @@ module Aspera
809
780
  return Main.result_nothing
810
781
  when :initialize
811
782
  config_value = options.get_next_argument('extended value', validation: Hash)
812
- if @config_presets.key?(name)
813
- Log.log.warn{"configuration already exists: #{name}, overwriting"}
814
- end
783
+ Log.log.warn{"configuration already exists: #{name}, overwriting"} if @config_presets.key?(name)
815
784
  @config_presets[name] = config_value
816
785
  return Main.result_status("Modified: #{@option_config_file}")
817
786
  when :update
@@ -834,7 +803,7 @@ module Aspera
834
803
  url = options.get_option(:url, mandatory: true)
835
804
  user = options.get_option(:username, mandatory: true)
836
805
  result = lookup_preset(url: url, username: user)
837
- raise 'no such config found' if result.nil?
806
+ raise Error, 'no such config found' if result.nil?
838
807
  return Main.result_single_object(result)
839
808
  when :secure
840
809
  identifier = options.get_next_argument('config name', mandatory: false)
@@ -901,7 +870,7 @@ module Aspera
901
870
  when :preset # newer syntax
902
871
  return execute_preset
903
872
  when :open
904
- Environment.open_editor(@option_config_file.to_s)
873
+ Environment.instance.open_editor(@option_config_file.to_s)
905
874
  return Main.result_nothing
906
875
  when :documentation
907
876
  section = options.get_next_argument('private key file path', mandatory: false)
@@ -910,12 +879,12 @@ module Aspera
910
879
  return Main.result_nothing
911
880
  when :genkey # generate new rsa key
912
881
  private_key_path = options.get_next_argument('private key file path')
913
- private_key_length = options.get_next_argument('size in bits', mandatory: false, validation: Integer, default: DEFAULT_PRIV_KEY_LENGTH)
914
- self.class.generate_rsa_private_key(path: private_key_path, length: private_key_length)
882
+ private_key_length = options.get_next_argument('size in bits', mandatory: false, validation: Integer, default: OAuth::Jwt::DEFAULT_PRIV_KEY_LENGTH)
883
+ OAuth::Jwt.generate_rsa_private_key(path: private_key_path, length: private_key_length)
915
884
  return Main.result_status("Generated #{private_key_length} bit RSA key: #{private_key_path}")
916
885
  when :pubkey # get pub key
917
886
  private_key_pem = options.get_next_argument('private key PEM value')
918
- return Main.result_status(OpenSSL::PKey::RSA.new(private_key_pem).public_key.to_s)
887
+ return Main.result_text(OpenSSL::PKey::RSA.new(private_key_pem).public_key.to_s)
919
888
  when :remote_certificate
920
889
  cert_action = options.get_next_command(%i[chain only name])
921
890
  remote_url = options.get_next_argument('remote URL')
@@ -923,20 +892,18 @@ module Aspera
923
892
  raise "No certificate found for #{remote_url}" unless remote_chain&.first
924
893
  case cert_action
925
894
  when :chain
926
- return Main.result_status(remote_chain.map(&:to_pem).join("\n"))
895
+ return Main.result_text(remote_chain.map(&:to_pem).join("\n"))
927
896
  when :only
928
- return Main.result_status(remote_chain.first.to_pem)
897
+ return Main.result_text(remote_chain.first.to_pem)
929
898
  when :name
930
- return Main.result_status(remote_chain.first.subject.to_a.find{ |name, _, _| name == 'CN'}[1])
899
+ return Main.result_text(remote_chain.first.subject.to_a.find{ |name, _, _| name == 'CN'}[1])
931
900
  end
932
901
  when :echo # display the content of a value given on command line
933
902
  return Main.result_auto(options.get_next_argument('value', validation: nil))
934
903
  when :download
935
904
  file_url = options.get_next_argument('source URL').chomp
936
905
  file_dest = options.get_next_argument('file path', mandatory: false)
937
- if file_dest.nil?
938
- file_dest = File.join(transfer.destination_folder(Transfer::Spec::DIRECTION_RECEIVE), file_url.gsub(%r{.*/}, ''))
939
- end
906
+ file_dest = File.join(transfer.destination_folder(Transfer::Spec::DIRECTION_RECEIVE), file_url.gsub(%r{.*/}, '')) if file_dest.nil?
940
907
  formatter.display_status("Downloading: #{file_url}")
941
908
  Rest.new(base_url: file_url).call(operation: 'GET', save_to_file: file_dest)
942
909
  return Main.result_status("Saved to: #{file_dest}")
@@ -990,27 +957,27 @@ module Aspera
990
957
  # interactive mode
991
958
  options.ask_missing_mandatory = true
992
959
  # detect plugins by url and optional query
993
- apps = identify_plugins_for_url.freeze
960
+ apps = @wizard.identify_plugins_for_url.freeze
994
961
  return Main.result_object_list(apps) if action.eql?(:detect)
995
- return wizard_find(apps)
962
+ return @wizard.find(apps)
996
963
  when :coffee
997
- return Main.result_image(COFFEE_IMAGE, formatter: formatter)
964
+ return Main.result_image(COFFEE_IMAGE_URL)
998
965
  when :image
999
- return Main.result_image(options.get_next_argument('image uri or blob'), formatter: formatter)
966
+ return Main.result_image(options.get_next_argument('image URI or blob'))
1000
967
  when :ascp
1001
968
  execute_action_ascp
1002
969
  when :transferd
1003
970
  execute_action_transferd
1004
971
  when :gem
1005
972
  case options.get_next_command(%i[path version name])
1006
- when :path then return Main.result_status(self.class.gem_src_root)
1007
- when :version then return Main.result_status(Cli::VERSION)
1008
- when :name then return Main.result_status(Info::GEM_NAME)
973
+ when :path then return Main.result_text(self.class.gem_src_root)
974
+ when :version then return Main.result_text(Cli::VERSION)
975
+ when :name then return Main.result_text(Info::GEM_NAME)
1009
976
  end
1010
977
  when :folder
1011
- return Main.result_status(@main_folder)
978
+ return Main.result_text(@main_folder)
1012
979
  when :file
1013
- return Main.result_status(@option_config_file)
980
+ return Main.result_text(@option_config_file)
1014
981
  when :email_test
1015
982
  send_email_template(email_template_default: EMAIL_TEST_TEMPLATE)
1016
983
  return Main.result_nothing
@@ -1020,7 +987,7 @@ module Aspera
1020
987
  # ensure fpac was provided
1021
988
  options.get_option(:fpac, mandatory: true)
1022
989
  server_url = options.get_next_argument('server url')
1023
- return Main.result_status(@pac_exec.find_proxy_for_url(server_url))
990
+ return Main.result_text(@pac_exec.get_proxies(server_url))
1024
991
  when :check_update
1025
992
  return Main.result_single_object(check_gem_version)
1026
993
  when :initdemo
@@ -1047,101 +1014,11 @@ module Aspera
1047
1014
  when :vault then execute_vault
1048
1015
  when :test then return execute_test
1049
1016
  when :platform
1050
- return Main.result_status(Environment.architecture)
1017
+ return Main.result_text(Environment.instance.architecture)
1051
1018
  else Aspera.error_unreachable_line
1052
1019
  end
1053
1020
  end
1054
1021
 
1055
- def wizard_find(apps)
1056
- identification = if apps.length.eql?(1)
1057
- Log.log.debug{"Detected: #{identification}"}
1058
- apps.first
1059
- else
1060
- formatter.display_status('Multiple applications detected, please select from:')
1061
- formatter.display_results(type: :object_list, data: apps, fields: %w[product url version])
1062
- answer = options.prompt_user_input_in_list('product', apps.map{ |a| a[:product]})
1063
- apps.find{ |a| a[:product].eql?(answer)}
1064
- end
1065
- wiz_preset_name = options.get_next_argument('preset name', default: '')
1066
- Log.log.debug{Log.dump(:identification, identification)}
1067
- wiz_url = identification[:url]
1068
- formatter.display_status("Using: #{identification[:name]} at #{wiz_url}".bold)
1069
- # set url for instantiation of plugin
1070
- options.add_option_preset({url: wiz_url}, 'wizard')
1071
- # instantiate plugin: command line options will be known and wizard can be called
1072
- wiz_plugin_class = PluginFactory.instance.plugin_class(identification[:product])
1073
- Aspera.assert(wiz_plugin_class.respond_to?(:wizard), exception_class: Cli::BadArgument) do
1074
- "Detected: #{identification[:product]}, but this application has no wizard"
1075
- end
1076
- # instantiate plugin: command line options will be known, e.g. private_key
1077
- plugin_instance = wiz_plugin_class.new(**init_params)
1078
- wiz_params = {
1079
- object: plugin_instance
1080
- }
1081
- # is private key needed ?
1082
- if options.known_options.key?(:private_key) &&
1083
- (!wiz_plugin_class.respond_to?(:private_key_required?) || wiz_plugin_class.private_key_required?(wiz_url))
1084
- # lets see if path to priv key is provided
1085
- private_key_path = options.get_option(:key_path)
1086
- # give a chance to provide
1087
- if private_key_path.nil?
1088
- formatter.display_status('Please provide the path to your private RSA key, or nothing to generate one:')
1089
- private_key_path = options.get_option(:key_path, mandatory: true).to_s
1090
- end
1091
- # else generate path
1092
- if private_key_path.empty?
1093
- private_key_path = File.join(@main_folder, DEFAULT_PRIV_KEY_FILENAME)
1094
- end
1095
- if File.exist?(private_key_path)
1096
- formatter.display_status('Using existing key:')
1097
- else
1098
- formatter.display_status("Generating #{DEFAULT_PRIV_KEY_LENGTH} bit RSA key...")
1099
- self.class.generate_rsa_private_key(path: private_key_path)
1100
- formatter.display_status('Created key:')
1101
- end
1102
- formatter.display_status(private_key_path)
1103
- private_key_pem = File.read(private_key_path)
1104
- options.set_option(:private_key, private_key_pem)
1105
- wiz_params[:private_key_path] = private_key_path
1106
- wiz_params[:pub_key_pem] = OpenSSL::PKey::RSA.new(private_key_pem).public_key.to_s
1107
- end
1108
- Log.log.debug{Log.dump(:wiz_params, wiz_params)}
1109
- # finally, call the wizard
1110
- wizard_result = wiz_plugin_class.wizard(**wiz_params)
1111
- Log.log.debug{"wizard result: #{wizard_result}"}
1112
- Aspera.assert(WIZARD_RESULT_KEYS.eql?(wizard_result.keys.sort)){"missing or extra keys in wizard result: #{wizard_result.keys}"}
1113
- # get preset name from user or default
1114
- if wiz_preset_name.empty?
1115
- elements = [
1116
- identification[:product],
1117
- URI.parse(wiz_url).host
1118
- ]
1119
- elements.push(options.get_option(:username, mandatory: true)) unless wizard_result[:preset_value].key?(:link) rescue nil
1120
- wiz_preset_name = elements.join('_').strip.downcase.gsub(/[^a-z0-9]/, '_').squeeze('_')
1121
- end
1122
- # test mode does not change conf file
1123
- return Main.result_single_object(wizard_result) if options.get_option(:test_mode)
1124
- # Write configuration file
1125
- formatter.display_status("Preparing preset: #{wiz_preset_name}")
1126
- # init defaults if necessary
1127
- @config_presets[CONF_PRESET_DEFAULTS] ||= {}
1128
- option_override = options.get_option(:override, mandatory: true)
1129
- raise Cli::Error, "A default configuration already exists for plugin '#{identification[:product]}' (use --override=yes or --default=no)" \
1130
- if !option_override && options.get_option(:default, mandatory: true) && @config_presets[CONF_PRESET_DEFAULTS].key?(identification[:product])
1131
- raise Cli::Error, "Preset already exists: #{wiz_preset_name} (use --override=yes or --id=<name>)" \
1132
- if !option_override && @config_presets.key?(wiz_preset_name)
1133
- @config_presets[wiz_preset_name] = wizard_result[:preset_value].stringify_keys
1134
- test_args = wizard_result[:test_args]
1135
- if options.get_option(:default, mandatory: true)
1136
- formatter.display_status("Setting config preset as default for #{identification[:product]}")
1137
- @config_presets[CONF_PRESET_DEFAULTS][identification[:product].to_s] = wiz_preset_name
1138
- else
1139
- test_args = "-P#{wiz_preset_name} #{test_args}"
1140
- end
1141
- # TODO: actually test the command
1142
- return Main.result_status("You can test with:\n#{Info::CMD_NAME} #{identification[:product]} #{test_args}")
1143
- end
1144
-
1145
1022
  # @return [Hash] email server setting with defaults if not defined
1146
1023
  def email_settings
1147
1024
  smtp = options.get_option(:smtp, mandatory: true)
@@ -1193,7 +1070,7 @@ module Aspera
1193
1070
  end
1194
1071
  # execute template
1195
1072
  msg_with_headers = ERB.new(notify_template).result(template_binding)
1196
- Log.log.debug{Log.dump(:msg_with_headers, msg_with_headers)}
1073
+ Log.dump(:msg_with_headers, msg_with_headers)
1197
1074
  require 'net/smtp'
1198
1075
  smtp = Net::SMTP.new(mail_conf[:server], mail_conf[:port])
1199
1076
  smtp.enable_starttls if mail_conf[:tls]
@@ -1207,7 +1084,7 @@ module Aspera
1207
1084
  # Save current configuration to config file
1208
1085
  # return true if file was saved
1209
1086
  def save_config_file_if_needed
1210
- raise 'no configuration loaded' if @config_presets.nil?
1087
+ raise Error, 'no configuration loaded' if @config_presets.nil?
1211
1088
  current_checksum = config_checksum
1212
1089
  return false if @config_checksum_on_disk.eql?(current_checksum)
1213
1090
  FileUtils.mkdir_p(@main_folder)
@@ -1225,7 +1102,7 @@ module Aspera
1225
1102
  Aspera.assert(!@config_presets.nil?){'config_presets shall be defined'}
1226
1103
  if !@use_plugin_defaults
1227
1104
  Log.log.debug('skip default config')
1228
- return nil
1105
+ return
1229
1106
  end
1230
1107
  if @config_presets.key?(CONF_PRESET_DEFAULTS) &&
1231
1108
  @config_presets[CONF_PRESET_DEFAULTS].key?(plugin_name_sym.to_s)
@@ -1241,7 +1118,7 @@ module Aspera
1241
1118
  raise Cli::Error, "Config name [#{default_config_name}] must be a hash, check config file." if !@config_presets[default_config_name].is_a?(Hash)
1242
1119
  return default_config_name
1243
1120
  end
1244
- return nil
1121
+ return
1245
1122
  end
1246
1123
 
1247
1124
  # @return [Hash] result of execution of vault command
@@ -1256,11 +1133,7 @@ module Aspera
1256
1133
  when :show
1257
1134
  return Main.result_single_object(vault.get(label: options.get_next_argument('label')))
1258
1135
  when :create
1259
- label = options.get_next_argument('label', validation: String)
1260
- info = options.get_next_argument('info', validation: Hash)
1261
- info = info.symbolize_keys
1262
- info[:label] = label
1263
- vault.set(info)
1136
+ vault.set(options.get_next_argument('info', validation: Hash).symbolize_keys)
1264
1137
  return Main.result_status('Secret added')
1265
1138
  when :delete
1266
1139
  label_to_delete = options.get_next_argument('label')
@@ -1276,7 +1149,7 @@ module Aspera
1276
1149
  # @return [String] value from vault matching <name>.<param>
1277
1150
  def vault_value(name)
1278
1151
  m = name.split('.')
1279
- raise 'vault name shall match <name>.<param>' unless m.length.eql?(2)
1152
+ raise BadArgument, 'vault name shall match <name>.<param>' unless m.length.eql?(2)
1280
1153
  # this raise exception if label not found:
1281
1154
  info = vault.get(label: m[0])
1282
1155
  value = info[m[1].to_sym]
@@ -1298,6 +1171,7 @@ module Aspera
1298
1171
  )
1299
1172
  end
1300
1173
 
1174
+ # Artifically raise an exception for tests
1301
1175
  def execute_test
1302
1176
  case options.get_next_command(%i[throw web])
1303
1177
  when :throw
@@ -1305,18 +1179,20 @@ module Aspera
1305
1179
  # options
1306
1180
  exception_class_name = options.get_next_argument('exception class name', mandatory: true)
1307
1181
  exception_text = options.get_next_argument('exception text', mandatory: true)
1308
- exception_class = Object.const_get(exception_class_name)
1309
- Aspera.assert(exception_class <= Exception){"#{exception_class} is not an exception: #{exception_class.class}"}
1310
- raise exception_class, exception_text
1182
+ type = Object.const_get(exception_class_name)
1183
+ Aspera.assert(type <= Exception){"#{type} is not an exception: #{type.class}"}
1184
+ raise type, exception_text
1311
1185
  when :web
1312
1186
  end
1313
1187
  end
1314
1188
 
1315
1189
  # version of URL without trailing "/" and removing default port
1316
1190
  def canonical_url(url)
1317
- url.sub(%r{/+$}, '').sub(%r{^(https://[^/]+):443$}, '\1')
1191
+ url.chomp('/').sub(%r{^(https://[^/]+):443$}, '\1')
1318
1192
  end
1319
1193
 
1194
+ # Look for a preset that has the corresponding URL and username
1195
+ # @return the first one matching
1320
1196
  def lookup_preset(url:, username:)
1321
1197
  # remove extra info to maximize match
1322
1198
  url = canonical_url(url)
@@ -1329,6 +1205,8 @@ module Aspera
1329
1205
  nil
1330
1206
  end
1331
1207
 
1208
+ # Lookup the corresponding secret for the given URL and usernames
1209
+ # @raise Exception if mandatory and not found
1332
1210
  def lookup_secret(url:, username:, mandatory: false)
1333
1211
  secret = options.get_option(:secret)
1334
1212
  if secret.nil?