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