aspera-cli 4.14.0 → 4.16.0

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