aspera-cli 4.23.0 → 4.24.1

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 (110) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/CHANGELOG.md +37 -1
  4. data/CONTRIBUTING.md +86 -29
  5. data/README.md +2109 -1300
  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 +44 -31
  16. data/lib/aspera/api/cos_node.rb +7 -5
  17. data/lib/aspera/api/httpgw.rb +15 -18
  18. data/lib/aspera/api/node.rb +104 -22
  19. data/lib/aspera/ascmd.rb +22 -16
  20. data/lib/aspera/ascp/installation.rb +37 -40
  21. data/lib/aspera/ascp/management.rb +5 -4
  22. data/lib/aspera/assert.rb +54 -23
  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 +29 -3
  28. data/lib/aspera/cli/main.rb +138 -107
  29. data/lib/aspera/cli/manager.rb +50 -30
  30. data/lib/aspera/cli/plugin.rb +148 -77
  31. data/lib/aspera/cli/plugin_factory.rb +2 -2
  32. data/lib/aspera/cli/plugins/aoc.rb +189 -70
  33. data/lib/aspera/cli/plugins/ats.rb +15 -13
  34. data/lib/aspera/cli/plugins/config.rb +100 -214
  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 +164 -165
  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 +144 -162
  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 +28 -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 +51 -50
  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 +157 -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/environment.rb +145 -101
  55. data/lib/aspera/faspex_gw.rb +1 -1
  56. data/lib/aspera/faspex_postproc.rb +3 -2
  57. data/lib/aspera/hash_ext.rb +1 -1
  58. data/lib/aspera/id_generator.rb +10 -10
  59. data/lib/aspera/keychain/base.rb +18 -0
  60. data/lib/aspera/keychain/encrypted_hash.rb +6 -12
  61. data/lib/aspera/keychain/factory.rb +9 -3
  62. data/lib/aspera/keychain/hashicorp_vault.rb +9 -6
  63. data/lib/aspera/keychain/macos_security.rb +13 -13
  64. data/lib/aspera/log.rb +91 -19
  65. data/lib/aspera/nagios.rb +5 -6
  66. data/lib/aspera/node_simulator.rb +12 -7
  67. data/lib/aspera/oauth/base.rb +5 -3
  68. data/lib/aspera/oauth/factory.rb +24 -18
  69. data/lib/aspera/oauth/jwt.rb +13 -1
  70. data/lib/aspera/oauth/url_json.rb +3 -3
  71. data/lib/aspera/oauth/web.rb +5 -3
  72. data/lib/aspera/persistency_folder.rb +2 -2
  73. data/lib/aspera/preview/file_types.rb +4 -3
  74. data/lib/aspera/preview/generator.rb +25 -12
  75. data/lib/aspera/preview/terminal.rb +10 -7
  76. data/lib/aspera/preview/utils.rb +11 -9
  77. data/lib/aspera/products/connect.rb +1 -1
  78. data/lib/aspera/products/desktop.rb +1 -1
  79. data/lib/aspera/products/other.rb +2 -2
  80. data/lib/aspera/products/transferd.rb +8 -6
  81. data/lib/aspera/proxy_auto_config.rb +1 -1
  82. data/lib/aspera/rest.rb +29 -22
  83. data/lib/aspera/rest_call_error.rb +1 -1
  84. data/lib/aspera/resumer.rb +1 -1
  85. data/lib/aspera/secret_hider.rb +46 -40
  86. data/lib/aspera/ssh.rb +13 -3
  87. data/lib/aspera/sync/args.schema.yaml +102 -0
  88. data/lib/aspera/sync/conf.schema.yaml +701 -0
  89. data/lib/aspera/sync/database.rb +83 -0
  90. data/lib/aspera/sync/operations.rb +296 -0
  91. data/lib/aspera/temp_file_manager.rb +3 -2
  92. data/lib/aspera/transfer/error.rb +1 -1
  93. data/lib/aspera/transfer/error_info.rb +1 -2
  94. data/lib/aspera/transfer/faux_file.rb +11 -10
  95. data/lib/aspera/transfer/parameters.rb +6 -5
  96. data/lib/aspera/transfer/spec.rb +15 -1
  97. data/lib/aspera/transfer/spec.schema.yaml +316 -293
  98. data/lib/aspera/transfer/spec_doc.rb +34 -16
  99. data/lib/aspera/transfer/uri.rb +5 -5
  100. data/lib/aspera/uri_reader.rb +14 -10
  101. data/lib/aspera/web_auth.rb +2 -2
  102. data/lib/aspera/web_server_simple.rb +2 -2
  103. data.tar.gz.sig +0 -0
  104. metadata +15 -13
  105. metadata.gz.sig +0 -0
  106. data/lib/aspera/transfer/async_conf.schema.yaml +0 -716
  107. data/lib/aspera/transfer/convert.rb +0 -29
  108. data/lib/aspera/transfer/sync.rb +0 -232
  109. data/lib/aspera/transfer/sync_instance.schema.yaml +0 -20
  110. data/lib/aspera/transfer/sync_session.schema.yaml +0 -86
@@ -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,11 +192,8 @@ 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
198
  options.declare(:ascp_path, 'Ascp: Path to ascp', handler: {o: Ascp::Installation.instance, m: :ascp_path})
214
199
  options.declare(:use_product, 'Ascp: Use ascp from specified product', handler: {o: self, m: :option_use_product})
@@ -223,7 +208,7 @@ module Aspera
223
208
  # HTTP options
224
209
  options.declare(:insecure, 'HTTP/S: Do not validate any certificate', values: :bool, handler: {o: self, m: :option_insecure}, default: :no)
225
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})
226
- options.declare(:silent_insecure, 'HTTP/S: Issue a warning if certificate is ignored', values: :bool, handler: {o: self, m: :option_warn_insecure_cert}, default: :yes)
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)
227
212
  options.declare(:cert_stores, 'HTTP/S: List of folder with trusted certificates', types: [Array, String], handler: {o: self, m: :trusted_cert_locations})
228
213
  options.declare(:http_options, 'HTTP/S: Options for HTTP/S socket', types: Hash, handler: {o: self, m: :option_http_options}, default: {})
229
214
  options.declare(:http_proxy, 'HTTP/S: URL for proxy with optional credentials', types: String, handler: {o: self, m: :option_http_proxy})
@@ -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
@@ -460,6 +473,19 @@ module Aspera
460
473
  return result
461
474
  end
462
475
 
476
+ def defaults_set(plugin_name, preset_name, preset_values, option_default, option_override)
477
+ @config_presets[CONF_PRESET_DEFAULTS] ||= {}
478
+ raise Cli::Error, "A default configuration already exists for plugin '#{plugin_name}' (use --override=yes or --default=no)" \
479
+ if !option_override && option_default && @config_presets[CONF_PRESET_DEFAULTS].key?(plugin_name)
480
+ raise Cli::Error, "Preset already exists: #{preset_name} (use --override=yes or provide alternate name on command line)" \
481
+ if !option_override && @config_presets.key?(preset_name)
482
+ if option_default
483
+ formatter.display_status("Setting config preset as default for #{plugin_name}")
484
+ @config_presets[CONF_PRESET_DEFAULTS][plugin_name.to_s] = preset_name
485
+ end
486
+ @config_presets[preset_name] = preset_values
487
+ end
488
+
463
489
  def set_preset_key(preset, param_name, param_value)
464
490
  Aspera.assert_values(param_name.class, [String, Symbol]){'parameter'}
465
491
  param_name = param_name.to_s
@@ -492,15 +518,15 @@ module Aspera
492
518
  attr_reader :gem_url
493
519
  attr_accessor :option_config_file
494
520
 
495
- # @return the hash from name (also expands possible includes)
496
521
  # @param config_name name of the preset in config file
497
522
  # @param include_path used to detect and avoid include loops
498
- def preset_by_name(config_name, include_path=[])
523
+ # @return copy of the hash from name (also expands possible includes)
524
+ def preset_by_name(config_name, include_path = [])
499
525
  raise Cli::Error, 'loop in include' if include_path.include?(config_name)
500
526
  include_path = include_path.clone # avoid messing up if there are multiple branches
501
527
  current = @config_presets
502
528
  config_name.split(PRESET_DIG_SEPARATOR).each do |name|
503
- Aspera.assert_type(current, Hash, exception_class: Cli::Error){"sub key: #{include_path}"}
529
+ Aspera.assert_type(current, Hash, type: Cli::Error){"sub key: #{include_path}"}
504
530
  include_path.push(name)
505
531
  current = current[name]
506
532
  raise Cli::Error, "No such config preset: #{include_path}" if current.nil?
@@ -537,7 +563,7 @@ module Aspera
537
563
  when String
538
564
  options.add_option_preset(preset_by_name(value), 'set_by_name')
539
565
  else
540
- raise 'Preset definition must be a String for preset name, or Hash for set of values'
566
+ raise BadArgument, 'Preset definition must be a String for preset name, or Hash for set of values'
541
567
  end
542
568
  end
543
569
 
@@ -563,12 +589,12 @@ module Aspera
563
589
  @config_checksum_on_disk = config_checksum
564
590
  end
565
591
  files_to_copy = []
566
- Log.log.trace1{Log.dump('Available_presets', @config_presets)}
592
+ Log.dump(:available_presets, @config_presets, level: :trace1)
567
593
  Aspera.assert_type(@config_presets, Hash){'config file YAML'}
568
594
  # check there is at least the config section
569
595
  Aspera.assert(@config_presets.key?(CONF_PRESET_CONFIG)){"Cannot find key: #{CONF_PRESET_CONFIG}"}
570
596
  version = @config_presets[CONF_PRESET_CONFIG][CONF_PRESET_VERSION]
571
- raise 'No version found in config section.' if version.nil?
597
+ raise Error, 'No version found in config section.' if version.nil?
572
598
  Log.log.debug{"conf version: #{version}"}
573
599
  # VVV if there are any conversion needed, those happen here.
574
600
  # fix bug in 4.4 (creating key "true" in "default" preset)
@@ -598,46 +624,6 @@ module Aspera
598
624
  raise Cli::Error, e.to_s
599
625
  end
600
626
 
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
627
  def execute_connect_action
642
628
  command = options.get_next_command(%i[list info version])
643
629
  if %i[info version].include?(command)
@@ -686,7 +672,7 @@ module Aspera
686
672
  set_global_default(:ascp_path, ascp_path)
687
673
  return Main.result_nothing
688
674
  when :show
689
- return Main.result_status(Ascp::Installation.instance.path(:ascp))
675
+ return Main.result_text(Ascp::Installation.instance.path(:ascp))
690
676
  when :info
691
677
  # collect info from ascp executable
692
678
  data = Ascp::Installation.instance.ascp_info
@@ -715,10 +701,8 @@ module Aspera
715
701
  n, v = Ascp::Installation.instance.install_sdk(url: options.get_option(:sdk_url, mandatory: true), version: version)
716
702
  return Main.result_status("Installed #{n} version #{v}")
717
703
  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
- )
704
+ fields, data = Transfer::SpecDoc.man_table(Formatter)
705
+ return Main.result_object_list(data, fields: fields.map(&:to_s))
722
706
  when :schema
723
707
  schema = Transfer::Spec::SCHEMA.merge({'$comment'=>'DO NOT EDIT, this file was generated from the YAML.'})
724
708
  agent = options.get_next_argument('transfer agent name', mandatory: false)
@@ -772,7 +756,7 @@ module Aspera
772
756
  raise "no such preset: #{name}" if PRESET_EXIST_ACTIONS.include?(action) && !@config_presets.key?(name)
773
757
  case action
774
758
  when :list
775
- return Main.result_value_list(@config_presets.keys, 'name')
759
+ return Main.result_value_list(@config_presets.keys, name: 'name')
776
760
  when :overview
777
761
  # display process modifies the value (hide secrets): we do not want to save removed secrets
778
762
  data = self.class.deep_clone(@config_presets)
@@ -809,9 +793,7 @@ module Aspera
809
793
  return Main.result_nothing
810
794
  when :initialize
811
795
  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
796
+ Log.log.warn{"configuration already exists: #{name}, overwriting"} if @config_presets.key?(name)
815
797
  @config_presets[name] = config_value
816
798
  return Main.result_status("Modified: #{@option_config_file}")
817
799
  when :update
@@ -834,7 +816,7 @@ module Aspera
834
816
  url = options.get_option(:url, mandatory: true)
835
817
  user = options.get_option(:username, mandatory: true)
836
818
  result = lookup_preset(url: url, username: user)
837
- raise 'no such config found' if result.nil?
819
+ raise Error, 'no such config found' if result.nil?
838
820
  return Main.result_single_object(result)
839
821
  when :secure
840
822
  identifier = options.get_next_argument('config name', mandatory: false)
@@ -901,7 +883,7 @@ module Aspera
901
883
  when :preset # newer syntax
902
884
  return execute_preset
903
885
  when :open
904
- Environment.open_editor(@option_config_file.to_s)
886
+ Environment.instance.open_editor(@option_config_file.to_s)
905
887
  return Main.result_nothing
906
888
  when :documentation
907
889
  section = options.get_next_argument('private key file path', mandatory: false)
@@ -910,12 +892,12 @@ module Aspera
910
892
  return Main.result_nothing
911
893
  when :genkey # generate new rsa key
912
894
  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)
895
+ private_key_length = options.get_next_argument('size in bits', mandatory: false, validation: Integer, default: OAuth::Jwt::DEFAULT_PRIV_KEY_LENGTH)
896
+ OAuth::Jwt.generate_rsa_private_key(path: private_key_path, length: private_key_length)
915
897
  return Main.result_status("Generated #{private_key_length} bit RSA key: #{private_key_path}")
916
898
  when :pubkey # get pub key
917
899
  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)
900
+ return Main.result_text(OpenSSL::PKey::RSA.new(private_key_pem).public_key.to_s)
919
901
  when :remote_certificate
920
902
  cert_action = options.get_next_command(%i[chain only name])
921
903
  remote_url = options.get_next_argument('remote URL')
@@ -923,20 +905,18 @@ module Aspera
923
905
  raise "No certificate found for #{remote_url}" unless remote_chain&.first
924
906
  case cert_action
925
907
  when :chain
926
- return Main.result_status(remote_chain.map(&:to_pem).join("\n"))
908
+ return Main.result_text(remote_chain.map(&:to_pem).join("\n"))
927
909
  when :only
928
- return Main.result_status(remote_chain.first.to_pem)
910
+ return Main.result_text(remote_chain.first.to_pem)
929
911
  when :name
930
- return Main.result_status(remote_chain.first.subject.to_a.find{ |name, _, _| name == 'CN'}[1])
912
+ return Main.result_text(remote_chain.first.subject.to_a.find{ |name, _, _| name == 'CN'}[1])
931
913
  end
932
914
  when :echo # display the content of a value given on command line
933
915
  return Main.result_auto(options.get_next_argument('value', validation: nil))
934
916
  when :download
935
917
  file_url = options.get_next_argument('source URL').chomp
936
918
  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
919
+ file_dest = File.join(transfer.destination_folder(Transfer::Spec::DIRECTION_RECEIVE), file_url.gsub(%r{.*/}, '')) if file_dest.nil?
940
920
  formatter.display_status("Downloading: #{file_url}")
941
921
  Rest.new(base_url: file_url).call(operation: 'GET', save_to_file: file_dest)
942
922
  return Main.result_status("Saved to: #{file_dest}")
@@ -990,27 +970,27 @@ module Aspera
990
970
  # interactive mode
991
971
  options.ask_missing_mandatory = true
992
972
  # detect plugins by url and optional query
993
- apps = identify_plugins_for_url.freeze
973
+ apps = @wizard.identify_plugins_for_url.freeze
994
974
  return Main.result_object_list(apps) if action.eql?(:detect)
995
- return wizard_find(apps)
975
+ return @wizard.find(apps)
996
976
  when :coffee
997
- return Main.result_image(COFFEE_IMAGE, formatter: formatter)
977
+ return Main.result_image(COFFEE_IMAGE_URL)
998
978
  when :image
999
- return Main.result_image(options.get_next_argument('image uri or blob'), formatter: formatter)
979
+ return Main.result_image(options.get_next_argument('image URI or blob'))
1000
980
  when :ascp
1001
981
  execute_action_ascp
1002
982
  when :transferd
1003
983
  execute_action_transferd
1004
984
  when :gem
1005
985
  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)
986
+ when :path then return Main.result_text(self.class.gem_src_root)
987
+ when :version then return Main.result_text(Cli::VERSION)
988
+ when :name then return Main.result_text(Info::GEM_NAME)
1009
989
  end
1010
990
  when :folder
1011
- return Main.result_status(@main_folder)
991
+ return Main.result_text(@main_folder)
1012
992
  when :file
1013
- return Main.result_status(@option_config_file)
993
+ return Main.result_text(@option_config_file)
1014
994
  when :email_test
1015
995
  send_email_template(email_template_default: EMAIL_TEST_TEMPLATE)
1016
996
  return Main.result_nothing
@@ -1020,7 +1000,7 @@ module Aspera
1020
1000
  # ensure fpac was provided
1021
1001
  options.get_option(:fpac, mandatory: true)
1022
1002
  server_url = options.get_next_argument('server url')
1023
- return Main.result_status(@pac_exec.find_proxy_for_url(server_url))
1003
+ return Main.result_text(@pac_exec.get_proxies(server_url))
1024
1004
  when :check_update
1025
1005
  return Main.result_single_object(check_gem_version)
1026
1006
  when :initdemo
@@ -1047,101 +1027,11 @@ module Aspera
1047
1027
  when :vault then execute_vault
1048
1028
  when :test then return execute_test
1049
1029
  when :platform
1050
- return Main.result_status(Environment.architecture)
1030
+ return Main.result_text(Environment.instance.architecture)
1051
1031
  else Aspera.error_unreachable_line
1052
1032
  end
1053
1033
  end
1054
1034
 
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
1035
  # @return [Hash] email server setting with defaults if not defined
1146
1036
  def email_settings
1147
1037
  smtp = options.get_option(:smtp, mandatory: true)
@@ -1193,7 +1083,7 @@ module Aspera
1193
1083
  end
1194
1084
  # execute template
1195
1085
  msg_with_headers = ERB.new(notify_template).result(template_binding)
1196
- Log.log.debug{Log.dump(:msg_with_headers, msg_with_headers)}
1086
+ Log.dump(:msg_with_headers, msg_with_headers)
1197
1087
  require 'net/smtp'
1198
1088
  smtp = Net::SMTP.new(mail_conf[:server], mail_conf[:port])
1199
1089
  smtp.enable_starttls if mail_conf[:tls]
@@ -1207,7 +1097,7 @@ module Aspera
1207
1097
  # Save current configuration to config file
1208
1098
  # return true if file was saved
1209
1099
  def save_config_file_if_needed
1210
- raise 'no configuration loaded' if @config_presets.nil?
1100
+ raise Error, 'no configuration loaded' if @config_presets.nil?
1211
1101
  current_checksum = config_checksum
1212
1102
  return false if @config_checksum_on_disk.eql?(current_checksum)
1213
1103
  FileUtils.mkdir_p(@main_folder)
@@ -1225,7 +1115,7 @@ module Aspera
1225
1115
  Aspera.assert(!@config_presets.nil?){'config_presets shall be defined'}
1226
1116
  if !@use_plugin_defaults
1227
1117
  Log.log.debug('skip default config')
1228
- return nil
1118
+ return
1229
1119
  end
1230
1120
  if @config_presets.key?(CONF_PRESET_DEFAULTS) &&
1231
1121
  @config_presets[CONF_PRESET_DEFAULTS].key?(plugin_name_sym.to_s)
@@ -1241,7 +1131,7 @@ module Aspera
1241
1131
  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
1132
  return default_config_name
1243
1133
  end
1244
- return nil
1134
+ return
1245
1135
  end
1246
1136
 
1247
1137
  # @return [Hash] result of execution of vault command
@@ -1256,11 +1146,7 @@ module Aspera
1256
1146
  when :show
1257
1147
  return Main.result_single_object(vault.get(label: options.get_next_argument('label')))
1258
1148
  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)
1149
+ vault.set(options.get_next_argument('info', validation: Hash).symbolize_keys)
1264
1150
  return Main.result_status('Secret added')
1265
1151
  when :delete
1266
1152
  label_to_delete = options.get_next_argument('label')
@@ -1276,7 +1162,7 @@ module Aspera
1276
1162
  # @return [String] value from vault matching <name>.<param>
1277
1163
  def vault_value(name)
1278
1164
  m = name.split('.')
1279
- raise 'vault name shall match <name>.<param>' unless m.length.eql?(2)
1165
+ raise BadArgument, 'vault name shall match <name>.<param>' unless m.length.eql?(2)
1280
1166
  # this raise exception if label not found:
1281
1167
  info = vault.get(label: m[0])
1282
1168
  value = info[m[1].to_sym]
@@ -1306,16 +1192,16 @@ module Aspera
1306
1192
  # options
1307
1193
  exception_class_name = options.get_next_argument('exception class name', mandatory: true)
1308
1194
  exception_text = options.get_next_argument('exception text', mandatory: true)
1309
- exception_class = Object.const_get(exception_class_name)
1310
- Aspera.assert(exception_class <= Exception){"#{exception_class} is not an exception: #{exception_class.class}"}
1311
- raise exception_class, exception_text
1195
+ type = Object.const_get(exception_class_name)
1196
+ Aspera.assert(type <= Exception){"#{type} is not an exception: #{type.class}"}
1197
+ raise type, exception_text
1312
1198
  when :web
1313
1199
  end
1314
1200
  end
1315
1201
 
1316
1202
  # version of URL without trailing "/" and removing default port
1317
1203
  def canonical_url(url)
1318
- url.sub(%r{/+$}, '').sub(%r{^(https://[^/]+):443$}, '\1')
1204
+ url.chomp('/').sub(%r{^(https://[^/]+):443$}, '\1')
1319
1205
  end
1320
1206
 
1321
1207
  # Look for a preset that has the corresponding URL and username