aspera-cli 4.14.0 → 4.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (104) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/BUGS.md +29 -3
  4. data/CHANGELOG.md +300 -185
  5. data/CONTRIBUTING.md +74 -23
  6. data/README.md +2346 -1619
  7. data/bin/ascli +16 -25
  8. data/bin/asession +15 -15
  9. data/examples/dascli +2 -2
  10. data/examples/proxy.pac +1 -1
  11. data/lib/aspera/aoc.rb +216 -150
  12. data/lib/aspera/ascmd.rb +25 -18
  13. data/lib/aspera/assert.rb +45 -0
  14. data/lib/aspera/cli/basic_auth_plugin.rb +9 -6
  15. data/lib/aspera/cli/error.rb +17 -0
  16. data/lib/aspera/cli/extended_value.rb +51 -16
  17. data/lib/aspera/cli/formatter.rb +276 -174
  18. data/lib/aspera/cli/hints.rb +81 -0
  19. data/lib/aspera/cli/main.rb +114 -147
  20. data/lib/aspera/cli/manager.rb +181 -136
  21. data/lib/aspera/cli/plugin.rb +82 -64
  22. data/lib/aspera/cli/plugins/alee.rb +0 -1
  23. data/lib/aspera/cli/plugins/aoc.rb +327 -331
  24. data/lib/aspera/cli/plugins/ats.rb +12 -8
  25. data/lib/aspera/cli/plugins/bss.rb +2 -2
  26. data/lib/aspera/cli/plugins/config.rb +575 -439
  27. data/lib/aspera/cli/plugins/console.rb +40 -0
  28. data/lib/aspera/cli/plugins/cos.rb +4 -5
  29. data/lib/aspera/cli/plugins/faspex.rb +111 -92
  30. data/lib/aspera/cli/plugins/faspex5.rb +245 -182
  31. data/lib/aspera/cli/plugins/node.rb +239 -160
  32. data/lib/aspera/cli/plugins/orchestrator.rb +56 -19
  33. data/lib/aspera/cli/plugins/preview.rb +54 -38
  34. data/lib/aspera/cli/plugins/server.rb +63 -20
  35. data/lib/aspera/cli/plugins/shares.rb +64 -38
  36. data/lib/aspera/cli/sync_actions.rb +68 -0
  37. data/lib/aspera/cli/transfer_agent.rb +64 -67
  38. data/lib/aspera/cli/transfer_progress.rb +73 -0
  39. data/lib/aspera/cli/version.rb +1 -1
  40. data/lib/aspera/colors.rb +3 -1
  41. data/lib/aspera/command_line_builder.rb +27 -22
  42. data/lib/aspera/cos_node.rb +6 -4
  43. data/lib/aspera/coverage.rb +22 -0
  44. data/lib/aspera/data_repository.rb +33 -2
  45. data/lib/aspera/environment.rb +21 -8
  46. data/lib/aspera/fasp/agent_alpha.rb +116 -0
  47. data/lib/aspera/fasp/agent_base.rb +40 -76
  48. data/lib/aspera/fasp/agent_connect.rb +21 -22
  49. data/lib/aspera/fasp/agent_direct.rb +169 -179
  50. data/lib/aspera/fasp/agent_httpgw.rb +200 -195
  51. data/lib/aspera/fasp/agent_node.rb +43 -35
  52. data/lib/aspera/fasp/agent_trsdk.rb +124 -41
  53. data/lib/aspera/fasp/error_info.rb +2 -2
  54. data/lib/aspera/fasp/faux_file.rb +52 -0
  55. data/lib/aspera/fasp/installation.rb +89 -191
  56. data/lib/aspera/fasp/management.rb +249 -0
  57. data/lib/aspera/fasp/parameters.rb +86 -47
  58. data/lib/aspera/fasp/parameters.yaml +75 -8
  59. data/lib/aspera/fasp/products.rb +162 -0
  60. data/lib/aspera/fasp/resume_policy.rb +7 -5
  61. data/lib/aspera/fasp/sync.rb +273 -0
  62. data/lib/aspera/fasp/transfer_spec.rb +10 -8
  63. data/lib/aspera/fasp/uri.rb +6 -6
  64. data/lib/aspera/faspex_gw.rb +11 -8
  65. data/lib/aspera/faspex_postproc.rb +8 -7
  66. data/lib/aspera/hash_ext.rb +2 -2
  67. data/lib/aspera/id_generator.rb +3 -1
  68. data/lib/aspera/json_rpc.rb +51 -0
  69. data/lib/aspera/keychain/encrypted_hash.rb +46 -11
  70. data/lib/aspera/keychain/macos_security.rb +15 -13
  71. data/lib/aspera/line_logger.rb +23 -0
  72. data/lib/aspera/log.rb +61 -19
  73. data/lib/aspera/nagios.rb +7 -2
  74. data/lib/aspera/node.rb +105 -21
  75. data/lib/aspera/node_simulator.rb +214 -0
  76. data/lib/aspera/oauth.rb +57 -36
  77. data/lib/aspera/open_application.rb +4 -4
  78. data/lib/aspera/persistency_action_once.rb +13 -14
  79. data/lib/aspera/persistency_folder.rb +5 -4
  80. data/lib/aspera/preview/file_types.rb +56 -268
  81. data/lib/aspera/preview/generator.rb +28 -39
  82. data/lib/aspera/preview/options.rb +2 -0
  83. data/lib/aspera/preview/terminal.rb +36 -16
  84. data/lib/aspera/preview/utils.rb +23 -29
  85. data/lib/aspera/proxy_auto_config.rb +6 -3
  86. data/lib/aspera/rest.rb +127 -80
  87. data/lib/aspera/rest_call_error.rb +1 -1
  88. data/lib/aspera/rest_error_analyzer.rb +16 -14
  89. data/lib/aspera/rest_errors_aspera.rb +39 -34
  90. data/lib/aspera/secret_hider.rb +18 -17
  91. data/lib/aspera/ssh.rb +10 -5
  92. data/lib/aspera/temp_file_manager.rb +11 -4
  93. data/lib/aspera/web_auth.rb +10 -7
  94. data/lib/aspera/web_server_simple.rb +11 -5
  95. data.tar.gz.sig +0 -0
  96. metadata +108 -39
  97. metadata.gz.sig +0 -0
  98. data/lib/aspera/cli/listener/line_dump.rb +0 -19
  99. data/lib/aspera/cli/listener/logger.rb +0 -22
  100. data/lib/aspera/cli/listener/progress.rb +0 -50
  101. data/lib/aspera/cli/listener/progress_multi.rb +0 -84
  102. data/lib/aspera/cli/plugins/sync.rb +0 -44
  103. data/lib/aspera/fasp/listener.rb +0 -13
  104. data/lib/aspera/sync.rb +0 -213
@@ -1,25 +1,29 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # cspell:ignore initdemo genkey pubkey asperasoft
3
4
  require 'aspera/cli/basic_auth_plugin'
4
5
  require 'aspera/cli/extended_value'
5
6
  require 'aspera/cli/version'
6
7
  require 'aspera/cli/formatter'
7
8
  require 'aspera/cli/info'
9
+ require 'aspera/cli/transfer_progress'
8
10
  require 'aspera/fasp/installation'
11
+ require 'aspera/fasp/products'
9
12
  require 'aspera/fasp/parameters'
10
13
  require 'aspera/fasp/transfer_spec'
11
14
  require 'aspera/fasp/error_info'
15
+ require 'aspera/keychain/encrypted_hash'
16
+ require 'aspera/keychain/macos_security'
12
17
  require 'aspera/proxy_auto_config'
13
18
  require 'aspera/open_application'
14
19
  require 'aspera/persistency_action_once'
15
20
  require 'aspera/id_generator'
16
- require 'aspera/keychain/encrypted_hash'
17
- require 'aspera/keychain/macos_security'
18
- require 'aspera/aoc'
21
+ require 'aspera/persistency_folder'
22
+ require 'aspera/data_repository'
23
+ require 'aspera/line_logger'
19
24
  require 'aspera/rest'
20
- require 'xmlsimple'
21
- require 'base64'
22
- require 'net/smtp'
25
+ require 'aspera/log'
26
+ require 'aspera/assert'
23
27
  require 'open3'
24
28
  require 'date'
25
29
  require 'erb'
@@ -33,28 +37,28 @@ module Aspera
33
37
  ASPERA_HOME_FOLDER_NAME = '.aspera'
34
38
  # default config file
35
39
  DEFAULT_CONFIG_FILENAME = 'config.yaml'
40
+ DEFAULT_VAULT_FILENAME = 'vault.bin'
36
41
  # reserved preset names
37
42
  CONF_PRESET_CONFIG = 'config'
38
43
  CONF_PRESET_VERSION = 'version'
39
- CONF_PRESET_DEFAULT = 'default'
44
+ CONF_PRESET_DEFAULTS = 'default'
40
45
  CONF_PRESET_GLOBAL = 'global_common_defaults'
46
+ # special name to identify value of default
47
+ GLOBAL_DEFAULT_KEYWORD = 'GLOBAL'
41
48
  CONF_PLUGIN_SYM = :config # Plugins::Config.name.split('::').last.downcase.to_sym
42
49
  CONF_GLOBAL_SYM = :config
43
- # default redirect for AoC web auth
44
- DEFAULT_REDIRECT = 'http://localhost:12345'
45
50
  # folder containing custom plugins in user's config folder
46
51
  ASPERA_PLUGINS_FOLDERNAME = 'plugins'
52
+ PERSISTENCY_FOLDER = 'persist_store'
47
53
  RUBY_FILE_EXT = '.rb'
48
54
  ASPERA = 'aspera'
49
- AOC_COMMAND = 'aoc'
50
55
  SERVER_COMMAND = 'server'
51
56
  APP_NAME_SDK = 'sdk'
52
57
  CONNECT_WEB_URL = 'https://d3gcli72yxqn2z.cloudfront.net/connect'
53
- CONNECT_VERSIONS = 'connectversions.js'
58
+ CONNECT_VERSIONS = 'connectversions.js' # cspell: disable-line
54
59
  TRANSFER_SDK_ARCHIVE_URL = 'https://ibm.biz/aspera_transfer_sdk'
55
60
  DEMO = 'demo'
56
- DEMO_SERVER_PRESET = 'demoserver'
57
- AOC_PATH_API_CLIENTS = 'admin/api-clients'
61
+ DEMO_SERVER_PRESET = 'demoserver' # cspell: disable-line
58
62
  EMAIL_TEST_TEMPLATE = <<~END_OF_TEMPLATE
59
63
  From: <%=from_name%> <<%=from_email%>>
60
64
  To: <<%=to%>>
@@ -63,103 +67,200 @@ module Aspera
63
67
  This email was sent to test #{PROGRAM_NAME}.
64
68
  END_OF_TEMPLATE
65
69
  # special extended values
66
- EXTV_INCLUDE_PRESETS = :incps
67
- EXTV_PRESET = :preset
68
- EXTV_VAULT = :vault
70
+ EXTEND_PRESET = :preset
71
+ EXTEND_VAULT = :vault
69
72
  PRESET_DIG_SEPARATOR = '.'
70
73
  DEFAULT_CHECK_NEW_VERSION_DAYS = 7
71
- DEFAULT_PRIV_KEY_FILENAME = 'aspera_aoc_key' # pragma: allowlist secret
72
- DEFAULT_PRIVKEY_LENGTH = 4096
74
+ DEFAULT_PRIV_KEY_FILENAME = 'my_private_key.pem' # pragma: allowlist secret
75
+ DEFAULT_PRIV_KEY_LENGTH = 4096
73
76
  COFFEE_IMAGE = 'https://enjoyjava.com/wp-content/uploads/2018/01/How-to-make-strong-coffee.jpg'
77
+ WIZARD_RESULT_KEYS = %i[preset_value test_args].freeze
78
+ GEM_CHECK_DATE_FMT = '%Y/%m/%d'
79
+ # for testing only
80
+ SELF_SIGNED_CERT = OpenSSL::SSL.const_get(:enon_yfirev.to_s.upcase.reverse) # cspell: disable-line
74
81
  private_constant :DEFAULT_CONFIG_FILENAME,
75
82
  :CONF_PRESET_CONFIG,
76
83
  :CONF_PRESET_VERSION,
77
- :CONF_PRESET_DEFAULT,
84
+ :CONF_PRESET_DEFAULTS,
78
85
  :CONF_PRESET_GLOBAL,
79
- :DEFAULT_REDIRECT,
80
86
  :ASPERA_PLUGINS_FOLDERNAME,
81
87
  :RUBY_FILE_EXT,
82
88
  :ASPERA,
83
- :AOC_COMMAND,
84
89
  :DEMO,
85
90
  :TRANSFER_SDK_ARCHIVE_URL,
86
- :AOC_PATH_API_CLIENTS,
87
91
  :DEMO_SERVER_PRESET,
88
92
  :EMAIL_TEST_TEMPLATE,
89
- :EXTV_INCLUDE_PRESETS,
90
- :EXTV_PRESET,
91
- :EXTV_VAULT,
93
+ :EXTEND_PRESET,
94
+ :EXTEND_VAULT,
92
95
  :DEFAULT_CHECK_NEW_VERSION_DAYS,
93
96
  :DEFAULT_PRIV_KEY_FILENAME,
94
97
  :SERVER_COMMAND,
95
98
  :PRESET_DIG_SEPARATOR,
96
- :COFFEE_IMAGE
99
+ :COFFEE_IMAGE,
100
+ :WIZARD_RESULT_KEYS,
101
+ :SELF_SIGNED_CERT,
102
+ :PERSISTENCY_FOLDER,
103
+ :DEFAULT_PRIV_KEY_LENGTH
104
+
105
+ class << self
106
+ def generate_rsa_private_key(path:, length: DEFAULT_PRIV_KEY_LENGTH)
107
+ require 'openssl'
108
+ priv_key = OpenSSL::PKey::RSA.new(length)
109
+ File.write(path, priv_key.to_s)
110
+ File.write("#{path}.pub", priv_key.public_key.to_s)
111
+ Environment.restrict_file_access(path)
112
+ Environment.restrict_file_access("#{path}.pub")
113
+ nil
114
+ end
115
+
116
+ # folder containing plugins in the gem's main folder
117
+ def gem_plugins_folder
118
+ File.dirname(File.expand_path(__FILE__))
119
+ end
120
+
121
+ # name of englobing module
122
+ # @return "Aspera::Cli::Plugins"
123
+ def module_full_name
124
+ return Module.nesting[2].to_s
125
+ end
126
+
127
+ # @return main folder where code is, i.e. .../lib
128
+ # go up as many times as englobing modules (not counting class, as it is a file)
129
+ def gem_src_root
130
+ File.expand_path(module_full_name.gsub('::', '/').gsub(%r{[^/]+}, '..'), gem_plugins_folder)
131
+ end
132
+
133
+ # instantiate a plugin
134
+ # plugins must be Capitalized
135
+ def plugin_class(plugin_name_sym)
136
+ # Module.nesting[2] is Aspera::Cli::Plugins
137
+ return Object.const_get("#{module_full_name}::#{plugin_name_sym.to_s.capitalize}")
138
+ end
139
+
140
+ # deep clone hash so that it does not get modified in case of display and secret hide
141
+ def protect_presets(val)
142
+ return JSON.parse(JSON.generate(val))
143
+ end
144
+
145
+ # return product family folder (~/.aspera)
146
+ def module_family_folder
147
+ user_home_folder = Dir.home
148
+ assert(Dir.exist?(user_home_folder), exception_class: Cli::Error){"Home folder does not exist: #{user_home_folder}. Check your user environment."}
149
+ return File.join(user_home_folder, ASPERA_HOME_FOLDER_NAME)
150
+ end
151
+
152
+ # return product config folder (~/.aspera/<name>)
153
+ def default_app_main_folder(app_name:)
154
+ assert_type(app_name, String)
155
+ assert(!app_name.empty?)
156
+ return File.join(module_family_folder, app_name)
157
+ end
158
+ end # self
159
+
97
160
  def initialize(env, params)
98
- raise 'env and params must be Hash' unless env.is_a?(Hash) && params.is_a?(Hash)
99
- raise 'missing param' unless %i[name help version gem].sort.eql?(params.keys.sort)
161
+ assert_type(env, Hash)
162
+ assert_type(params, Hash)
163
+ assert(%i[gem help name version].eql?(params.keys.sort)){'missing param'}
164
+ # we need to defer parsing of options until we have the config file, so we can use @extend with @preset
100
165
  super(env)
101
166
  @info = params
102
- @main_folder = default_app_main_folder
103
167
  @plugins = {}
104
168
  @plugin_lookup_folders = []
105
169
  @use_plugin_defaults = true
106
170
  @config_presets = nil
171
+ @config_checksum_on_disk = nil
107
172
  @connect_versions = nil
108
173
  @vault = nil
109
- @conf_file_default = File.join(@main_folder, DEFAULT_CONFIG_FILENAME)
110
- @option_config_file = @conf_file_default
111
174
  @pac_exec = nil
112
175
  @sdk_default_location = false
176
+ @option_insecure = false
177
+ @option_warn_insecure_cert = true
178
+ @option_ignore_cert_host_port = []
179
+ @option_http_options = {}
180
+ @ssl_warned_urls = []
181
+ @option_cache_tokens = true
182
+ @proxy_credentials = nil
183
+ @main_folder = nil
184
+ @option_config_file = nil
185
+ @certificate_store = nil
186
+ @certificate_paths = nil
187
+ @progress_bar = nil
188
+ # option to set main folder
189
+ options.declare(
190
+ :home, 'Home folder for tool',
191
+ handler: {o: self, m: :main_folder},
192
+ types: String,
193
+ default: self.class.default_app_main_folder(app_name: @info[:name]))
194
+ options.parse_options!
113
195
  Log.log.debug{"#{@info[:name]} folder: #{@main_folder}"}
114
- # set folder for FASP SDK
196
+ # data persistency manager
197
+ env[:persistency] = PersistencyFolder.new(File.join(@main_folder, PERSISTENCY_FOLDER))
198
+ # set folders for plugin lookup
115
199
  add_plugin_lookup_folder(self.class.gem_plugins_folder)
116
200
  add_plugin_lookup_folder(File.join(@main_folder, ASPERA_PLUGINS_FOLDERNAME))
117
- # do file parameter first
118
- options.declare(:config_file, "Read parameters from file in YAML format, current=#{@option_config_file}", handler: {o: self, m: :option_config_file})
201
+ # option to set config file
202
+ options.declare(
203
+ :config_file, 'Path to YAML file with preset configuration',
204
+ handler: {o: self, m: :option_config_file},
205
+ default: File.join(@main_folder, DEFAULT_CONFIG_FILENAME))
119
206
  options.parse_options!
120
- # read correct file (set @config_presets)
207
+ # read config file (set @config_presets)
121
208
  read_config_file
122
209
  # add preset handler (needed for smtp)
123
- ExtendedValue.instance.set_handler(EXTV_PRESET, lambda{|v|preset_by_name(v)})
124
- ExtendedValue.instance.set_handler(EXTV_INCLUDE_PRESETS, lambda{|v|expanded_with_preset_includes(v)})
125
- ExtendedValue.instance.set_handler(EXTV_VAULT, lambda{|v|vault_value(v)})
210
+ ExtendedValue.instance.set_handler(EXTEND_PRESET, lambda{|v|preset_by_name(v)})
211
+ ExtendedValue.instance.set_handler(EXTEND_VAULT, lambda{|v|vault_value(v)})
126
212
  # load defaults before it can be overridden
127
213
  add_plugin_default_preset(CONF_GLOBAL_SYM)
214
+ # vault options
215
+ options.declare(:secret, 'Secret for access keys')
216
+ options.declare(:vault, 'Vault for secrets', types: Hash)
217
+ options.declare(:vault_password, 'Vault password')
128
218
  options.parse_options!
219
+ # declare generic plugin options only after handlers are declared
220
+ Plugin.declare_generic_options(options)
221
+ # configuration options
129
222
  options.declare(:no_default, 'Do not load default configuration for plugin', values: :none, short: 'N') { @use_plugin_defaults = false }
223
+ options.declare(:preset, 'Load the named option preset from current config file', short: 'P', handler: {o: self, m: :option_preset})
224
+ options.declare(:version_check_days, 'Period in days to check new version (zero to disable)', coerce: Integer, default: DEFAULT_CHECK_NEW_VERSION_DAYS)
225
+ options.declare(:plugin_folder, 'Folder where to find additional plugins', handler: {o: self, m: :option_plugin_folder})
226
+ # wizard options
130
227
  options.declare(:override, 'Wizard: override existing value', values: :bool, default: :no)
131
- options.declare(:use_generic_client, 'Wizard: AoC: use global or org specific jwt client id', values: :bool, default: true)
132
228
  options.declare(:default, 'Wizard: set as default configuration for specified plugin (also: update)', values: :bool, default: true)
133
229
  options.declare(:test_mode, 'Wizard: skip private key check step', values: :bool, default: false)
134
- options.declare(:preset, 'Load the named option preset from current config file', short: 'P', handler: {o: self, m: :option_preset})
135
- options.declare(:pkeypath, 'Wizard: path to private key for JWT')
230
+ options.declare(:key_path, 'Wizard: path to private key for JWT')
231
+ # Transfer SDK options
136
232
  options.declare(:ascp_path, 'Path to ascp', handler: {o: Fasp::Installation.instance, m: :ascp_path})
137
233
  options.declare(:use_product, 'Use ascp from specified product', handler: {o: self, m: :option_use_product})
138
- options.declare(:smtp, 'SMTP configuration', types: Hash)
139
- options.declare(:fpac, 'Proxy auto configuration script')
140
- options.declare(:proxy_credentials, 'HTTP proxy credentials (Array with user and password)')
141
- options.declare(:secret, 'Secret for access keys')
142
- options.declare(:vault, 'Vault for secrets')
143
- options.declare(:vault_password, 'Vault password')
144
234
  options.declare(:sdk_url, 'URL to get SDK', default: TRANSFER_SDK_ARCHIVE_URL)
145
235
  options.declare(:sdk_folder, 'SDK folder path', handler: {o: Fasp::Installation.instance, m: :sdk_folder})
146
- options.declare(:notif_to, 'Email recipient for notification of transfers')
147
- options.declare(:notif_template, 'Email ERB template for notification of transfers')
148
- options.declare(:version_check_days, 'Period in days to check new version (zero to disable)', coerce: Integer, default: DEFAULT_CHECK_NEW_VERSION_DAYS)
149
- options.declare(:plugin_folder, 'Folder where to find additional plugins', handler: {o: self, m: :option_plugin_folder})
236
+ options.declare(:progress_bar, 'Display progress bar', values: :bool, default: Environment.terminal?)
237
+ # email options
238
+ options.declare(:smtp, 'SMTP configuration', types: Hash)
239
+ options.declare(:notify_to, 'Email recipient for notification of transfers')
240
+ options.declare(:notify_template, 'Email ERB template for notification of transfers')
241
+ # HTTP options
242
+ options.declare(:insecure, 'Do not validate any HTTPS certificate', values: :bool, handler: {o: self, m: :option_insecure}, default: :no)
243
+ options.declare(:ignore_certificate, 'Do not validate HTTPS certificate for these URLs', types: Array, handler: {o: self, m: :option_ignore_cert_host_port})
244
+ options.declare(:silent_insecure, 'Issue a warning if certificate is ignored', values: :bool, handler: {o: self, m: :option_warn_insecure_cert}, default: :yes)
245
+ options.declare(:cert_stores, 'List of folder with trusted certificates', types: [Array, String], handler: {o: self, m: :trusted_cert_locations})
246
+ options.declare(:http_options, 'Options for HTTP/S socket', types: Hash, handler: {o: self, m: :option_http_options}, default: {})
247
+ options.declare(:cache_tokens, 'Save and reuse Oauth tokens', values: :bool, handler: {o: self, m: :option_cache_tokens})
248
+ options.declare(:fpac, 'Proxy auto configuration script')
249
+ options.declare(:proxy_credentials, 'HTTP proxy credentials (user and password)', types: Array)
150
250
  options.parse_options!
251
+ @progress_bar = TransferProgress.new if options.get_option(:progress_bar)
151
252
  # Check SDK folder is set or not, for compatibility, we check in two places
152
253
  sdk_folder = Fasp::Installation.instance.sdk_folder rescue nil
153
254
  if sdk_folder.nil?
154
255
  @sdk_default_location = true
155
256
  Log.log.debug('SDK folder is not set, checking default')
156
257
  # new location
157
- sdk_folder = default_app_main_folder(app_name: APP_NAME_SDK)
258
+ sdk_folder = self.class.default_app_main_folder(app_name: APP_NAME_SDK)
158
259
  Log.log.debug{"checking: #{sdk_folder}"}
159
260
  if !Dir.exist?(sdk_folder)
160
261
  Log.log.debug{"not exists: #{sdk_folder}"}
161
262
  # former location
162
- former_sdk_folder = File.join(default_app_main_folder, APP_NAME_SDK)
263
+ former_sdk_folder = File.join(self.class.default_app_main_folder(app_name: @info[:name]), APP_NAME_SDK)
163
264
  Log.log.debug{"checking: #{former_sdk_folder}"}
164
265
  sdk_folder = former_sdk_folder if Dir.exist?(former_sdk_folder)
165
266
  end
@@ -169,36 +270,115 @@ module Aspera
169
270
  pac_script = options.get_option(:fpac)
170
271
  # create PAC executor
171
272
  @pac_exec = Aspera::ProxyAutoConfig.new(pac_script).register_uri_generic unless pac_script.nil?
172
- proxy_creds = options.get_option(:proxy_credentials)
173
- if !proxy_creds.nil?
174
- raise CliBadArgument, 'proxy credentials shall be an array (#{proxy_creds.class})' unless proxy_creds.is_a?(Array)
175
- raise CliBadArgument, 'proxy credentials shall have two elements (#{proxy_creds.length})' unless proxy_creds.length.eql?(2)
176
- @pac_exec.proxy_user = Rest.proxy_user = proxy_creds[0]
177
- @pac_exec.proxy_pass = Rest.proxy_pass = proxy_creds[1]
273
+ proxy_user_pass = options.get_option(:proxy_credentials)
274
+ if !proxy_user_pass.nil?
275
+ assert(proxy_user_pass.length.eql?(2), exception_class: Cli::BadArgument){"proxy_credentials shall have two elements (#{proxy_user_pass.length})"}
276
+ @proxy_credentials = {user: proxy_user_pass[0], pass: proxy_user_pass[1]}
277
+ @pac_exec.proxy_user = @proxy_credentials[:user]
278
+ @pac_exec.proxy_pass = @proxy_credentials[:pass]
279
+ end
280
+ Rest.set_parameters(
281
+ user_agent: PROGRAM_NAME,
282
+ session_cb: lambda{|http_session|update_http_session(http_session)},
283
+ progress_bar: @progress_bar)
284
+ Oauth.persist_mgr = persistency if @option_cache_tokens
285
+ Fasp::Parameters.file_list_folder = File.join(@main_folder, 'filelists') # cspell: disable-line
286
+ Aspera::RestErrorAnalyzer.instance.log_file = File.join(@main_folder, 'rest_exceptions.log')
287
+ # register aspera REST call error handlers
288
+ Aspera::RestErrorsAspera.register_handlers
289
+ end
290
+
291
+ attr_accessor :main_folder, :option_cache_tokens, :option_insecure, :option_warn_insecure_cert, :option_http_options
292
+ attr_reader :option_ignore_cert_host_port, :progress_bar
293
+
294
+ def trusted_cert_locations=(path_list)
295
+ path_list = [path_list] unless path_list.is_a?(Array)
296
+ if @certificate_store.nil?
297
+ Log.log.debug('Creating SSL Cert store')
298
+ @certificate_store = OpenSSL::X509::Store.new
299
+ @certificate_store.set_default_paths
300
+ @certificate_paths = []
301
+ end
302
+
303
+ path_list.each do |path|
304
+ assert_type(path, String){'Expecting a String for cert location'}
305
+ Log.log.debug("Adding cert location: #{path}")
306
+ if path.eql?(ExtendedValue::DEF)
307
+ path = OpenSSL::X509::DEFAULT_CERT_DIR
308
+ @certificate_store.add_path(path)
309
+ @certificate_paths.push(path)
310
+ path = OpenSSL::X509::DEFAULT_CERT_FILE
311
+ @certificate_store.add_file(path)
312
+ elsif File.file?(path)
313
+ @certificate_store.add_file(path)
314
+ elsif File.directory?(path)
315
+ @certificate_store.add_path(path)
316
+ else
317
+ raise "No such file or folder: #{path}"
318
+ end
319
+ @certificate_paths.push(path)
178
320
  end
179
321
  end
180
322
 
181
- # env var name to override the app's main folder
182
- # default main folder is $HOME/<vendor main app folder>/<program name>
183
- def conf_dir_env_var
184
- return "#{@info[:name]}_home".upcase
323
+ def trusted_cert_locations(files_only: false)
324
+ locations = if @certificate_paths.nil?
325
+ [OpenSSL::X509::DEFAULT_CERT_DIR, OpenSSL::X509::DEFAULT_CERT_FILE]
326
+ else
327
+ @certificate_paths
328
+ end
329
+ locations = locations.select{|f|File.file?(f)} if files_only
330
+ return locations
185
331
  end
186
332
 
187
- # return product family folder (~/.aspera)
188
- def module_family_folder
189
- user_home_folder = Dir.home
190
- raise CliError, "Home folder does not exist: #{user_home_folder}. Check your user environment." unless Dir.exist?(user_home_folder)
191
- return File.join(user_home_folder, ASPERA_HOME_FOLDER_NAME)
333
+ def option_ignore_cert_host_port=(url_list)
334
+ url_list.each do |url|
335
+ uri = URI.parse(url)
336
+ raise "Expecting https scheme: #{url}" unless uri.scheme.eql?('https')
337
+ @option_ignore_cert_host_port.push([uri.host, uri.port].freeze)
338
+ end
192
339
  end
193
340
 
194
- # return product config folder (~/.aspera/<name>)
195
- def default_app_main_folder(app_name: nil)
196
- app_name = @info[:name] if app_name.nil?
197
- # find out application main folder
198
- app_folder = ENV[conf_dir_env_var]
199
- # if env var undefined or empty
200
- app_folder = File.join(module_family_folder, app_name) if app_folder.nil? || app_folder.empty?
201
- return app_folder
341
+ def ignore_cert?(address, port)
342
+ endpoint = [address, port].freeze
343
+ ignore_cert = false
344
+ if @option_insecure || @option_ignore_cert_host_port.any?(endpoint)
345
+ ignore_cert = true
346
+ if @option_warn_insecure_cert
347
+ base_url = "https://#{address}:#{port}"
348
+ if !@ssl_warned_urls.include?(base_url)
349
+ formatter.display_message(
350
+ :error,
351
+ "#{Formatter::WARNING_FLASH} Ignoring certificate for: #{base_url}. Do not deactivate certificate verification in production.")
352
+ @ssl_warned_urls.push(base_url)
353
+ end
354
+ end
355
+ end
356
+ Log.log.debug{"ignore cert? #{endpoint} -> #{ignore_cert}"}
357
+ return ignore_cert
358
+ end
359
+
360
+ # called every time a new REST HTTP session is opened to set user-provided options
361
+ # @param http_session [Net::HTTP] the newly created HTTP/S session object
362
+ def update_http_session(http_session)
363
+ http_session.set_debug_output(LineLogger.new(:trace2)) if Log.instance.logger.trace2?
364
+ # Rest.io_http_session(http_session).debug_output = Log.log
365
+ http_session.verify_mode = SELF_SIGNED_CERT if http_session.use_ssl? && ignore_cert?(http_session.address, http_session.port)
366
+ http_session.cert_store = @certificate_store if @certificate_store
367
+ Log.log.debug{"using cert store #{http_session.cert_store} (#{@certificate_store})"} unless http_session.cert_store.nil?
368
+ if @proxy_credentials
369
+ http_session.proxy_user = @proxy_credentials[:user]
370
+ http_session.proxy_pass = @proxy_credentials[:pass]
371
+ end
372
+ @option_http_options.each do |k, v|
373
+ method = "#{k}=".to_sym
374
+ # check if accessor is a method of Net::HTTP
375
+ # continue_timeout= read_timeout= write_timeout=
376
+ if http_session.respond_to?(method)
377
+ http_session.send(method, v)
378
+ else
379
+ Log.log.error{"no such HTTP session attribute: #{k}"}
380
+ end
381
+ end
202
382
  end
203
383
 
204
384
  def check_gem_version
@@ -211,7 +391,7 @@ module Aspera
211
391
  end
212
392
  if Gem::Version.new(Environment.ruby_version) < Gem::Version.new(RUBY_FUTURE_MINIMUM_VERSION)
213
393
  Log.log.warn do
214
- "Note that a future version will require Ruby version #{RUBY_FUTURE_MINIMUM_VERSION} at minimum, "\
394
+ "Note that a future version will require Ruby version #{RUBY_FUTURE_MINIMUM_VERSION} at minimum, " \
215
395
  "you are using #{Environment.ruby_version}"
216
396
  end
217
397
  end
@@ -226,7 +406,6 @@ module Aspera
226
406
  def periodic_check_newer_gem_version
227
407
  # get verification period
228
408
  delay_days = options.get_option(:version_check_days, mandatory: true)
229
- Log.log.info{"check days: #{delay_days}"}
230
409
  # check only if not zero day
231
410
  return if delay_days.eql?(0)
232
411
  # get last date from persistency
@@ -237,17 +416,11 @@ module Aspera
237
416
  id: 'version_last_check')
238
417
  # get persisted date or nil
239
418
  current_date = Date.today
240
- last_check_days =
241
- begin
242
- current_date - Date.strptime(last_check_array.first, '%Y/%m/%d')
243
- rescue StandardError
244
- # negative value will force check
245
- -1
246
- end
247
- Log.log.debug{"days elapsed: #{last_check_days}"}
248
- return if last_check_days < delay_days
419
+ last_check_days = (current_date - Date.strptime(last_check_array.first, GEM_CHECK_DATE_FMT)) rescue nil
420
+ Log.log.debug{"gem check new version: #{delay_days}, #{last_check_days}, #{current_date}, #{last_check_array}"}
421
+ return if !last_check_days.nil? && last_check_days < delay_days
249
422
  # generate timestamp
250
- last_check_array[0] = current_date.strftime('%Y/%m/%d')
423
+ last_check_array[0] = current_date.strftime(GEM_CHECK_DATE_FMT)
251
424
  check_date_persist.save
252
425
  # compare this version and the one on internet
253
426
  check_data = check_gem_version
@@ -266,7 +439,7 @@ module Aspera
266
439
  Log.log.debug{"javascript=[\n#{connect_versions_javascript}\n]"}
267
440
  # get javascript object only
268
441
  found = connect_versions_javascript.match(/^.*? = (.*);/)
269
- raise CliError, 'Problem when getting connect versions from internet' if found.nil?
442
+ raise Cli::Error, 'Problem when getting connect versions from internet' if found.nil?
270
443
  all_data = JSON.parse(found[1])
271
444
  @connect_versions = all_data['entries']
272
445
  end
@@ -284,66 +457,45 @@ module Aspera
284
457
  return nil
285
458
  end
286
459
 
287
- private
288
-
289
- class << self
290
- def generate_rsa_private_key(path:, length: DEFAULT_PRIVKEY_LENGTH)
291
- require 'openssl'
292
- priv_key = OpenSSL::PKey::RSA.new(length)
293
- File.write(path, priv_key.to_s)
294
- File.write(path + '.pub', priv_key.public_key.to_s)
295
- Environment.restrict_file_access(path)
296
- Environment.restrict_file_access(path + '.pub')
297
- nil
298
- end
299
-
300
- # folder containing plugins in the gem's main folder
301
- def gem_plugins_folder
302
- File.dirname(File.expand_path(__FILE__))
303
- end
304
-
305
- # name of englobing module
306
- # @return "Aspera::Cli::Plugins"
307
- def module_full_name
308
- return Module.nesting[2].to_s
460
+ # get the default global preset, or set default one
461
+ def global_default_preset
462
+ result = get_plugin_default_config_name(CONF_GLOBAL_SYM)
463
+ if result.nil?
464
+ result = CONF_PRESET_GLOBAL
465
+ set_preset_key(CONF_PRESET_DEFAULTS, CONF_GLOBAL_SYM, result)
309
466
  end
467
+ return result
468
+ end
310
469
 
311
- # @return main folder where code is, i.e. .../lib
312
- # go up as many times as englobing modules (not counting class, as it is a file)
313
- def gem_src_root
314
- File.expand_path(module_full_name.gsub('::', '/').gsub(%r{[^/]+}, '..'), gem_plugins_folder)
470
+ def set_preset_key(preset, param_name, param_value)
471
+ assert_values(param_name.class, [String, Symbol]){'parameter'}
472
+ param_name = param_name.to_s
473
+ selected_preset = @config_presets[preset]
474
+ if selected_preset.nil?
475
+ Log.log.debug{"No such preset name: #{preset}, initializing"}
476
+ selected_preset = @config_presets[preset] = {}
315
477
  end
316
-
317
- # instantiate a plugin
318
- # plugins must be Capitalized
319
- def plugin_class(plugin_name_sym)
320
- # Module.nesting[2] is Aspera::Cli::Plugins
321
- return Object.const_get("#{module_full_name}::#{plugin_name_sym.to_s.capitalize}")
478
+ assert_type(selected_preset, Hash){"#{preset}.#{param_name}"}
479
+ if selected_preset.key?(param_name)
480
+ if selected_preset[param_name].eql?(param_value)
481
+ Log.log.warn{"keeping same value for #{preset}: #{param_name}: #{param_value}"}
482
+ return
483
+ end
484
+ Log.log.warn{"overwriting value: #{selected_preset[param_name]}"}
322
485
  end
486
+ selected_preset[param_name] = param_value
487
+ formatter.display_status("Updated: #{preset}: #{param_name} <- #{param_value}")
488
+ nil
323
489
  end
324
490
 
325
491
  # set parameter and value in global config
326
492
  # creates one if none already created
327
493
  # @return preset name that contains global default
328
494
  def set_global_default(key, value)
329
- # get default preset if it exists
330
- global_default_preset_name = get_plugin_default_config_name(CONF_GLOBAL_SYM)
331
- if global_default_preset_name.nil?
332
- global_default_preset_name = CONF_PRESET_GLOBAL
333
- @config_presets[CONF_PRESET_DEFAULT] ||= {}
334
- @config_presets[CONF_PRESET_DEFAULT][CONF_GLOBAL_SYM.to_s] = global_default_preset_name
335
- end
336
- @config_presets[global_default_preset_name] ||= {}
337
- @config_presets[global_default_preset_name][key.to_s] = value
338
- formatter.display_status("Updated: #{global_default_preset_name}: #{key} <- #{value}")
339
- save_presets_to_config_file
340
- return global_default_preset_name
495
+ set_preset_key(global_default_preset, key, value)
341
496
  end
342
497
 
343
- public
344
-
345
498
  # $HOME/.aspera/`program_name`
346
- attr_reader :main_folder
347
499
  attr_reader :gem_url, :plugins
348
500
  attr_accessor :option_config_file
349
501
 
@@ -351,40 +503,17 @@ module Aspera
351
503
  # @param config_name name of the preset in config file
352
504
  # @param include_path used to detect and avoid include loops
353
505
  def preset_by_name(config_name, include_path=[])
354
- raise CliError, 'loop in include' if include_path.include?(config_name)
506
+ raise Cli::Error, 'loop in include' if include_path.include?(config_name)
355
507
  include_path = include_path.clone # avoid messing up if there are multiple branches
356
508
  current = @config_presets
357
509
  config_name.split(PRESET_DIG_SEPARATOR).each do |name|
358
- raise CliError, "Expecting Hash for sub key: #{include_path} (#{current.class})" unless current.is_a?(Hash)
510
+ assert_type(current, Hash, exception_class: Cli::Error){"sub key: #{include_path}"}
359
511
  include_path.push(name)
360
512
  current = current[name]
361
- raise CliError, "No such config preset: #{include_path}" if current.nil?
362
- end
363
- case current
364
- when Hash then return expanded_with_preset_includes(current, include_path)
365
- when String then return ExtendedValue.instance.evaluate(current)
366
- else return current
513
+ raise Cli::Error, "No such config preset: #{include_path}" if current.nil?
367
514
  end
368
- end
369
-
370
- # @return the hash value with 'incps' keys expanded to include other presets
371
- # @param hash_val
372
- # @param include_path to avoid inclusion loop
373
- def expanded_with_preset_includes(hash_val, include_path=[])
374
- raise CliError, "#{EXTV_INCLUDE_PRESETS} requires a Hash, have #{hash_val.class}" unless hash_val.is_a?(Hash)
375
- if hash_val.key?(EXTV_INCLUDE_PRESETS)
376
- memory = hash_val.clone
377
- includes = memory[EXTV_INCLUDE_PRESETS]
378
- memory.delete(EXTV_INCLUDE_PRESETS)
379
- hash_val = {}
380
- raise "#{EXTV_INCLUDE_PRESETS} must be an Array" unless includes.is_a?(Array)
381
- raise "#{EXTV_INCLUDE_PRESETS} must contain names" unless includes.map(&:class).uniq.eql?([String])
382
- includes.each do |preset_name|
383
- hash_val.merge!(preset_by_name(preset_name, include_path))
384
- end
385
- hash_val.merge!(memory)
386
- end
387
- return hash_val
515
+ current = self.class.protect_presets(current) unless current.is_a?(String)
516
+ return ExtendedValue.instance.evaluate(current)
388
517
  end
389
518
 
390
519
  def option_use_product=(value)
@@ -396,11 +525,10 @@ module Aspera
396
525
  end
397
526
 
398
527
  def option_plugin_folder=(value)
399
- case value
400
- when String then add_plugin_lookup_folder(value)
401
- when Array then value.each{|f|add_plugin_lookup_folder(f)}
402
- else raise "folder shall be Array or String, not #{value.class}"
403
- end
528
+ assert_values(value.class, [String, Array]){'plugin folder'}
529
+ value = [value] if value.is_a?(String)
530
+ assert(value.all?(String)){'plugin folder'}
531
+ value.each{|f|add_plugin_lookup_folder(f)}
404
532
  end
405
533
 
406
534
  def option_plugin_folder
@@ -420,6 +548,10 @@ module Aspera
420
548
  end
421
549
  end
422
550
 
551
+ def config_checksum
552
+ JSON.generate(@config_presets).hash
553
+ end
554
+
423
555
  # read config file and validate format
424
556
  def read_config_file
425
557
  Log.log.debug{"config file is: #{@option_config_file}".red}
@@ -427,38 +559,35 @@ module Aspera
427
559
  search_files = [@option_config_file]
428
560
  # find first existing file (or nil)
429
561
  conf_file_to_load = search_files.find{|f| File.exist?(f)}
430
- # require save if old version of file
431
- save_required = false
432
562
  # if no file found, create default config
433
563
  if conf_file_to_load.nil?
434
- Log.log.warn{"No config file found. Creating empty configuration file: #{@option_config_file}"}
435
- @config_presets = {CONF_PRESET_CONFIG => {CONF_PRESET_VERSION => @info[:version]}}
564
+ Log.log.warn{"No config file found. New configuration file: #{@option_config_file}"}
565
+ @config_presets = {CONF_PRESET_CONFIG => {CONF_PRESET_VERSION => 'new file'}}
566
+ # @config_checksum_on_disk is nil
436
567
  else
437
568
  Log.log.debug{"loading #{@option_config_file}"}
438
569
  @config_presets = YAML.load_file(conf_file_to_load)
570
+ @config_checksum_on_disk = config_checksum
439
571
  end
440
572
  files_to_copy = []
441
- Log.log.debug{"Available_presets: #{@config_presets}"}
442
- raise 'Expecting YAML Hash' unless @config_presets.is_a?(Hash)
573
+ Log.log.debug{Log.dump('Available_presets', @config_presets)}
574
+ assert_type(@config_presets, Hash){'config file YAML'}
443
575
  # check there is at least the config section
444
- if !@config_presets.key?(CONF_PRESET_CONFIG)
445
- raise "Cannot find key: #{CONF_PRESET_CONFIG}"
446
- end
576
+ assert(@config_presets.key?(CONF_PRESET_CONFIG)){"Cannot find key: #{CONF_PRESET_CONFIG}"}
447
577
  version = @config_presets[CONF_PRESET_CONFIG][CONF_PRESET_VERSION]
448
- if version.nil?
449
- raise 'No version found in config section.'
450
- end
578
+ raise 'No version found in config section.' if version.nil?
451
579
  Log.log.debug{"conf version: #{version}"}
452
- # if there are any conversion needed, those happen here.
453
- # Place new compatibility code here
454
- if save_required
455
- Log.log.warn('Saving automatic conversion.')
456
- @config_presets[CONF_PRESET_CONFIG][CONF_PRESET_VERSION] = @info[:version]
457
- save_presets_to_config_file
580
+ # VVV if there are any conversion needed, those happen here.
581
+ # fix bug in 4.4 (creating key "true" in "default" preset)
582
+ @config_presets[CONF_PRESET_DEFAULTS].delete(true) if @config_presets[CONF_PRESET_DEFAULTS].is_a?(Hash)
583
+ # ^^^ Place new compatibility code before this line
584
+ # set version to current
585
+ @config_presets[CONF_PRESET_CONFIG][CONF_PRESET_VERSION] = @info[:version]
586
+ unless files_to_copy.empty?
458
587
  Log.log.warn('Copying referenced files')
459
588
  files_to_copy.each do |file|
460
589
  FileUtils.cp(file, @main_folder)
461
- Log.log.warn{"..#{file} -> #{@main_folder}"}
590
+ Log.log.warn{"#{file} -> #{@main_folder}"}
462
591
  end
463
592
  end
464
593
  rescue Psych::SyntaxError => e
@@ -473,7 +602,7 @@ module Aspera
473
602
  Log.log.warn{"Renamed config file to #{new_name}."}
474
603
  Log.log.warn('Manual Conversion is required. Next time, a new empty file will be created.')
475
604
  end
476
- raise CliError, e.to_s
605
+ raise Cli::Error, e.to_s
477
606
  end
478
607
 
479
608
  # find plugins in defined paths
@@ -505,45 +634,40 @@ module Aspera
505
634
 
506
635
  # Find a plugin, and issue the "require"
507
636
  # @return [Hash] plugin info: { product: , url:, version: }
508
- def identify_plugin_for_url(url, check_only: nil)
637
+ def identify_plugins_for_url
638
+ app_url = options.get_next_argument('url', mandatory: true)
639
+ check_only = options.get_next_argument('plugin name', mandatory: false)
509
640
  check_only = check_only.to_sym unless check_only.nil?
641
+ found_apps = []
510
642
  plugins.each do |plugin_name_sym, plugin_info|
511
643
  # no detection for internal plugin
512
644
  next if plugin_name_sym.eql?(CONF_PLUGIN_SYM)
513
645
  next if check_only && !check_only.eql?(plugin_name_sym)
514
646
  # load plugin class
515
647
  require plugin_info[:require_stanza]
516
- c = self.class.plugin_class(plugin_name_sym)
648
+ detect_plugin_class = self.class.plugin_class(plugin_name_sym)
517
649
  # requires detection method
518
- next unless c.respond_to?(:detect)
519
- current_url = url
650
+ next unless detect_plugin_class.respond_to?(:detect)
520
651
  detection_info = nil
521
- # first try : direct
522
652
  begin
523
- detection_info = c.detect(current_url)
653
+ detection_info = detect_plugin_class.detect(app_url)
524
654
  rescue OpenSSL::SSL::SSLError => e
525
655
  Log.log.warn(e.message)
526
656
  Log.log.warn('Use option --insecure=yes to allow unchecked certificate') if e.message.include?('cert')
527
657
  rescue StandardError => e
528
- Log.log.debug{"Cannot detect #{plugin_name_sym} : #{e.class}/#{e.message}"}
529
- end
530
- # second try : is there a redirect ?
531
- if detection_info.nil?
532
- begin
533
- # TODO: check if redirect ?
534
- new_url = Rest.new(base_url: url).call(operation: 'GET', subpath: '', redirect_max: 1)[:http].uri.to_s
535
- unless url.eql?(new_url)
536
- detection_info = c.detect(new_url)
537
- current_url = new_url
538
- end
539
- rescue StandardError => e
540
- Log.log.debug{"Cannot detect #{plugin_name_sym} : #{e.message}"}
541
- end
658
+ Log.log.debug{"detect error: #{e}"}
659
+ next
542
660
  end
661
+ next if detection_info.nil?
662
+ assert_type(detection_info, Hash)
663
+ assert_type(detection_info[:url], String) if detection_info.key?(:url)
664
+ app_name = detect_plugin_class.respond_to?(:application_name) ? detect_plugin_class.application_name : detect_plugin_class.name.split('::').last
543
665
  # if there is a redirect, then the detector can override the url.
544
- return {product: plugin_name_sym, url: current_url}.merge(detection_info) unless detection_info.nil?
666
+ found_apps.push({product: plugin_name_sym, name: app_name, url: app_url, version: 'unknown'}.merge(detection_info))
545
667
  end # loop
546
- raise "No known application found at #{url}"
668
+ raise "No known application found at #{app_url}" if found_apps.empty?
669
+ assert(found_apps.all?{|a|a.keys.all?(Symbol)})
670
+ return found_apps
547
671
  end
548
672
 
549
673
  def execute_connect_action
@@ -551,15 +675,15 @@ module Aspera
551
675
  if %i[info version].include?(command)
552
676
  connect_id = options.get_next_argument('id or title')
553
677
  one_res = connect_versions.find{|i|i['id'].eql?(connect_id) || i['title'].eql?(connect_id)}
554
- raise CliNoSuchId.new(:connect, connect_id) if one_res.nil?
678
+ raise Cli::NoSuchIdentifier.new(:connect, connect_id) if one_res.nil?
555
679
  end
556
680
  case command
557
681
  when :list
558
682
  return {type: :object_list, data: connect_versions, fields: %w[id title version]}
559
- when :info # shows files used
683
+ when :info
560
684
  one_res.delete('links')
561
685
  return {type: :single_object, data: one_res}
562
- when :version # shows files used
686
+ when :version
563
687
  all_links = one_res['links']
564
688
  command = options.get_next_command(%i[list download open])
565
689
  if %i[download open].include?(command)
@@ -568,7 +692,7 @@ module Aspera
568
692
  raise 'no such value' if one_link.nil?
569
693
  end
570
694
  case command
571
- when :list # shows files used
695
+ when :list
572
696
  return {type: :object_list, data: all_links}
573
697
  when :download
574
698
  folder_dest = transfer.destination_folder(Fasp::TransferSpec::DIRECTION_RECEIVE)
@@ -594,63 +718,34 @@ module Aspera
594
718
  ascp_path = options.get_next_argument('path to ascp')
595
719
  ascp_version = Fasp::Installation.instance.get_ascp_version(ascp_path)
596
720
  formatter.display_status("ascp version: #{ascp_version}")
597
- preset_name = set_global_default(:ascp_path, ascp_path)
598
- return Main.result_status("Saved to default global preset #{preset_name}")
599
- when :show # shows files used
721
+ set_global_default(:ascp_path, ascp_path)
722
+ return Main.result_nothing
723
+ when :show
600
724
  return {type: :status, data: Fasp::Installation.instance.path(:ascp)}
601
- when :info # shows files used
602
- data = Fasp::Installation::FILES.each_with_object({}) do |v, m|
603
- m[v.to_s] =
604
- begin
605
- Fasp::Installation.instance.path(v)
606
- rescue => e
607
- e.message
608
- end
609
- end
610
- # read PATHs from ascp directly, and pvcl modules as well
611
- Open3.popen3(Fasp::Installation.instance.path(:ascp), '-DDL-') do |_stdin, _stdout, stderr, thread|
612
- last_line = ''
613
- while (line = stderr.gets)
614
- line.chomp!
615
- last_line = line
616
- case line
617
- when /^DBG Path ([^ ]+) (dir|file) +: (.*)$/
618
- data[Regexp.last_match(1)] = Regexp.last_match(3)
619
- when /^DBG Added module group:"([^"]+)" name:"([^"]+)", version:"([^"]+)" interface:"([^"]+)"$/
620
- data[Regexp.last_match(2)] = "#{Regexp.last_match(4)} #{Regexp.last_match(1)} v#{Regexp.last_match(3)}"
621
- when %r{^DBG License result \(/license/(\S+)\): (.+)$}
622
- data[Regexp.last_match(1)] = Regexp.last_match(2)
623
- when /^LOG (.+) version ([0-9.]+)$/
624
- data['product_name'] = Regexp.last_match(1)
625
- data['product_version'] = Regexp.last_match(2)
626
- when /^LOG Initializing FASP version ([^,]+),/
627
- data['ascp_version'] = Regexp.last_match(1)
628
- end
629
- end
630
- if !thread.value.exitstatus.eql?(1) && !data.key?('root')
631
- raise last_line
632
- end
633
- end
634
- data['keypass'] = Fasp::Installation.instance.bypass_pass
635
- # log is "-" no need to display
636
- data.delete('log')
637
- # show command line transfer spec
725
+ when :info
726
+ # collect info from ascp executable
727
+ data = Fasp::Installation.instance.ascp_info
728
+ # add command line transfer spec
638
729
  data['ts'] = transfer.updated_ts
730
+ # add keys
731
+ DataRepository::ELEMENTS.each_with_object(data){|i, h|h[i.to_s] = DataRepository.instance.item(i)}
732
+ # declare those as secrets
733
+ SecretHider::ADDITIONAL_KEYS_TO_HIDE.push(*DataRepository::ELEMENTS.map(&:to_s))
639
734
  return {type: :single_object, data: data}
640
735
  when :products
641
736
  command = options.get_next_command(%i[list use])
642
737
  case command
643
738
  when :list
644
- return {type: :object_list, data: Fasp::Installation.instance.installed_products, fields: %w[name app_root]}
739
+ return {type: :object_list, data: Fasp::Products.installed_products, fields: %w[name app_root]}
645
740
  when :use
646
741
  default_product = options.get_next_argument('product name')
647
742
  Fasp::Installation.instance.use_ascp_from_product(default_product)
648
- preset_name = set_global_default(:ascp_path, Fasp::Installation.instance.path(:ascp))
649
- return Main.result_status("Saved to default global preset #{preset_name}")
743
+ set_global_default(:ascp_path, Fasp::Installation.instance.path(:ascp))
744
+ return Main.result_nothing
650
745
  end
651
746
  when :install
652
747
  # reset to default location, if older default was used
653
- Fasp::Installation.instance.sdk_folder = default_app_main_folder(app_name: APP_NAME_SDK) if @sdk_default_location
748
+ Fasp::Installation.instance.sdk_folder = self.class.default_app_main_folder(app_name: APP_NAME_SDK) if @sdk_default_location
654
749
  v = Fasp::Installation.instance.install_sdk(options.get_option(:sdk_url, mandatory: true))
655
750
  return Main.result_status("Installed version #{v}")
656
751
  when :spec
@@ -680,24 +775,23 @@ module Aspera
680
775
  def execute_preset(action: nil, name: nil)
681
776
  action = options.get_next_command(PRESET_ALL_ACTIONS) if action.nil?
682
777
  name = instance_identifier if name.nil? && PRESET_INSTANCE_ACTIONS.include?(action)
778
+ name = global_default_preset if name.eql?(GLOBAL_DEFAULT_KEYWORD)
683
779
  # those operations require existing option
684
780
  raise "no such preset: #{name}" if PRESET_EXIST_ACTIONS.include?(action) && !@config_presets.key?(name)
685
- selected_preset = @config_presets[name]
686
781
  case action
687
782
  when :list
688
783
  return {type: :value_list, data: @config_presets.keys, name: 'name'}
689
784
  when :overview
690
- return {type: :object_list, data: Formatter.flatten_config_overview(@config_presets)}
785
+ # display process modifies the value (hide secrets): we do not want to save removed secrets
786
+ return {type: :config_over, data: self.class.protect_presets(@config_presets)}
691
787
  when :show
692
- raise "no such config: #{name}" if selected_preset.nil?
693
- return {type: :single_object, data: selected_preset}
788
+ return {type: :single_object, data: self.class.protect_presets(@config_presets[name])}
694
789
  when :delete
695
790
  @config_presets.delete(name)
696
- save_presets_to_config_file
697
791
  return Main.result_status("Deleted: #{name}")
698
792
  when :get
699
793
  param_name = options.get_next_argument('parameter name')
700
- value = selected_preset[param_name]
794
+ value = @config_presets[name][param_name]
701
795
  raise "no such option in preset #{name} : #{param_name}" if value.nil?
702
796
  case value
703
797
  when Numeric, String then return {type: :text, data: ExtendedValue.instance.evaluate(value.to_s)}
@@ -705,30 +799,20 @@ module Aspera
705
799
  return {type: :single_object, data: value}
706
800
  when :unset
707
801
  param_name = options.get_next_argument('parameter name')
708
- selected_preset.delete(param_name)
709
- save_presets_to_config_file
802
+ @config_presets[name].delete(param_name)
710
803
  return Main.result_status("Removed: #{name}: #{param_name}")
711
804
  when :set
712
805
  param_name = options.get_next_argument('parameter name')
713
- param_value = options.get_next_argument('parameter value')
714
806
  param_name = Manager.option_line_to_name(param_name)
715
- if !@config_presets.key?(name)
716
- Log.log.debug{"no such config name: #{name}, initializing"}
717
- selected_preset = @config_presets[name] = {}
718
- end
719
- if selected_preset.key?(param_name)
720
- Log.log.warn{"overwriting value: #{selected_preset[param_name]}"}
721
- end
722
- selected_preset[param_name] = param_value
723
- save_presets_to_config_file
724
- return Main.result_status("Updated: #{name}: #{param_name} <- #{param_value}")
807
+ param_value = options.get_next_argument('parameter value')
808
+ set_preset_key(name, param_name, param_value)
809
+ return Main.result_nothing
725
810
  when :initialize
726
811
  config_value = options.get_next_argument('extended value', type: Hash)
727
812
  if @config_presets.key?(name)
728
813
  Log.log.warn{"configuration already exists: #{name}, overwriting"}
729
814
  end
730
815
  @config_presets[name] = config_value
731
- save_presets_to_config_file
732
816
  return Main.result_status("Modified: #{@option_config_file}")
733
817
  when :update
734
818
  # get unprocessed options
@@ -736,9 +820,6 @@ module Aspera
736
820
  Log.log.debug{"opts=#{unprocessed_options}"}
737
821
  @config_presets[name] ||= {}
738
822
  @config_presets[name].merge!(unprocessed_options)
739
- # fix bug in 4.4 (creating key "true" in "default" preset)
740
- @config_presets[CONF_PRESET_DEFAULT].delete(true) if @config_presets[CONF_PRESET_DEFAULT].is_a?(Hash)
741
- save_presets_to_config_file
742
823
  return Main.result_status("Updated: #{name}")
743
824
  when :ask
744
825
  options.ask_missing_mandatory = :yes
@@ -747,10 +828,9 @@ module Aspera
747
828
  option_value = options.get_interactive(:option, option_name)
748
829
  @config_presets[name][option_name] = option_value
749
830
  end
750
- save_presets_to_config_file
751
831
  return Main.result_status("Updated: #{name}")
752
832
  when :lookup
753
- BasicAuthPlugin.register_options(@agents)
833
+ BasicAuthPlugin.declare_options(options)
754
834
  url = options.get_option(:url, mandatory: true)
755
835
  user = options.get_option(:username, mandatory: true)
756
836
  result = lookup_preset(url: url, username: user)
@@ -789,8 +869,10 @@ module Aspera
789
869
  open
790
870
  documentation
791
871
  genkey
872
+ pubkey
873
+ remote_certificate
792
874
  gem
793
- plugin
875
+ plugins
794
876
  flush_tokens
795
877
  echo
796
878
  wizard
@@ -804,9 +886,10 @@ module Aspera
804
886
  file
805
887
  check_update
806
888
  initdemo
807
- vault].freeze
889
+ vault
890
+ throw].freeze
808
891
 
809
- # "config" plugin
892
+ # Main action procedure for plugin
810
893
  def execute_action
811
894
  action = options.get_next_command(ACTIONS)
812
895
  case action
@@ -817,31 +900,48 @@ module Aspera
817
900
  return Main.result_nothing
818
901
  when :documentation
819
902
  section = options.get_next_argument('private key file path', mandatory: false)
820
- section = '#' + section unless section.nil?
903
+ section = "##{section}" unless section.nil?
821
904
  OpenApplication.instance.uri("#{@info[:help]}#{section}")
822
905
  return Main.result_nothing
823
906
  when :genkey # generate new rsa key
824
907
  private_key_path = options.get_next_argument('private key file path')
825
- private_key_length = options.get_next_argument('size in bits', mandatory: false) || DEFAULT_PRIVKEY_LENGTH
908
+ private_key_length = options.get_next_argument('size in bits', mandatory: false, type: Integer, default: DEFAULT_PRIV_KEY_LENGTH)
826
909
  self.class.generate_rsa_private_key(path: private_key_path, length: private_key_length)
827
- return Main.result_status('Generated key: ' + private_key_path)
910
+ return Main.result_status("Generated #{private_key_length} bit RSA key: #{private_key_path}")
911
+ when :pubkey # get pub key
912
+ private_key_pem = options.get_next_argument('private key PEM value')
913
+ return Main.result_status(OpenSSL::PKey::RSA.new(private_key_pem).public_key.to_s)
914
+ when :remote_certificate
915
+ remote_url = options.get_next_argument('remote URL')
916
+ @option_insecure = true
917
+ remote_certificate = Rest.start_http_session(remote_url).peer_cert
918
+ remote_certificate.subject.to_a.find { |name, _, _| name == 'CN' }[1]
919
+ formatter.display_status("CN=#{remote_certificate.subject.to_a.find { |name, _, _| name == 'CN' }[1] rescue ''}")
920
+ return Main.result_status(remote_certificate.to_pem)
828
921
  when :echo # display the content of a value given on command line
829
- result = {type: :other_struct, data: options.get_next_argument('value')}
830
- # special for csv
831
- result[:type] = :object_list if result[:data].is_a?(Array) && result[:data].first.is_a?(Hash)
832
- result[:type] = :single_object if result[:data].is_a?(Hash)
833
- return result
922
+ return Formatter.auto_type(options.get_next_argument('value'))
834
923
  when :flush_tokens
835
924
  deleted_files = Oauth.flush_tokens
836
925
  return {type: :value_list, data: deleted_files, name: 'file'}
837
- when :plugin
926
+ when :plugins
838
927
  case options.get_next_command(%i[list create])
839
928
  when :list
840
- return {type: :object_list, data: @plugins.keys.map { |i| { 'plugin' => i.to_s, 'path' => @plugins[i][:source] } }, fields: %w[plugin path]}
929
+ result = []
930
+ @plugins.each do |name, info|
931
+ require info[:require_stanza]
932
+ plugin_class = self.class.plugin_class(name)
933
+ result.push({
934
+ plugin: name,
935
+ detect: Formatter.tick(plugin_class.respond_to?(:detect)),
936
+ wizard: Formatter.tick(plugin_class.respond_to?(:wizard)),
937
+ path: info[:source]
938
+ })
939
+ end
940
+ return {type: :object_list, data: result, fields: %w[plugin detect wizard path]}
841
941
  when :create
842
942
  plugin_name = options.get_next_argument('name', expected: :single).downcase
843
- plugin_folder = options.get_next_argument('folder', expected: :single, mandatory: false) || File.join(@main_folder, ASPERA_PLUGINS_FOLDERNAME)
844
- plugin_file = File.join(plugin_folder, "#{plugin_name}.rb")
943
+ destination_folder = options.get_next_argument('folder', expected: :single, mandatory: false) || File.join(@main_folder, ASPERA_PLUGINS_FOLDERNAME)
944
+ plugin_file = File.join(destination_folder, "#{plugin_name}.rb")
845
945
  content = <<~END_OF_PLUGIN_CODE
846
946
  require 'aspera/cli/plugin'
847
947
  module Aspera
@@ -858,94 +958,19 @@ module Aspera
858
958
  File.write(plugin_file, content)
859
959
  return Main.result_status("Created #{plugin_file}")
860
960
  end
861
- when :wizard
961
+ when :detect, :wizard
862
962
  # interactive mode
863
963
  options.ask_missing_mandatory = true
864
- # register url option
865
- BasicAuthPlugin.register_options(@agents)
866
- params = {}
867
- # get from option, or ask
868
- params[:instance_url] = options.get_option(:url, mandatory: true)
869
- # check it is a well formatted url: starts with scheme
870
- if !params[:instance_url].match?(%r{^[a-z]{1,6}://})
871
- new_url = "https://#{params[:instance_url]}"
872
- Log.log.warn("URL #{params[:instance_url]} does not start with a scheme, using #{new_url}")
873
- params[:instance_url] = new_url
874
- end
875
- # allow user to tell the preset name
876
- params[:preset_name] = options.get_option(:id)
877
- # allow user to specify type of application (symbol)
878
- identification = identify_plugin_for_url(params[:instance_url], check_only: value_or_query(allowed_types: String))
879
- Log.log.debug{"Detected: #{identification}"}
880
- formatter.display_status("Detected: #{identification[:name]} at #{identification[:url]}".bold)
881
- # we detected application (not set by user)
882
- params[:plugin_sym] = identification[:product]
883
- # update the url option
884
- params[:instance_url] = identification[:url]
885
- options.set_option(:url, params[:instance_url])
886
- # instantiate plugin: command line options are known and wizard can be called
887
- plugin_instance = self.class.plugin_class(params[:plugin_sym]).new(@agents.merge({skip_basic_auth_options: true}))
888
- raise CliBadArgument, "Detected: #{params[:plugin_sym]}, no wizard available for this application" unless plugin_instance.respond_to?(:wizard)
889
- # get default preset name if not set by user
890
- params[:prepare] = true
891
- plugin_instance.send(:wizard, params)
892
- params[:prepare] = false
893
-
894
- if params[:need_private_key]
895
- # lets see if path to priv key is provided
896
- private_key_path = options.get_option(:pkeypath)
897
- # give a chance to provide
898
- if private_key_path.nil?
899
- formatter.display_status('Please provide path to your private RSA key, or empty to generate one:')
900
- private_key_path = options.get_option(:pkeypath, mandatory: true).to_s
901
- # private_key_path = File.expand_path(private_key_path)
902
- end
903
- # else generate path
904
- if private_key_path.empty?
905
- private_key_path = File.join(@main_folder, DEFAULT_PRIV_KEY_FILENAME)
906
- end
907
- if File.exist?(private_key_path)
908
- formatter.display_status('Using existing key:')
909
- else
910
- formatter.display_status("Generating #{DEFAULT_PRIVKEY_LENGTH} bit RSA key...")
911
- Config.generate_rsa_private_key(path: private_key_path)
912
- formatter.display_status('Created key:')
913
- end
914
- formatter.display_status(private_key_path)
915
- params[:pub_key_pem] = OpenSSL::PKey::RSA.new(File.read(private_key_path)).public_key.to_s
916
- params[:private_key_path] = private_key_path
917
- end
918
-
919
- formatter.display_status("Preparing preset: #{params[:preset_name]}")
920
- # init defaults if necessary
921
- @config_presets[CONF_PRESET_DEFAULT] ||= {}
922
- option_override = options.get_option(:override, mandatory: true)
923
- raise CliError, "A default configuration already exists for plugin '#{params[:plugin_sym]}' (use --override=yes or --default=no)" \
924
- if !option_override && options.get_option(:default, mandatory: true) && @config_presets[CONF_PRESET_DEFAULT].key?(params[:plugin_sym])
925
- raise CliError, "Preset already exists: #{params[:preset_name]} (use --override=yes or --id=<name>)" \
926
- if !option_override && @config_presets.key?(params[:preset_name])
927
- wizard_result = plugin_instance.send(:wizard, params)
928
- Log.log.debug{"wizard result: #{wizard_result}"}
929
- raise "Internal error: missing keys in wizard result: #{wizard_result.keys}" unless %i[preset_value test_args].eql?(wizard_result.keys.sort)
930
- @config_presets[params[:preset_name]] = wizard_result[:preset_value].stringify_keys
931
- params[:test_args] = wizard_result[:test_args]
932
- if options.get_option(:default, mandatory: true)
933
- formatter.display_status("Setting config preset as default for #{params[:plugin_sym]}")
934
- @config_presets[CONF_PRESET_DEFAULT][params[:plugin_sym].to_s] = params[:preset_name]
935
- else
936
- params[:test_args] = "-P#{params[:preset_name]} #{params[:test_args]}"
937
- end
938
- formatter.display_status('Saving config file.')
939
- save_presets_to_config_file
940
- return Main.result_status("Done.\nYou can test with:\n#{@info[:name]} #{params[:test_args]}")
941
- when :detect
942
- # need url / username
943
- BasicAuthPlugin.register_options(@agents)
944
- return {type: :single_object, data: identify_plugin_for_url(options.get_option(:url, mandatory: true))}
964
+ # detect plugins by url and optional query
965
+ apps = identify_plugins_for_url.freeze
966
+ return {
967
+ type: :object_list,
968
+ data: apps
969
+ } if action.eql?(:detect)
970
+ return wizard_find(apps)
945
971
  when :coffee
946
972
  if OpenApplication.instance.url_method.eql?(:text)
947
- require 'aspera/preview/terminal'
948
- return Main.result_status(Preview::Terminal.build(Rest.new(base_url: COFFEE_IMAGE).read('')[:http].body, reserved_lines: 3))
973
+ return Main.result_picture_in_terminal(options, Rest.new(base_url: COFFEE_IMAGE).read('')[:http].body)
949
974
  end
950
975
  OpenApplication.instance.uri(COFFEE_IMAGE)
951
976
  return Main.result_nothing
@@ -979,30 +1004,126 @@ module Aspera
979
1004
  else
980
1005
  Log.log.info{"Creating Demo server preset: #{DEMO_SERVER_PRESET}"}
981
1006
  @config_presets[DEMO_SERVER_PRESET] = {
982
- 'url' => 'ssh://' + DEMO + '.asperasoft.com:33001',
1007
+ 'url' => "ssh://#{DEMO}.asperasoft.com:33001",
983
1008
  'username' => ASPERA,
984
1009
  'ssAP'.downcase.reverse + 'drow'.reverse => DEMO + ASPERA # cspell:disable-line
985
1010
  }
986
1011
  end
987
- @config_presets[CONF_PRESET_DEFAULT] ||= {}
988
- if @config_presets[CONF_PRESET_DEFAULT].key?(SERVER_COMMAND)
989
- Log.log.warn{"Server default preset already set to: #{@config_presets[CONF_PRESET_DEFAULT][SERVER_COMMAND]}"}
1012
+ @config_presets[CONF_PRESET_DEFAULTS] ||= {}
1013
+ if @config_presets[CONF_PRESET_DEFAULTS].key?(SERVER_COMMAND)
1014
+ Log.log.warn{"Server default preset already set to: #{@config_presets[CONF_PRESET_DEFAULTS][SERVER_COMMAND]}"}
990
1015
  Log.log.warn{"Use #{DEMO_SERVER_PRESET} for demo: -P#{DEMO_SERVER_PRESET}"} unless
991
- DEMO_SERVER_PRESET.eql?(@config_presets[CONF_PRESET_DEFAULT][SERVER_COMMAND])
1016
+ DEMO_SERVER_PRESET.eql?(@config_presets[CONF_PRESET_DEFAULTS][SERVER_COMMAND])
992
1017
  else
993
- @config_presets[CONF_PRESET_DEFAULT][SERVER_COMMAND] = DEMO_SERVER_PRESET
1018
+ @config_presets[CONF_PRESET_DEFAULTS][SERVER_COMMAND] = DEMO_SERVER_PRESET
994
1019
  Log.log.info{"Setting server default preset to : #{DEMO_SERVER_PRESET}"}
995
1020
  end
996
- save_presets_to_config_file
997
1021
  return Main.result_status('Done')
998
1022
  when :vault then execute_vault
999
- else raise 'INTERNAL ERROR: wrong case'
1023
+ when :throw
1024
+ # :type [String]
1025
+ options
1026
+ exception_class_name = options.get_next_argument('exception class name', mandatory: true)
1027
+ exception_text = options.get_next_argument('exception text', mandatory: true)
1028
+ exception_class = Object.const_get(exception_class_name)
1029
+ assert(exception_class <= Exception){"#{exception_class} is not an exception: #{exception_class.class}"}
1030
+ raise exception_class, exception_text
1031
+ else error_unreachable_line
1000
1032
  end
1001
1033
  end
1002
1034
 
1003
- # @return email server setting with defaults if not defined
1035
+ def wizard_find(apps)
1036
+ identification = if apps.length.eql?(1)
1037
+ Log.log.debug{"Detected: #{identification}"}
1038
+ apps.first
1039
+ else
1040
+ formatter.display_status('Multiple applications detected, please select from:')
1041
+ formatter.display_results({type: :object_list, data: apps, fields: %w[product url version]})
1042
+ answer = options.prompt_user_input_in_list('product', apps.map{|a|a[:product]})
1043
+ apps.find{|a|a[:product].eql?(answer)}
1044
+ end
1045
+ wiz_url = identification[:url]
1046
+ Log.log.debug{Log.dump(:identification, identification, :ruby)}
1047
+ formatter.display_status("Using: #{identification[:name]} at #{wiz_url}".bold)
1048
+ # set url for instantiation of plugin
1049
+ options.add_option_preset({url: wiz_url})
1050
+ # instantiate plugin: command line options will be known and wizard can be called
1051
+ wiz_plugin_class = self.class.plugin_class(identification[:product])
1052
+ assert(wiz_plugin_class.respond_to?(:wizard), exception_class: Cli::BadArgument){"Detected: #{identification[:product]}, but this application has no wizard"}
1053
+ # instantiate plugin: command line options will be known, e.g. private_key
1054
+ plugin_instance = wiz_plugin_class.new(@agents)
1055
+ wiz_params = {
1056
+ object: plugin_instance
1057
+ }
1058
+ # is private key needed ?
1059
+ if options.known_options.key?(:private_key) &&
1060
+ (!wiz_plugin_class.respond_to?(:private_key_required?) || wiz_plugin_class.private_key_required?(wiz_url))
1061
+ # lets see if path to priv key is provided
1062
+ private_key_path = options.get_option(:key_path)
1063
+ # give a chance to provide
1064
+ if private_key_path.nil?
1065
+ formatter.display_status('Please provide the path to your private RSA key, or nothing to generate one:')
1066
+ private_key_path = options.get_option(:key_path, mandatory: true).to_s
1067
+ # private_key_path = File.expand_path(private_key_path)
1068
+ end
1069
+ # else generate path
1070
+ if private_key_path.empty?
1071
+ private_key_path = File.join(@main_folder, DEFAULT_PRIV_KEY_FILENAME)
1072
+ end
1073
+ if File.exist?(private_key_path)
1074
+ formatter.display_status('Using existing key:')
1075
+ else
1076
+ formatter.display_status("Generating #{DEFAULT_PRIV_KEY_LENGTH} bit RSA key...")
1077
+ self.class.generate_rsa_private_key(path: private_key_path)
1078
+ formatter.display_status('Created key:')
1079
+ end
1080
+ formatter.display_status(private_key_path)
1081
+ private_key_pem = File.read(private_key_path)
1082
+ options.set_option(:private_key, private_key_pem)
1083
+ wiz_params[:private_key_path] = private_key_path
1084
+ wiz_params[:pub_key_pem] = OpenSSL::PKey::RSA.new(private_key_pem).public_key.to_s
1085
+ end
1086
+ Log.log.debug{Log.dump(:wiz_params, wiz_params)}
1087
+ # finally, call the wizard
1088
+ wizard_result = wiz_plugin_class.wizard(**wiz_params)
1089
+ Log.log.debug{"wizard result: #{wizard_result}"}
1090
+ assert(WIZARD_RESULT_KEYS.eql?(wizard_result.keys.sort)){"missing or extra keys in wizard result: #{wizard_result.keys}"}
1091
+ # get preset name from user or default
1092
+ wiz_preset_name = options.get_option(:id)
1093
+ if wiz_preset_name.nil?
1094
+ elements = [
1095
+ identification[:product],
1096
+ URI.parse(wiz_url).host
1097
+ ]
1098
+ elements.push(options.get_option(:username, mandatory: true)) unless wizard_result[:preset_value].key?(:link)
1099
+ wiz_preset_name = elements.join('_').strip.downcase.gsub(/[^a-z0-9]/, '_').squeeze('_')
1100
+ end
1101
+ # test mode does not change conf file
1102
+ return {type: :single_object, data: wizard_result} if options.get_option(:test_mode)
1103
+ # Write configuration file
1104
+ formatter.display_status("Preparing preset: #{wiz_preset_name}")
1105
+ # init defaults if necessary
1106
+ @config_presets[CONF_PRESET_DEFAULTS] ||= {}
1107
+ option_override = options.get_option(:override, mandatory: true)
1108
+ raise Cli::Error, "A default configuration already exists for plugin '#{identification[:product]}' (use --override=yes or --default=no)" \
1109
+ if !option_override && options.get_option(:default, mandatory: true) && @config_presets[CONF_PRESET_DEFAULTS].key?(identification[:product])
1110
+ raise Cli::Error, "Preset already exists: #{wiz_preset_name} (use --override=yes or --id=<name>)" \
1111
+ if !option_override && @config_presets.key?(wiz_preset_name)
1112
+ @config_presets[wiz_preset_name] = wizard_result[:preset_value].stringify_keys
1113
+ test_args = wizard_result[:test_args]
1114
+ if options.get_option(:default, mandatory: true)
1115
+ formatter.display_status("Setting config preset as default for #{identification[:product]}")
1116
+ @config_presets[CONF_PRESET_DEFAULTS][identification[:product].to_s] = wiz_preset_name
1117
+ else
1118
+ test_args = "-P#{wiz_preset_name} #{test_args}"
1119
+ end
1120
+ # TODO: actually test the command
1121
+ return Main.result_status("You can test with:\n#{@info[:name]} #{identification[:product]} #{test_args}")
1122
+ end
1123
+
1124
+ # @return [Hash] email server setting with defaults if not defined
1004
1125
  def email_settings
1005
- smtp = options.get_option(:smtp, mandatory: true, allowed_types: [Hash])
1126
+ smtp = options.get_option(:smtp, mandatory: true)
1006
1127
  # change string keys into symbol keys
1007
1128
  smtp = smtp.symbolize_keys
1008
1129
  # defaults
@@ -1019,91 +1140,97 @@ module Aspera
1019
1140
  smtp[:domain] ||= smtp[:from_email].gsub(/^.*@/, '') if smtp.key?(:from_email)
1020
1141
  # check minimum required
1021
1142
  %i[server port domain].each do |n|
1022
- raise "Missing mandatory smtp parameter: #{n}" unless smtp.key?(n)
1143
+ assert(smtp.key?(n)){"Missing mandatory smtp parameter: #{n}"}
1023
1144
  end
1024
1145
  Log.log.debug{"smtp=#{smtp}"}
1025
1146
  return smtp
1026
1147
  end
1027
1148
 
1028
- # create a clean binding (ruby variable environment)
1029
- def empty_binding
1030
- Kernel.binding
1031
- end
1032
-
1149
+ # send email using ERB template
1033
1150
  def send_email_template(email_template_default: nil, values: {})
1034
- values[:to] ||= options.get_option(:notif_to, mandatory: true)
1035
- notif_template = options.get_option(:notif_template, mandatory: email_template_default.nil?) || email_template_default
1151
+ values[:to] ||= options.get_option(:notify_to, mandatory: true)
1152
+ notify_template = options.get_option(:notify_template, mandatory: email_template_default.nil?) || email_template_default
1036
1153
  mail_conf = email_settings
1037
1154
  values[:from_name] ||= mail_conf[:from_name]
1038
1155
  values[:from_email] ||= mail_conf[:from_email]
1039
1156
  %i[from_name from_email].each do |n|
1040
- raise "Missing email parameter: #{n}" unless values.key?(n)
1157
+ assert(values.key?(n)){"Missing email parameter: #{n}"}
1041
1158
  end
1042
1159
  start_options = [mail_conf[:domain]]
1043
1160
  start_options.push(mail_conf[:username], mail_conf[:password], :login) if mail_conf.key?(:username) && mail_conf.key?(:password)
1044
1161
  # create a binding with only variables defined in values
1045
- template_binding = empty_binding
1162
+ template_binding = Environment.empty_binding
1046
1163
  # add variables to binding
1047
1164
  values.each do |k, v|
1048
- raise "key (#{k.class}) must be Symbol" unless k.is_a?(Symbol)
1165
+ assert_type(k, Symbol)
1049
1166
  template_binding.local_variable_set(k, v)
1050
1167
  end
1051
1168
  # execute template
1052
- msg_with_headers = ERB.new(notif_template).result(template_binding)
1053
- Log.dump(:msg_with_headers, msg_with_headers)
1169
+ msg_with_headers = ERB.new(notify_template).result(template_binding)
1170
+ Log.log.debug{Log.dump(:msg_with_headers, msg_with_headers)}
1171
+ require 'net/smtp'
1054
1172
  smtp = Net::SMTP.new(mail_conf[:server], mail_conf[:port])
1055
1173
  smtp.enable_starttls if mail_conf[:tls]
1056
1174
  smtp.enable_tls if mail_conf[:ssl]
1057
1175
  smtp.start(*start_options) do |smtp_session|
1058
1176
  smtp_session.send_message(msg_with_headers, values[:from_email], values[:to])
1059
1177
  end
1178
+ nil
1060
1179
  end
1061
1180
 
1062
- def save_presets_to_config_file
1181
+ # Save current configuration to config file
1182
+ # return true if file was saved
1183
+ def save_config_file_if_needed
1063
1184
  raise 'no configuration loaded' if @config_presets.nil?
1064
- FileUtils.mkdir_p(@main_folder) unless Dir.exist?(@main_folder)
1065
- Log.log.debug{"Writing #{@option_config_file}"}
1066
- File.write(@option_config_file, @config_presets.to_yaml)
1185
+ current_checksum = config_checksum
1186
+ return false if @config_checksum_on_disk.eql?(current_checksum)
1187
+ FileUtils.mkdir_p(@main_folder)
1067
1188
  Environment.restrict_file_access(@main_folder)
1068
- Environment.restrict_file_access(@option_config_file)
1189
+ Log.log.info{"Writing #{@option_config_file}"}
1190
+ formatter.display_status('Saving config file.')
1191
+ Environment.write_file_restricted(@option_config_file, force: true) {@config_presets.to_yaml}
1192
+ @config_checksum_on_disk = current_checksum
1193
+ return true
1069
1194
  end
1070
1195
 
1071
1196
  # returns [String] name if config_presets has default
1072
1197
  # returns nil if there is no config or bypass default params
1073
1198
  def get_plugin_default_config_name(plugin_name_sym)
1074
- raise 'internal error: config_presets shall be defined' if @config_presets.nil?
1199
+ assert(!@config_presets.nil?){'config_presets shall be defined'}
1075
1200
  if !@use_plugin_defaults
1076
1201
  Log.log.debug('skip default config')
1077
1202
  return nil
1078
1203
  end
1079
- if @config_presets.key?(CONF_PRESET_DEFAULT) &&
1080
- @config_presets[CONF_PRESET_DEFAULT].key?(plugin_name_sym.to_s)
1081
- default_config_name = @config_presets[CONF_PRESET_DEFAULT][plugin_name_sym.to_s]
1204
+ if @config_presets.key?(CONF_PRESET_DEFAULTS) &&
1205
+ @config_presets[CONF_PRESET_DEFAULTS].key?(plugin_name_sym.to_s)
1206
+ default_config_name = @config_presets[CONF_PRESET_DEFAULTS][plugin_name_sym.to_s]
1082
1207
  if !@config_presets.key?(default_config_name)
1083
1208
  Log.log.error do
1084
- "Default config name [#{default_config_name}] specified for plugin [#{plugin_name_sym}], but it does not exist in config file.\n"\
1085
- 'Please fix the issue: either create preset with one parameter: '\
1209
+ "Default config name [#{default_config_name}] specified for plugin [#{plugin_name_sym}], but it does not exist in config file.\n" \
1210
+ 'Please fix the issue: either create preset with one parameter: ' \
1086
1211
  "(#{@info[:name]} config id #{default_config_name} init @json:'{}') or remove default (#{@info[:name]} config id default remove #{plugin_name_sym})."
1087
1212
  end
1088
1213
  end
1089
- raise CliError, "Config name [#{default_config_name}] must be a hash, check config file." if !@config_presets[default_config_name].is_a?(Hash)
1214
+ raise Cli::Error, "Config name [#{default_config_name}] must be a hash, check config file." if !@config_presets[default_config_name].is_a?(Hash)
1090
1215
  return default_config_name
1091
1216
  end
1092
1217
  return nil
1093
1218
  end # get_plugin_default_config_name
1094
1219
 
1095
- ALLOWED_KEYS = %i[password username description].freeze
1220
+ # TODO: delete: ALLOWED_KEYS = %i[password username description].freeze
1221
+ # @return [Hash] result of execution of vault command
1096
1222
  def execute_vault
1097
- command = options.get_next_command(%i[list show create delete password])
1223
+ command = options.get_next_command(%i[info list show create delete password])
1098
1224
  case command
1225
+ when :info
1226
+ return {type: :single_object, data: vault_info}
1099
1227
  when :list
1100
1228
  return {type: :object_list, data: vault.list}
1101
1229
  when :show
1102
1230
  return {type: :single_object, data: vault.get(label: options.get_next_argument('label'))}
1103
1231
  when :create
1104
- label = options.get_next_argument('label')
1105
- info = options.get_next_argument('info Hash')
1106
- raise 'info must be Hash' unless info.is_a?(Hash)
1232
+ label = options.get_next_argument('label', type: String)
1233
+ info = options.get_next_argument('info', type: Hash)
1107
1234
  info = info.symbolize_keys
1108
1235
  info[:label] = label
1109
1236
  vault.set(info)
@@ -1112,7 +1239,7 @@ module Aspera
1112
1239
  vault.delete(label: options.get_next_argument('label'))
1113
1240
  return Main.result_status('Password deleted')
1114
1241
  when :password
1115
- raise 'Vault does not support password change' unless vault.respond_to?(:password=)
1242
+ assert(vault.respond_to?(:password=)){'Vault does not support password change'}
1116
1243
  new_password = options.get_next_argument('new_password')
1117
1244
  vault.password = new_password
1118
1245
  vault.save
@@ -1120,38 +1247,47 @@ module Aspera
1120
1247
  end
1121
1248
  end
1122
1249
 
1250
+ # @return [String] value from vault matching <name>.<param>
1123
1251
  def vault_value(name)
1124
1252
  m = name.match(/^(.+)\.(.+)$/)
1125
1253
  raise 'vault name shall match <name>.<param>' if m.nil?
1254
+ # this raise exception if label not found:
1126
1255
  info = vault.get(label: m[1])
1127
- # raise "no such vault entry: #{m[1]}" if info.nil?
1128
1256
  value = info[m[2].to_sym]
1129
1257
  raise "no such entry value: #{m[2]}" if value.nil?
1130
1258
  return value
1131
1259
  end
1132
1260
 
1261
+ def vault_info
1262
+ info = options.get_option(:vault) || {}
1263
+ info = info.symbolize_keys
1264
+ info[:type] ||= 'file'
1265
+ info[:name] ||= (info[:type].eql?('file') ? DEFAULT_VAULT_FILENAME : PROGRAM_NAME)
1266
+ assert(info.keys.sort == %i[name type]) {"vault info shall have exactly keys 'type' and 'name'"}
1267
+ assert(info.values.all?(String)){'vault info shall have only string values'}
1268
+ info[:password] = options.get_option(:vault_password, mandatory: true)
1269
+ return info
1270
+ end
1271
+
1272
+ # @return [Object] vault, from options or cache
1133
1273
  def vault
1134
1274
  if @vault.nil?
1135
- vault_info = options.get_option(:vault) || {'type' => 'file', 'name' => 'vault.bin'}
1136
- vault_password = options.get_option(:vault_password, mandatory: true)
1137
- raise 'vault must be Hash' unless vault_info.is_a?(Hash)
1138
- vault_type = vault_info['type'] || 'file'
1139
- vault_name = vault_info['name'] || (vault_type.eql?('file') ? 'vault.bin' : PROGRAM_NAME)
1140
- case vault_type
1275
+ info = vault_info
1276
+ case info[:type]
1141
1277
  when 'file'
1142
1278
  # absolute_path? introduced in ruby 2.7
1143
- vault_path = vault_name.eql?(File.absolute_path(vault_name)) ? vault_name : File.join(@main_folder, vault_name)
1144
- @vault = Keychain::EncryptedHash.new(vault_path, vault_password)
1279
+ @vault = Keychain::EncryptedHash.new(
1280
+ info[:name].eql?(File.absolute_path(info[:name])) ? info[:name] : File.join(@main_folder, info[:name]),
1281
+ info[:password])
1145
1282
  when 'system'
1146
1283
  case Environment.os
1147
1284
  when Environment::OS_X
1148
- @vault = Keychain::MacosSystem.new(vault_name, vault_password)
1149
- when Environment::OS_WINDOWS, Environment::OS_LINUX, Environment::OS_AIX
1150
- raise 'not implemented'
1151
- else raise 'Error, OS not supported'
1285
+ @vault = Keychain::MacosSystem.new(info[:name], info[:password])
1286
+ else
1287
+ raise 'not implemented for this OS'
1152
1288
  end
1153
1289
  else
1154
- raise CliBadArgument, "Unknown vault type: #{vault_type}"
1290
+ raise Cli::BadArgument, "Unknown vault type: #{info[:type]}"
1155
1291
  end
1156
1292
  end
1157
1293
  raise 'No vault defined' if @vault.nil?
@@ -1170,7 +1306,7 @@ module Aspera
1170
1306
  @config_presets.each do |_k, v|
1171
1307
  next unless v.is_a?(Hash)
1172
1308
  conf_url = v['url'].is_a?(String) ? canonical_url(v['url']) : nil
1173
- return v if conf_url.eql?(url) && v['username'].eql?(username)
1309
+ return self.class.protect_presets(v) if conf_url.eql?(url) && v['username'].eql?(username)
1174
1310
  end
1175
1311
  nil
1176
1312
  end