aspera-cli 4.26.0 → 4.26.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (99) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/CHANGELOG.md +36 -4
  4. data/CONTRIBUTING.md +2 -5
  5. data/bin/ascli +2 -2
  6. data/lib/aspera/agent/direct.rb +1 -1
  7. data/lib/aspera/agent/factory.rb +4 -0
  8. data/lib/aspera/agent/httpgw.rb +1 -1
  9. data/lib/aspera/agent/node.rb +2 -2
  10. data/lib/aspera/api/aoc.rb +45 -37
  11. data/lib/aspera/api/cos_node.rb +1 -1
  12. data/lib/aspera/api/faspex.rb +7 -6
  13. data/lib/aspera/api/node.rb +9 -9
  14. data/lib/aspera/ascmd.rb +4 -4
  15. data/lib/aspera/ascp/installation.rb +13 -6
  16. data/lib/aspera/assert.rb +17 -13
  17. data/lib/aspera/cli/context.rb +58 -0
  18. data/lib/aspera/cli/extended_value.rb +9 -4
  19. data/lib/aspera/cli/formatter.rb +91 -187
  20. data/lib/aspera/cli/http.rb +167 -0
  21. data/lib/aspera/cli/manager.rb +132 -77
  22. data/lib/aspera/cli/options.schema.yaml +82 -0
  23. data/lib/aspera/cli/plugins/alee.rb +2 -2
  24. data/lib/aspera/cli/plugins/aoc.rb +113 -77
  25. data/lib/aspera/cli/plugins/ats.rb +16 -16
  26. data/lib/aspera/cli/plugins/base.rb +64 -49
  27. data/lib/aspera/cli/plugins/config.rb +172 -444
  28. data/lib/aspera/cli/plugins/console.rb +7 -7
  29. data/lib/aspera/cli/plugins/cos.rb +2 -2
  30. data/lib/aspera/cli/plugins/factory.rb +3 -0
  31. data/lib/aspera/cli/plugins/faspex.rb +22 -22
  32. data/lib/aspera/cli/plugins/faspex5.rb +68 -49
  33. data/lib/aspera/cli/plugins/faspio.rb +1 -1
  34. data/lib/aspera/cli/plugins/httpgw.rb +2 -2
  35. data/lib/aspera/cli/plugins/node.rb +80 -80
  36. data/lib/aspera/cli/plugins/orchestrator.rb +21 -26
  37. data/lib/aspera/cli/plugins/preview.rb +9 -9
  38. data/lib/aspera/cli/plugins/server.rb +7 -7
  39. data/lib/aspera/cli/plugins/shares.rb +2 -2
  40. data/lib/aspera/cli/preset_manager.rb +235 -0
  41. data/lib/aspera/cli/result.rb +310 -0
  42. data/lib/aspera/cli/{main.rb → runner.rb} +48 -130
  43. data/lib/aspera/cli/sync_actions.rb +15 -11
  44. data/lib/aspera/cli/terminal_formatter.rb +65 -0
  45. data/lib/aspera/cli/transfer_agent.rb +17 -15
  46. data/lib/aspera/cli/transfer_progress.rb +6 -6
  47. data/lib/aspera/cli/version.rb +1 -1
  48. data/lib/aspera/cli/wizard.rb +5 -6
  49. data/lib/aspera/command_line_builder.rb +22 -18
  50. data/lib/aspera/data_repository.rb +4 -0
  51. data/lib/aspera/dot_container.rb +1 -1
  52. data/lib/aspera/environment.rb +25 -6
  53. data/lib/aspera/faspex_gw.rb +1 -1
  54. data/lib/aspera/formatter_interface.rb +14 -0
  55. data/lib/aspera/hash_ext.rb +6 -0
  56. data/lib/aspera/json_rpc.rb +6 -5
  57. data/lib/aspera/keychain/base.rb +1 -1
  58. data/lib/aspera/keychain/encrypted_hash.rb +1 -1
  59. data/lib/aspera/keychain/factory.rb +1 -1
  60. data/lib/aspera/keychain/macos_security.rb +1 -1
  61. data/lib/aspera/log.rb +10 -5
  62. data/lib/aspera/markdown.rb +4 -1
  63. data/lib/aspera/nagios.rb +2 -2
  64. data/lib/aspera/oauth/base.rb +5 -5
  65. data/lib/aspera/oauth/boot.rb +43 -0
  66. data/lib/aspera/oauth/factory.rb +38 -14
  67. data/lib/aspera/oauth/web.rb +2 -2
  68. data/lib/aspera/oauth.rb +1 -0
  69. data/lib/aspera/persistency_action_once.rb +2 -2
  70. data/lib/aspera/preview/file_types.rb +4 -0
  71. data/lib/aspera/products/connect.rb +3 -0
  72. data/lib/aspera/products/transferd.rb +2 -2
  73. data/lib/aspera/proxy_auto_config.rb +6 -3
  74. data/lib/aspera/rest.rb +9 -5
  75. data/lib/aspera/rest_error_analyzer.rb +4 -0
  76. data/lib/aspera/rest_list.rb +10 -3
  77. data/lib/aspera/schema/IBM Aspera Faspex API-5.0-enhanced.yaml +62811 -0
  78. data/lib/aspera/schema/IBM Aspera on Cloud API-0.2.6-enhanced.yaml +9637 -0
  79. data/lib/aspera/schema/async_tables.yaml +361 -0
  80. data/lib/aspera/schema/documentation.rb +107 -0
  81. data/lib/aspera/schema/reader.rb +75 -0
  82. data/lib/aspera/schema/registry.rb +70 -0
  83. data/lib/aspera/secret_hider.rb +4 -0
  84. data/lib/aspera/sync/conf.schema.yaml +0 -26
  85. data/lib/aspera/sync/database.rb +14 -10
  86. data/lib/aspera/sync/operations.rb +14 -10
  87. data/lib/aspera/temp_file_manager.rb +4 -0
  88. data/lib/aspera/transfer/faux_file.rb +1 -1
  89. data/lib/aspera/transfer/parameters.rb +4 -4
  90. data/lib/aspera/transfer/resumer.rb +2 -2
  91. data/lib/aspera/transfer/spec.rb +3 -3
  92. data/lib/aspera/transfer/spec.schema.yaml +18 -9
  93. data/lib/aspera/transfer/uri.rb +1 -1
  94. data/lib/aspera/uri_reader.rb +1 -1
  95. data/lib/aspera/yaml.rb +4 -2
  96. data.tar.gz.sig +0 -0
  97. metadata +28 -15
  98. metadata.gz.sig +1 -1
  99. data/lib/aspera/transfer/spec_doc.rb +0 -76
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'aspera/assert'
4
+
5
+ module Aspera
6
+ module Cli
7
+ # Global objects shared with plugins
8
+ class Context
9
+ # @type [Array<Symbol>]
10
+ MEMBERS = %i[options transfer config formatter persistency man_header presets http_config].freeze
11
+ # @!attribute [rw] options
12
+ # @return [Manager] the command line options manager
13
+ # @!attribute [rw] transfer
14
+ # @return [TransferAgent] the transfer agent, used by transfer plugins
15
+ # @!attribute [rw] config
16
+ # @return [Plugins::Config] the configuration plugin, used by plugins to get configuration values and presets
17
+ # @!attribute [rw] formatter
18
+ # @return [Formatter] the formatter, used by plugins to display results and messages
19
+ # @!attribute [rw] persistency
20
+ # @return [Object] # whatever the type is
21
+ # @!attribute [rw] man_header
22
+ # @return [Boolean] whether to display the manual header in plugin help
23
+ # @!attribute [rw] presets
24
+ # @return [PresetManager] manages the YAML config file and preset resolution
25
+ # @!attribute [rw] http_config
26
+ # @return [Http] manages HTTP/S and TLS runtime options
27
+ attr_accessor(*MEMBERS)
28
+
29
+ # Initialize all members to nil, so that they are defined and can be validated later
30
+ # @return [nil]
31
+ def initialize
32
+ MEMBERS.each{ |i| instance_variable_set(:"@#{i}", nil)}
33
+ end
34
+
35
+ # Validate that all members are set, raise exception if not
36
+ # @raise [Aspera::AssertionError] if any member is not set
37
+ # @return [nil]
38
+ def validate
39
+ MEMBERS.each do |i|
40
+ Aspera.assert(instance_variable_defined?(:"@#{i}")){"context member @#{i} is not defined"}
41
+ Aspera.assert(!instance_variable_get(:"@#{i}").nil?){"context member @#{i} is nil"}
42
+ end
43
+ end
44
+
45
+ # Check if the context is in manual-only mode
46
+ # @return [Boolean] true if in manual-only mode
47
+ def only_manual?
48
+ transfer.eql?(:only_manual)
49
+ end
50
+
51
+ # Set the context to manual-only mode
52
+ # @return [Symbol] :only_manual
53
+ def only_manual!
54
+ @transfer = :only_manual
55
+ end
56
+ end
57
+ end
58
+ end
@@ -15,6 +15,10 @@ require 'singleton'
15
15
  module Aspera
16
16
  module Cli
17
17
  # Command line extended values
18
+ #
19
+ # @!method self.instance
20
+ # Returns the singleton instance of ExtendedValue
21
+ # @return [ExtendedValue] the singleton instance
18
22
  class ExtendedValue
19
23
  include Singleton
20
24
 
@@ -82,6 +86,7 @@ module Aspera
82
86
  # Base handlers
83
87
  # Other handlers can be set using `on`
84
88
  # e.g. `preset` is reader in config plugin
89
+ # @type [Hash{Symbol => Proc}]
85
90
  @handlers = {
86
91
  val: lambda{ |i| i},
87
92
  base64: lambda{ |i| Base64.decode64(i)},
@@ -91,12 +96,12 @@ module Aspera
91
96
  uri: lambda{ |i| UriReader.read(i)},
92
97
  json: lambda{ |i| ExtendedValue.JSON_parse(i)},
93
98
  lines: lambda{ |i| i.split("\n")},
94
- list: lambda{ |i| i[1..-1].split(i[0])},
99
+ list: lambda{ |i| i[1..].split(i[0])},
95
100
  none: lambda{ |i| ExtendedValue.assert_no_value(i, :none); nil}, # rubocop:disable Style/Semicolon
96
101
  path: lambda{ |i| File.expand_path(i)},
97
102
  re: lambda{ |i| Regexp.new(i, Regexp::MULTILINE)},
98
103
  ruby: lambda{ |i| Environment.secure_eval(i, __FILE__, __LINE__)},
99
- s: lambda{ |i| i.to_s},
104
+ s: lambda(&:to_s),
100
105
  secret: lambda{ |i| prompt = i.empty? ? 'secret' : i; $stdin.getpass("#{prompt}> ")}, # rubocop:disable Style/Semicolon
101
106
  stdin: lambda{ |i| ExtendedValue.read_stdin(i)},
102
107
  yaml: lambda{ |i| YAML.load(i)},
@@ -133,7 +138,7 @@ module Aspera
133
138
  # Add a new handler
134
139
  def on(name, &block)
135
140
  Aspera.assert_type(name, Symbol){'name'}
136
- Aspera.assert(block)
141
+ Aspera.assert(block){"block required when registering handler for #{name}"}
137
142
  Log.log.debug{"Setting handler for #{name}"}
138
143
  @handlers[name] = block
139
144
  update_regex
@@ -145,7 +150,7 @@ module Aspera
145
150
  # @param value [String] the value to parse
146
151
  # @param context [String] Context in which evaluation is done
147
152
  # @param allowed [Array<Class>,NilClass] Expected types
148
- # @return [Object] Evaluated value
153
+ # @return [String, Integer, Array, Hash, Boolean] Evaluated value
149
154
  def evaluate(value, context:, allowed: nil)
150
155
  return value unless value.is_a?(String)
151
156
  Aspera.assert_array_all(allowed, Class) unless allowed.nil?
@@ -2,6 +2,7 @@
2
2
 
3
3
  # cspell:ignore jsonpp
4
4
  require 'aspera/cli/special_values'
5
+ require 'aspera/cli/terminal_formatter'
5
6
  require 'aspera/preview/terminal'
6
7
  require 'aspera/secret_hider'
7
8
  require 'aspera/environment'
@@ -20,70 +21,7 @@ module Aspera
20
21
  module Cli
21
22
  # Take care of CLI output on terminal
22
23
  class Formatter
23
- # remove a fields from the list
24
- FIELDS_LESS = '-'
25
- # supported output formats
26
- DISPLAY_FORMATS = %i[text nagios ruby json jsonpp yaml table csv image].freeze
27
- # user output levels
28
- DISPLAY_LEVELS = %i[info data error].freeze
29
- # column names for single object display in table
30
- SINGLE_OBJECT_COLUMN_NAMES = %i[field value].freeze
31
- # Terminal: vertical separator for list of strings.
32
- STR_LST_SEP_VERT = "\n"
33
-
34
- private_constant :FIELDS_LESS, :DISPLAY_FORMATS, :DISPLAY_LEVELS, :SINGLE_OBJECT_COLUMN_NAMES, :STR_LST_SEP_VERT
35
-
36
24
  class << self
37
- # nicer display for boolean
38
- # used by `spec_doc`
39
- def tick(yes)
40
- result =
41
- if Environment.terminal_supports_unicode?
42
- yes ? "\u2713" : "\u2717"
43
- else
44
- yes ? 'Y' : ' '
45
- end
46
- return result.green if yes
47
- return result.red
48
- end
49
-
50
- # Highlight special values on terminal
51
- # empty values are dim
52
- # used by `spec_doc`
53
- def special_format(what)
54
- result = "<#{what}>"
55
- return %w[null empty].any?{ |s| what.include?(s)} ? result.dim : result.reverse_color
56
- end
57
-
58
- # For transfer spec table, build line for display in terminal
59
- # used by `spec_doc`
60
- def check_row(row)
61
- row.each_key do |k|
62
- row[k] = row[k].map{ |i| WordWrap.ww(i.to_s, 120).chomp}.join("\n") if row[k].is_a?(Array)
63
- end
64
- end
65
-
66
- # Give Markdown String, or matched data, return formatted string for terminal
67
- # used by `spec_doc`
68
- # @param match [MatchData,String]
69
- def markdown_text(match)
70
- if match.is_a?(String)
71
- match = Markdown::FORMATS.match(match)
72
- Aspera.assert(match)
73
- end
74
- Aspera.assert_type(match, MatchData)
75
- if match[:entity]
76
- Aspera.assert_values(match[:entity], 'bsol')
77
- '\\'
78
- elsif match[:bold]
79
- match[:bold].to_s.blue
80
- elsif match[:code]
81
- match[:code].to_s.bold
82
- else
83
- Aspera.error_unexpected_value(match.to_s)
84
- end
85
- end
86
-
87
25
  # Replace special values with a readable version on terminal
88
26
  def replace_specific_for_terminal(input_hash, string_list_separator)
89
27
  hash_to_process = [input_hash]
@@ -92,14 +30,14 @@ module Aspera
92
30
  current.each do |key, value|
93
31
  case value
94
32
  when NilClass
95
- current[key] = special_format('null')
33
+ current[key] = TerminalFormatter.special_format('null')
96
34
  when String
97
- current[key] = special_format('empty string') if value.empty?
35
+ current[key] = TerminalFormatter.special_format('empty string') if value.empty?
98
36
  when Proc
99
- current[key] = special_format('lambda')
37
+ current[key] = TerminalFormatter.special_format('lambda')
100
38
  when Array
101
39
  if value.empty?
102
- current[key] = special_format('empty list')
40
+ current[key] = TerminalFormatter.special_format('empty list')
103
41
  elsif value.all?(String)
104
42
  current[key] = value.join(string_list_separator)
105
43
  else
@@ -109,7 +47,7 @@ module Aspera
109
47
  end
110
48
  when Hash
111
49
  if value.empty?
112
- current[key] = special_format('empty dict')
50
+ current[key] = TerminalFormatter.special_format('empty dict')
113
51
  else
114
52
  hash_to_process.push(value)
115
53
  end
@@ -129,6 +67,17 @@ module Aspera
129
67
  @spinner = nil
130
68
  end
131
69
 
70
+ # Display item count if total is provided
71
+ def display_item_count(count, total)
72
+ return if total.nil?
73
+ count = count.to_i
74
+ total = total.to_i
75
+ return if total.eql?(0) && count.eql?(0)
76
+ count_msg = "Items: #{count}/#{total}"
77
+ count_msg = count_msg.bg_red unless count.eql?(total)
78
+ display_status(count_msg)
79
+ end
80
+
132
81
  def long_operation(title = nil, action: :spin)
133
82
  return unless Environment.terminal?
134
83
  return if %i[error data].include?(@options[:display])
@@ -209,10 +158,16 @@ module Aspera
209
158
  nil
210
159
  end
211
160
 
212
- # Main output method
213
- # data: for requested data, not displayed if level==error
214
- # info: additional info, displayed if level==info
215
- # error: always displayed on stderr
161
+ # Main output method for displaying messages to the user
162
+ # This method is used by Result classes to output formatted data
163
+ # @param message_level [Symbol] The level of the message - must be one of: :data, :info, :error
164
+ # @param message [String] The message to display
165
+ # @param hide_secrets [Boolean] Whether to hide secrets in the message (default: true)
166
+ # @return [void]
167
+ # @note Message display behavior depends on the message_level:
168
+ # - +:data+ messages are displayed unless display level is +:error+
169
+ # - +:info+ messages are only displayed when display level is +:info+
170
+ # - +:error+ messages are always displayed on stderr
216
171
  def display_message(message_level, message, hide_secrets: true)
217
172
  message = SecretHider.instance.hide_secrets_in_string(message) if hide_secrets && message.is_a?(String) && hide_secrets?
218
173
  case message_level
@@ -227,129 +182,65 @@ module Aspera
227
182
  display_message(:info, status, **kwargs)
228
183
  end
229
184
 
230
- def display_item_count(number, total)
231
- number = number.to_i
232
- total = total.to_i
233
- return if total.eql?(0) && number.eql?(0)
234
- count_msg = "Items: #{number}/#{total}"
235
- count_msg = count_msg.bg_red unless number.eql?(total)
236
- display_status(count_msg)
237
- end
238
-
185
+ # Check if secrets should be hidden
239
186
  def hide_secrets?
240
187
  !@options[:show_secrets] && !@options[:display].eql?(:data)
241
188
  end
242
189
 
243
- # hides secrets in Hash or Array
190
+ # Hides secrets in Hash or Array
244
191
  def hide_secrets(data)
245
192
  SecretHider.instance.deep_remove_secret(data) if hide_secrets?
246
193
  end
247
194
 
248
- # Display results, especially the table format
249
- # @param type [Symbol] Type of data
250
- # @param data [Object] Data to display
251
- # @param fields [Array<String>] List of fields to display
252
- # @param total [Integer] Total number of items
253
- # @param name [String] Name of the column to display
254
- def display_results(type:, data: nil, fields: nil, total: nil, name: nil)
255
- Log.log.debug{"display_results: type=#{type}"}
256
- Log.dump(:data, data, level: :trace1)
257
- Log.dump(:fields, fields, level: :trace1)
258
- Aspera.assert_type(type, Symbol){'result must have type'}
259
- Aspera.assert(!data.nil? || %i[empty nothing].include?(type)){'result must have data'}
260
- display_item_count(data.length, total) unless total.nil?
261
- hide_secrets(data)
262
- data = SecretHider.instance.hide_secrets_in_string(data) if data.is_a?(String) && hide_secrets?
263
- @options[:format] = :image if type.eql?(:image)
264
- case @options[:format]
265
- when :text
266
- display_message(:data, data.to_s)
267
- when :nagios
268
- Nagios.process(data)
269
- when :ruby
270
- display_message(:data, PP.pp(filter_list_on_fields(data), +''))
271
- when :json
272
- display_message(:data, JSON.generate(filter_list_on_fields(data)))
273
- when :jsonpp
274
- display_message(:data, JSON.pretty_generate(filter_list_on_fields(data)))
275
- when :yaml
276
- display_message(:data, YAML.dump(filter_list_on_fields(data)))
277
- when :image
278
- # if object or list, then must be a single
279
- case type
280
- when :single_object, :object_list
281
- data = [data] if type.eql?(:single_object)
282
- raise BadArgument, 'image display requires a single result' unless data.length == 1
283
- fields = compute_fields(data, fields)
284
- raise BadArgument, 'select a single field to display' unless fields.length == 1
285
- data = data.first
286
- raise BadArgument, 'no such field' unless data.key?(fields.first)
287
- data = data[fields.first]
288
- end
289
- Aspera.assert_type(data, String){'URL or blob for image'}
290
- # Check if URL
291
- data =
292
- begin
293
- # just validate
294
- URI.parse(data)
295
- if Environment.instance.url_method.eql?(:text)
296
- UriReader.read(data)
297
- else
298
- Environment.instance.open_uri(data)
299
- display_message(:info, "Opened URL in browser: #{data}")
300
- :done
301
- end
302
- rescue URI::InvalidURIError
303
- data
304
- end
305
- # try base64
306
- data = begin
307
- Base64.strict_decode64(data)
308
- rescue
309
- data
310
- end
311
- # here, data is the image blob
312
- display_message(:data, Preview::Terminal.build(data, **@options[:image].symbolize_keys)) unless data.eql?(:done)
313
- when :table, :csv
314
- case type
315
- when :single_object
316
- # :single_object is a Hash, where key=column name
317
- Aspera.assert_type(data, Hash){'result'}
318
- if data.empty?
319
- display_message(:data, self.class.special_format('empty dict'))
320
- else
321
- data = DotContainer.new(data).to_dotted if @options[:flat_hash]
322
- display_table([data], compute_fields([data], fields), single: true)
323
- end
324
- when :object_list
325
- # :object_list is an Array of Hash, where key=column name
326
- Aspera.assert_array_all(data, Hash){'result'}
327
- data = data.map{ |obj| DotContainer.new(obj).to_dotted} if @options[:flat_hash]
328
- display_table(data, compute_fields(data, fields), single: type.eql?(:single_object))
329
- when :value_list
330
- # :value_list is a simple array of values, name of column provided in `name`
331
- display_table(data.map{ |i| {name => i}}, [name])
332
- when :special # no table
333
- if data.eql?(:nothing)
334
- Log.log.debug('no result expected')
335
- return
336
- end
337
- display_message(:info, self.class.special_format(data.to_s))
338
- return
339
- when :status # no table
340
- # :status displays a simple message
341
- display_message(:info, data)
342
- when :text # no table
343
- # :status displays a simple message
344
- display_message(:data, data)
345
- else Aspera.error_unexpected_value(type){'data type'}
346
- end
347
- else Aspera.error_unexpected_value(@options[:format]){'format'}
348
- end
195
+ # Display results using the Visitor pattern
196
+ # Each Result subclass knows how to format itself by calling the appropriate
197
+ # formatter method, eliminating the need for type checking in the formatter.
198
+ # @param result [Result] Result object to display
199
+ def display_results(result)
200
+ require 'aspera/cli/result'
201
+ Aspera.assert_type(result, Cli::Result){'result must be a Result object'}
202
+
203
+ Log.log.debug{"display_results: result class=#{result.class.name}"}
204
+ Log.dump(:data, result.data, level: :trace1)
205
+ Log.dump(:fields, result.fields, level: :trace1)
206
+
207
+ # Hide secrets in data
208
+ hide_secrets(result.data)
209
+
210
+ # Use the Visitor pattern: delegate formatting to the result object
211
+ result.format(self)
349
212
  end
350
- #==========================================================================================
213
+ # Public methods exposed to Result classes for formatting
351
214
 
352
- private
215
+ # Get the current format type
216
+ def format_type
217
+ @options[:format]
218
+ end
219
+
220
+ # Set the format type (used by Image result)
221
+ def set_format_type(format)
222
+ @options[:format] = format
223
+ end
224
+
225
+ # Hide secrets in a string
226
+ def hide_secrets_in_string(string)
227
+ SecretHider.instance.hide_secrets_in_string(string)
228
+ end
229
+
230
+ # Get special format string
231
+ def special_format(text)
232
+ TerminalFormatter.special_format(text)
233
+ end
234
+
235
+ # Check if flat_hash option is enabled
236
+ def flat_hash?
237
+ @options[:flat_hash]
238
+ end
239
+
240
+ # Get image options
241
+ def image_options
242
+ @options[:image].symbolize_keys
243
+ end
353
244
 
354
245
  # @return all fields of all objects in list of objects
355
246
  def all_fields(data)
@@ -379,7 +270,7 @@ module Aspera
379
270
  removal = false
380
271
  if item[0].eql?(FIELDS_LESS)
381
272
  removal = true
382
- item = item[1..-1]
273
+ item = item.delete_prefix(FIELDS_LESS)
383
274
  end
384
275
  case item
385
276
  when SpecialValues::ALL
@@ -439,7 +330,7 @@ module Aspera
439
330
  Aspera.assert_array_all(fields, String)
440
331
  if object_array.empty?
441
332
  # no display for csv
442
- display_message(:info, self.class.special_format('empty')) if @options[:format].eql?(:table)
333
+ display_message(:info, TerminalFormatter.special_format('empty')) if @options[:format].eql?(:table)
443
334
  return
444
335
  end
445
336
  filter_columns_on_select(object_array)
@@ -501,6 +392,19 @@ module Aspera
501
392
  end
502
393
  nil
503
394
  end
395
+
396
+ # remove a fields from the list
397
+ FIELDS_LESS = '-'
398
+ # supported output formats
399
+ DISPLAY_FORMATS = %i[text nagios ruby json jsonpp yaml table csv image].freeze
400
+ # user output levels
401
+ DISPLAY_LEVELS = %i[info data error].freeze
402
+ # column names for single object display in table
403
+ SINGLE_OBJECT_COLUMN_NAMES = %i[field value].freeze
404
+ # Terminal: vertical separator for list of strings.
405
+ STR_LST_SEP_VERT = "\n"
406
+
407
+ private_constant :FIELDS_LESS, :DISPLAY_FORMATS, :DISPLAY_LEVELS, :SINGLE_OBJECT_COLUMN_NAMES, :STR_LST_SEP_VERT
504
408
  end
505
409
  end
506
410
  end
@@ -0,0 +1,167 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'aspera/cli/special_values'
4
+ require 'aspera/log'
5
+ require 'aspera/assert'
6
+ require 'aspera/line_logger'
7
+ require 'openssl'
8
+
9
+ module Aspera
10
+ module Cli
11
+ # Encapsulates all HTTP/S and TLS runtime configuration options.
12
+ # Extracted from Plugins::Config so it can be referenced independently
13
+ # via Context#http_config without coupling to the plugin machinery.
14
+ class Http
15
+ # Certificate file extensions recognized when scanning a folder
16
+ CERT_EXT = %w[crt cer pem der].freeze
17
+ # OpenSSL constant that disables peer verification (VERIFY_NONE)
18
+ SELF_SIGNED_CERT = OpenSSL::SSL.const_get(:enon_yfirev.to_s.upcase.reverse) # cspell: disable-line
19
+
20
+ private_constant :CERT_EXT, :SELF_SIGNED_CERT
21
+
22
+ def initialize
23
+ @insecure = false
24
+ @warn_insecure = true
25
+ @ignore_cert_host_port = []
26
+ @http_options = {}
27
+ @ssl_warned_urls = []
28
+ @certificate_store = nil
29
+ @certificate_paths = nil
30
+ end
31
+
32
+ attr_accessor :insecure, :warn_insecure, :http_options
33
+ attr_reader :ignore_cert_host_port
34
+
35
+ # Declare all HTTP/S CLI options, with handlers pointing to self.
36
+ # Called once from Config#initialize after this object is instantiated.
37
+ # @param options [Aspera::Cli::Manager]
38
+ def declare_options(options)
39
+ options.declare(:insecure, 'HTTP/S: Do not validate any certificate', allowed: Allowed::TYPES_BOOLEAN, handler: {o: self, m: :insecure}, default: false)
40
+ options.declare(:ignore_certificate, 'HTTP/S: Do not validate certificate for these URLs', allowed: [Array, NilClass], handler: {o: self, m: :ignore_cert_host_port})
41
+ options.declare(:warn_insecure, 'HTTP/S: Issue a warning if certificate is ignored', allowed: Allowed::TYPES_BOOLEAN, handler: {o: self, m: :warn_insecure}, default: true)
42
+ options.declare(:cert_stores, 'HTTP/S: List of folder with trusted certificates', allowed: Allowed::TYPES_STRING_ARRAY, handler: {o: self, m: :trusted_cert_locations})
43
+ options.declare(:http_options, 'HTTP/S: Options for HTTP/S socket', allowed: Hash, handler: {o: self, m: :http_options}, default: {})
44
+ options.declare(:http_proxy, 'HTTP/S: URL for proxy with optional credentials', handler: {o: self, m: :http_proxy})
45
+ end
46
+
47
+ # ------------------------------------------------------------------
48
+ # Proxy
49
+ # ------------------------------------------------------------------
50
+
51
+ def http_proxy
52
+ ENV['http_proxy']
53
+ end
54
+
55
+ def http_proxy=(value)
56
+ URI.parse(value)
57
+ ENV['http_proxy'] = value
58
+ end
59
+
60
+ # ------------------------------------------------------------------
61
+ # Per-URL certificate ignore list
62
+ # ------------------------------------------------------------------
63
+
64
+ def ignore_cert_host_port=(url_list)
65
+ url_list.each do |url|
66
+ uri = URI.parse(url)
67
+ raise "Expecting https scheme: #{url}" unless uri.scheme.eql?('https')
68
+ @ignore_cert_host_port.push([uri.host, uri.port].freeze)
69
+ end
70
+ end
71
+
72
+ # Should the certificate be ignored for this host/port?
73
+ # Also logs a warning the first time (if warn_insecure is set).
74
+ def ignore_cert?(address, port)
75
+ endpoint = [address, port].freeze
76
+ ignore_cert = @insecure || @ignore_cert_host_port.any?(endpoint)
77
+ if ignore_cert && @warn_insecure
78
+ base_url = "https://#{address}:#{port}"
79
+ unless @ssl_warned_urls.include?(base_url)
80
+ Log.log.warn{"Ignoring certificate for: #{base_url}. Do not deactivate certificate verification in production."}
81
+ @ssl_warned_urls.push(base_url)
82
+ end
83
+ end
84
+ Log.log.debug{"ignore cert? #{endpoint} -> #{ignore_cert}"}
85
+ ignore_cert
86
+ end
87
+
88
+ # ------------------------------------------------------------------
89
+ # Trusted certificate store / paths
90
+ # ------------------------------------------------------------------
91
+
92
+ # Add files, folders or the default OS locations to the cert store.
93
+ # @param path_list [Array<String>]
94
+ def trusted_cert_locations=(path_list)
95
+ Aspera.assert_type(path_list, Array){'cert locations'}
96
+ if @certificate_store.nil?
97
+ Log.log.debug('Creating SSL Cert store')
98
+ @certificate_store = OpenSSL::X509::Store.new
99
+ @certificate_paths = []
100
+ end
101
+ path_list.each do |path|
102
+ Aspera.assert_type(path, String){'Expecting a String for certificate location'}
103
+ paths_to_add = [path]
104
+ Log.log.debug{"Adding cert location: #{path}"}
105
+ if path.eql?(SpecialValues::DEF)
106
+ @certificate_store.set_default_paths
107
+ paths_to_add = [OpenSSL::X509::DEFAULT_CERT_DIR]
108
+ paths_to_add.push(OpenSSL::X509::DEFAULT_CERT_FILE) unless defined?(JRUBY_VERSION)
109
+ paths_to_add.select!{ |f| File.exist?(f)}
110
+ elsif File.file?(path)
111
+ @certificate_store.add_file(path)
112
+ elsif File.directory?(path)
113
+ @certificate_store.add_path(path)
114
+ else
115
+ raise "No such file or folder: #{path}"
116
+ end
117
+ paths_to_add.each do |p|
118
+ pp = [File.realpath(p)]
119
+ if File.directory?(p)
120
+ pp = Dir.entries(p)
121
+ .map{ |e| File.realpath(File.join(p, e))}
122
+ .select{ |entry| File.file?(entry)}
123
+ .select{ |entry| CERT_EXT.any?{ |ext| entry.end_with?(ext)}}
124
+ end
125
+ @certificate_paths.concat(pp)
126
+ end
127
+ end
128
+ @certificate_paths.uniq!
129
+ end
130
+
131
+ # Return cert file paths (computes OS defaults lazily if never set).
132
+ def trusted_cert_locations
133
+ if @certificate_paths.nil?
134
+ self.trusted_cert_locations = [SpecialValues::DEF]
135
+ locations = @certificate_paths
136
+ # Restore to "lazy" state so next call recomputes if store was reset
137
+ @certificate_paths = @certificate_store = nil
138
+ return locations
139
+ end
140
+ @certificate_paths
141
+ end
142
+
143
+ # ------------------------------------------------------------------
144
+ # HTTP session callback
145
+ # Called every time a new Net::HTTP session is opened.
146
+ # ------------------------------------------------------------------
147
+
148
+ # @param http_session [Net::HTTP]
149
+ def update_session(http_session)
150
+ http_session.set_debug_output(LineLogger.new(:trace2)) if Log.instance.logger.trace2?
151
+ if http_session.use_ssl? && ignore_cert?(http_session.address, http_session.port)
152
+ http_session.verify_mode = SELF_SIGNED_CERT
153
+ end
154
+ http_session.cert_store = @certificate_store if @certificate_store
155
+ Log.log.debug{"Using cert store #{http_session.cert_store} (#{@certificate_store})"} unless http_session.cert_store.nil?
156
+ @http_options.each do |k, v|
157
+ method = "#{k}=".to_sym
158
+ if http_session.respond_to?(method)
159
+ http_session.send(method, v)
160
+ else
161
+ Log.log.error{"Unknown HTTP session attribute: #{k}"}
162
+ end
163
+ end
164
+ end
165
+ end
166
+ end
167
+ end