aspera-cli 4.14.0 → 4.15.0

Sign up to get free protection for your applications and to get access to all the features.
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