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.
- checksums.yaml +4 -4
- checksums.yaml.gz.sig +0 -0
- data/CHANGELOG.md +36 -4
- data/CONTRIBUTING.md +2 -5
- data/bin/ascli +2 -2
- data/lib/aspera/agent/direct.rb +1 -1
- data/lib/aspera/agent/factory.rb +4 -0
- data/lib/aspera/agent/httpgw.rb +1 -1
- data/lib/aspera/agent/node.rb +2 -2
- data/lib/aspera/api/aoc.rb +45 -37
- data/lib/aspera/api/cos_node.rb +1 -1
- data/lib/aspera/api/faspex.rb +7 -6
- data/lib/aspera/api/node.rb +9 -9
- data/lib/aspera/ascmd.rb +4 -4
- data/lib/aspera/ascp/installation.rb +13 -6
- data/lib/aspera/assert.rb +17 -13
- data/lib/aspera/cli/context.rb +58 -0
- data/lib/aspera/cli/extended_value.rb +9 -4
- data/lib/aspera/cli/formatter.rb +91 -187
- data/lib/aspera/cli/http.rb +167 -0
- data/lib/aspera/cli/manager.rb +132 -77
- data/lib/aspera/cli/options.schema.yaml +82 -0
- data/lib/aspera/cli/plugins/alee.rb +2 -2
- data/lib/aspera/cli/plugins/aoc.rb +113 -77
- data/lib/aspera/cli/plugins/ats.rb +16 -16
- data/lib/aspera/cli/plugins/base.rb +64 -49
- data/lib/aspera/cli/plugins/config.rb +172 -444
- data/lib/aspera/cli/plugins/console.rb +7 -7
- data/lib/aspera/cli/plugins/cos.rb +2 -2
- data/lib/aspera/cli/plugins/factory.rb +3 -0
- data/lib/aspera/cli/plugins/faspex.rb +22 -22
- data/lib/aspera/cli/plugins/faspex5.rb +68 -49
- data/lib/aspera/cli/plugins/faspio.rb +1 -1
- data/lib/aspera/cli/plugins/httpgw.rb +2 -2
- data/lib/aspera/cli/plugins/node.rb +80 -80
- data/lib/aspera/cli/plugins/orchestrator.rb +21 -26
- data/lib/aspera/cli/plugins/preview.rb +9 -9
- data/lib/aspera/cli/plugins/server.rb +7 -7
- data/lib/aspera/cli/plugins/shares.rb +2 -2
- data/lib/aspera/cli/preset_manager.rb +235 -0
- data/lib/aspera/cli/result.rb +310 -0
- data/lib/aspera/cli/{main.rb → runner.rb} +48 -130
- data/lib/aspera/cli/sync_actions.rb +15 -11
- data/lib/aspera/cli/terminal_formatter.rb +65 -0
- data/lib/aspera/cli/transfer_agent.rb +17 -15
- data/lib/aspera/cli/transfer_progress.rb +6 -6
- data/lib/aspera/cli/version.rb +1 -1
- data/lib/aspera/cli/wizard.rb +5 -6
- data/lib/aspera/command_line_builder.rb +22 -18
- data/lib/aspera/data_repository.rb +4 -0
- data/lib/aspera/dot_container.rb +1 -1
- data/lib/aspera/environment.rb +25 -6
- data/lib/aspera/faspex_gw.rb +1 -1
- data/lib/aspera/formatter_interface.rb +14 -0
- data/lib/aspera/hash_ext.rb +6 -0
- data/lib/aspera/json_rpc.rb +6 -5
- data/lib/aspera/keychain/base.rb +1 -1
- data/lib/aspera/keychain/encrypted_hash.rb +1 -1
- data/lib/aspera/keychain/factory.rb +1 -1
- data/lib/aspera/keychain/macos_security.rb +1 -1
- data/lib/aspera/log.rb +10 -5
- data/lib/aspera/markdown.rb +4 -1
- data/lib/aspera/nagios.rb +2 -2
- data/lib/aspera/oauth/base.rb +5 -5
- data/lib/aspera/oauth/boot.rb +43 -0
- data/lib/aspera/oauth/factory.rb +38 -14
- data/lib/aspera/oauth/web.rb +2 -2
- data/lib/aspera/oauth.rb +1 -0
- data/lib/aspera/persistency_action_once.rb +2 -2
- data/lib/aspera/preview/file_types.rb +4 -0
- data/lib/aspera/products/connect.rb +3 -0
- data/lib/aspera/products/transferd.rb +2 -2
- data/lib/aspera/proxy_auto_config.rb +6 -3
- data/lib/aspera/rest.rb +9 -5
- data/lib/aspera/rest_error_analyzer.rb +4 -0
- data/lib/aspera/rest_list.rb +10 -3
- data/lib/aspera/schema/IBM Aspera Faspex API-5.0-enhanced.yaml +62811 -0
- data/lib/aspera/schema/IBM Aspera on Cloud API-0.2.6-enhanced.yaml +9637 -0
- data/lib/aspera/schema/async_tables.yaml +361 -0
- data/lib/aspera/schema/documentation.rb +107 -0
- data/lib/aspera/schema/reader.rb +75 -0
- data/lib/aspera/schema/registry.rb +70 -0
- data/lib/aspera/secret_hider.rb +4 -0
- data/lib/aspera/sync/conf.schema.yaml +0 -26
- data/lib/aspera/sync/database.rb +14 -10
- data/lib/aspera/sync/operations.rb +14 -10
- data/lib/aspera/temp_file_manager.rb +4 -0
- data/lib/aspera/transfer/faux_file.rb +1 -1
- data/lib/aspera/transfer/parameters.rb +4 -4
- data/lib/aspera/transfer/resumer.rb +2 -2
- data/lib/aspera/transfer/spec.rb +3 -3
- data/lib/aspera/transfer/spec.schema.yaml +18 -9
- data/lib/aspera/transfer/uri.rb +1 -1
- data/lib/aspera/uri_reader.rb +1 -1
- data/lib/aspera/yaml.rb +4 -2
- data.tar.gz.sig +0 -0
- metadata +28 -15
- metadata.gz.sig +1 -1
- data/lib/aspera/transfer/spec_doc.rb +0 -76
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require 'aspera/log'
|
|
4
4
|
require 'aspera/assert'
|
|
5
|
+
require 'aspera/schema/registry'
|
|
5
6
|
require 'yaml'
|
|
6
7
|
module Aspera
|
|
7
8
|
# Helper class to build command line from a parameter list (key-value hash)
|
|
@@ -35,9 +36,9 @@ module Aspera
|
|
|
35
36
|
|
|
36
37
|
class << self
|
|
37
38
|
# Called by provider of definition before constructor of this class so that schema has all mandatory fields
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
validate_schema(
|
|
39
|
+
# @return [Aspera::Schema::Reader]
|
|
40
|
+
def read_schema(name_sym, ascp: false)
|
|
41
|
+
validate_schema(Schema::Registry.instance.reader(name_sym), ascp: ascp)
|
|
41
42
|
end
|
|
42
43
|
|
|
43
44
|
# @param agent [Symbol] Transfer agent name
|
|
@@ -49,29 +50,32 @@ module Aspera
|
|
|
49
50
|
|
|
50
51
|
private
|
|
51
52
|
|
|
53
|
+
DIRECT_PROPERTIES = %w[x-cli-option x-cli-envvar x-cli-special].freeze
|
|
54
|
+
|
|
52
55
|
# Fill default values for some fields in the schema
|
|
53
|
-
# @param schema [
|
|
54
|
-
# @param ascp
|
|
56
|
+
# @param schema [Schema::Reader] The JSON schema
|
|
57
|
+
# @param ascp [Boolean] `true` if `ascp`
|
|
58
|
+
# @return [Schema::Reader] The JSON schema
|
|
55
59
|
def validate_schema(schema, ascp: false)
|
|
56
|
-
|
|
57
|
-
schema
|
|
58
|
-
|
|
59
|
-
|
|
60
|
+
Aspera.assert_type(schema, Schema::Reader){'schema'}
|
|
61
|
+
Aspera.assert(schema.current.key?('properties')){"Schema must have 'properties': #{schema}"}
|
|
62
|
+
schema.each_property do |property_schema, name, _full_name|
|
|
63
|
+
node = property_schema.current
|
|
64
|
+
unsupported_keys = node.keys - PROPERTY_KEYS
|
|
60
65
|
Aspera.assert(unsupported_keys.empty?){"Unsupported definition keys: #{unsupported_keys}"}
|
|
61
|
-
Aspera.assert(
|
|
62
|
-
Aspera.assert(
|
|
63
|
-
|
|
64
|
-
Aspera.assert(
|
|
65
|
-
|
|
66
|
-
validate_schema(info, ascp: ascp) if info['type'].eql?('object') && info['properties']
|
|
67
|
-
validate_schema(info['items'], ascp: ascp) if info['type'].eql?('array') && info['items'] && info['items']['properties']
|
|
66
|
+
Aspera.assert(node.key?('type') || node.key?('enum')){"Missing type for #{name} in #{schema.current.dig('description').current}"}
|
|
67
|
+
Aspera.assert(node['type'].eql?('boolean')){"switch must be bool: #{name}"} if node['x-cli-switch'] && !node['x-cli-special']
|
|
68
|
+
node['x-cli-option'] = "--#{name.to_s.tr('_', '-')}" if node['x-cli-option'].eql?(true) || (node['x-cli-switch'].eql?(true) && !node.key?('x-cli-option'))
|
|
69
|
+
Aspera.assert(DIRECT_PROPERTIES.any?{ |i| node.key?(i)}, name, type: :warn) if ascp && supported_by_agent(:direct, node)
|
|
70
|
+
node.freeze
|
|
68
71
|
end
|
|
69
72
|
schema
|
|
70
73
|
end
|
|
71
74
|
end
|
|
72
75
|
|
|
73
|
-
# @param [Hash] object with parameters
|
|
74
|
-
# @param [
|
|
76
|
+
# @param object [Hash] object with parameters
|
|
77
|
+
# @param schema [Schema::Reader] JSON schema
|
|
78
|
+
# @param convert [Object] function to convert value
|
|
75
79
|
def initialize(object, schema, convert)
|
|
76
80
|
@object = object # keep reference so that it can be modified by caller before calling `process_params`
|
|
77
81
|
@schema = schema
|
data/lib/aspera/dot_container.rb
CHANGED
|
@@ -39,7 +39,7 @@ module Aspera
|
|
|
39
39
|
# @param container [Hash, Array]
|
|
40
40
|
# @param index [String, Integer]
|
|
41
41
|
def array_requires_integer_index!(container, index)
|
|
42
|
-
Aspera.assert(container.is_a?(Hash) || index.is_a?(Integer)
|
|
42
|
+
Aspera.assert(container.is_a?(Hash) || index.is_a?(Integer), 'Using String index when Integer index used previously')
|
|
43
43
|
end
|
|
44
44
|
|
|
45
45
|
# Create a new `Hash` or `Array` depending on type of `key`
|
data/lib/aspera/environment.rb
CHANGED
|
@@ -13,6 +13,10 @@ require 'shellwords'
|
|
|
13
13
|
|
|
14
14
|
module Aspera
|
|
15
15
|
# detect OS, architecture, and specific stuff
|
|
16
|
+
#
|
|
17
|
+
# @!method self.instance
|
|
18
|
+
# Returns the singleton instance of Environment
|
|
19
|
+
# @return [Environment] the singleton instance
|
|
16
20
|
class Environment
|
|
17
21
|
include Singleton
|
|
18
22
|
|
|
@@ -111,7 +115,7 @@ module Aspera
|
|
|
111
115
|
# @raise [RuntimeError] If `:exception` is `true` and the process fails in `:capture` mode.
|
|
112
116
|
def secure_execute(*cmd, mode: :execute, **kwargs)
|
|
113
117
|
cmd = cmd.map(&:to_s)
|
|
114
|
-
Aspera.assert(cmd.size.positive?,
|
|
118
|
+
Aspera.assert(cmd.size.positive?, 'executable must be present', type: ArgumentError)
|
|
115
119
|
Aspera.assert_values(mode, PROCESS_MODES, type: ArgumentError){'mode'}
|
|
116
120
|
Log.log.debug do
|
|
117
121
|
parts = [mode.to_s, 'command:']
|
|
@@ -151,9 +155,9 @@ module Aspera
|
|
|
151
155
|
# @param path [String] the file path
|
|
152
156
|
# @param force [Boolean] if true, overwrite the file
|
|
153
157
|
# @param mode [Integer] the file mode (permissions)
|
|
154
|
-
# @
|
|
158
|
+
# @yieldreturn [String] The content to write to the file
|
|
155
159
|
def write_file_restricted(path, force: false, mode: nil)
|
|
156
|
-
Aspera.assert(block_given?, type: Aspera::InternalError)
|
|
160
|
+
Aspera.assert(block_given?, 'block required for write_file_restricted', type: Aspera::InternalError)
|
|
157
161
|
if force || !File.exist?(path)
|
|
158
162
|
# Windows may give error
|
|
159
163
|
File.unlink(path) rescue nil
|
|
@@ -192,10 +196,24 @@ module Aspera
|
|
|
192
196
|
end
|
|
193
197
|
|
|
194
198
|
# @return true if we can display Unicode characters
|
|
199
|
+
# Uses Encoding.locale_charmap for OS-independent detection.
|
|
200
|
+
# Falls back to locale env vars for systems where charmap is not available.
|
|
195
201
|
# https://www.gnu.org/software/libc/manual/html_node/Locale-Categories.html
|
|
196
202
|
# https://pubs.opengroup.org/onlinepubs/7908799/xbd/envvar.html
|
|
197
203
|
def terminal_supports_unicode?
|
|
198
|
-
|
|
204
|
+
return false unless terminal?
|
|
205
|
+
locale_charmap_utf8? || I18N_VARS.any?{ |var| ENV[var]&.include?('UTF-8')}
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
private
|
|
209
|
+
|
|
210
|
+
# @return true if the locale charmap resolves to UTF-8
|
|
211
|
+
# Unix: nl_langinfo(CODESET) returns "UTF-8"
|
|
212
|
+
# Windows: GetACP returns "CP65001" which Encoding.find resolves as UTF-8
|
|
213
|
+
def locale_charmap_utf8?
|
|
214
|
+
Encoding.find(Encoding.locale_charmap) == Encoding::UTF_8
|
|
215
|
+
rescue ArgumentError
|
|
216
|
+
false
|
|
199
217
|
end
|
|
200
218
|
end
|
|
201
219
|
attr_accessor :url_method, :file_illegal_characters
|
|
@@ -266,6 +284,7 @@ module Aspera
|
|
|
266
284
|
return unless @os.eql?(OS_WINDOWS) && ENV.key?('USERPROFILE') && Dir.exist?(ENV.fetch('USERPROFILE', nil))
|
|
267
285
|
ENV['HOME'] = ENV.fetch('USERPROFILE', nil)
|
|
268
286
|
Log.log.debug{"Windows: set HOME to USERPROFILE: #{Dir.home}"}
|
|
287
|
+
nil
|
|
269
288
|
end
|
|
270
289
|
|
|
271
290
|
def graphical?
|
|
@@ -280,7 +299,7 @@ module Aspera
|
|
|
280
299
|
when Environment::OS_MACOS then return self.class.secure_execute('open', uri.to_s)
|
|
281
300
|
when Environment::OS_WINDOWS then return self.class.secure_execute('start', 'explorer', %Q{"#{uri}"})
|
|
282
301
|
when Environment::OS_LINUX then return self.class.secure_execute('xdg-open', uri.to_s)
|
|
283
|
-
else
|
|
302
|
+
else Aspera.error_unexpected_value(os){'no graphical open method'}
|
|
284
303
|
end
|
|
285
304
|
end
|
|
286
305
|
|
|
@@ -334,7 +353,7 @@ module Aspera
|
|
|
334
353
|
filename = filename.chop while filename.end_with?(' ', '.')
|
|
335
354
|
if @file_illegal_characters&.size.to_i >= 2
|
|
336
355
|
# replace all illegal characters with safe_char
|
|
337
|
-
filename = filename.tr(@file_illegal_characters[1
|
|
356
|
+
filename = filename.tr(@file_illegal_characters[1..], safe_char)
|
|
338
357
|
end
|
|
339
358
|
# ensure only one safe_char is used at a time
|
|
340
359
|
return filename.gsub(/#{Regexp.escape(safe_char)}+/, safe_char).chomp(safe_char)
|
data/lib/aspera/faspex_gw.rb
CHANGED
|
@@ -68,7 +68,7 @@ module Aspera
|
|
|
68
68
|
case request.path
|
|
69
69
|
when '/aspera/faspex/send'
|
|
70
70
|
begin
|
|
71
|
-
Aspera.assert(!request.body.nil
|
|
71
|
+
Aspera.assert(!request.body.nil?, 'payload missing')
|
|
72
72
|
faspex_pkg_parameters = JSON.parse(request.body)
|
|
73
73
|
Log.log.debug{"faspex pkg create parameters=#{faspex_pkg_parameters}"}
|
|
74
74
|
# compare string, as class is not yet known here
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'aspera/assert'
|
|
4
|
+
|
|
5
|
+
module Aspera
|
|
6
|
+
# Common interface for documentation formatters
|
|
7
|
+
# Used by Schema::Documentation to format tables
|
|
8
|
+
module FormatterInterface
|
|
9
|
+
Aspera.require_method!(:tick)
|
|
10
|
+
Aspera.require_method!(:special_format)
|
|
11
|
+
Aspera.require_method!(:check_row)
|
|
12
|
+
Aspera.require_method!(:markdown_text)
|
|
13
|
+
end
|
|
14
|
+
end
|
data/lib/aspera/hash_ext.rb
CHANGED
|
@@ -9,6 +9,12 @@ class ::Hash
|
|
|
9
9
|
merge!(second){ |_key, v1, v2| v1.is_a?(Hash) && v2.is_a?(Hash) ? v1.deep_merge!(v2) : v2}
|
|
10
10
|
end
|
|
11
11
|
|
|
12
|
+
# Recursively iterate through hash and execute block on leaf values
|
|
13
|
+
# @param memory [Object, nil] Optional memory object passed to block
|
|
14
|
+
# @yieldparam hash [Hash] The current hash
|
|
15
|
+
# @yieldparam key [Object] The current key
|
|
16
|
+
# @yieldparam value [Object] The current value (non-Hash)
|
|
17
|
+
# @yieldparam memory [Object, nil] The memory object
|
|
12
18
|
def deep_do(memory = nil, &block)
|
|
13
19
|
each do |key, value|
|
|
14
20
|
if value.is_a?(Hash)
|
data/lib/aspera/json_rpc.rb
CHANGED
|
@@ -36,15 +36,16 @@ module Aspera
|
|
|
36
36
|
id: @request_id += 1
|
|
37
37
|
})
|
|
38
38
|
Aspera.assert_type(data, Hash){'response'}
|
|
39
|
-
Aspera.assert(data['jsonrpc'] == JSON_RPC_VERSION
|
|
40
|
-
Aspera.assert(data.key?('id')
|
|
41
|
-
Aspera.assert(!(data.key?('error') && data.key?('result'))
|
|
39
|
+
Aspera.assert(data['jsonrpc'] == JSON_RPC_VERSION, 'bad version in response')
|
|
40
|
+
Aspera.assert(data.key?('id'), 'missing id in response')
|
|
41
|
+
Aspera.assert(!(data.key?('error') && data.key?('result')), 'both error and response')
|
|
42
42
|
Aspera.assert(
|
|
43
43
|
!data.key?('error') ||
|
|
44
44
|
data['error'].is_a?(Hash) &&
|
|
45
45
|
data['error']['code'].is_a?(Integer) &&
|
|
46
|
-
data['error']['message'].is_a?(String)
|
|
47
|
-
|
|
46
|
+
data['error']['message'].is_a?(String),
|
|
47
|
+
'bad error response'
|
|
48
|
+
)
|
|
48
49
|
return data['result']
|
|
49
50
|
end
|
|
50
51
|
end
|
data/lib/aspera/keychain/base.rb
CHANGED
|
@@ -28,7 +28,7 @@ module Aspera
|
|
|
28
28
|
vault_file = File.read(@path)
|
|
29
29
|
if vault_file.start_with?('---')
|
|
30
30
|
vault_info = YAML.parse(vault_file).to_ruby
|
|
31
|
-
Aspera.assert(vault_info.keys.sort == FILE_KEYS
|
|
31
|
+
Aspera.assert(vault_info.keys.sort == FILE_KEYS, 'Invalid vault file')
|
|
32
32
|
@cipher_name = vault_info['cipher']
|
|
33
33
|
vault_encrypted_data = vault_info['data']
|
|
34
34
|
else
|
|
@@ -20,7 +20,7 @@ module Aspera
|
|
|
20
20
|
when 'file'
|
|
21
21
|
info[:file] = name || 'vault.bin'
|
|
22
22
|
info[:file] = File.join(folder, info[:file]) unless File.absolute_path?(info[:file])
|
|
23
|
-
Aspera.assert(!password.nil
|
|
23
|
+
Aspera.assert(!password.nil?, 'please provide password')
|
|
24
24
|
info[:password] = password
|
|
25
25
|
# this module requires compilation, so it is optional
|
|
26
26
|
require 'aspera/keychain/encrypted_hash'
|
|
@@ -42,7 +42,7 @@ module Aspera
|
|
|
42
42
|
url = options&.delete(:url)
|
|
43
43
|
if !url.nil?
|
|
44
44
|
uri = URI.parse(url)
|
|
45
|
-
Aspera.assert(uri.scheme.eql?('https')
|
|
45
|
+
Aspera.assert(uri.scheme.eql?('https'), 'only https')
|
|
46
46
|
options[:protocol] = 'htps' # cspell: disable-line
|
|
47
47
|
raise Error, 'host required in URL' if uri.host.nil?
|
|
48
48
|
options[:server] = uri.host
|
data/lib/aspera/log.rb
CHANGED
|
@@ -54,6 +54,10 @@ $VERBOSE = old_verbose
|
|
|
54
54
|
|
|
55
55
|
module Aspera
|
|
56
56
|
# Singleton object for logging
|
|
57
|
+
#
|
|
58
|
+
# @!method self.instance
|
|
59
|
+
# Returns the singleton instance of Log
|
|
60
|
+
# @return [Log] the singleton instance
|
|
57
61
|
class Log
|
|
58
62
|
include Singleton
|
|
59
63
|
|
|
@@ -77,10 +81,10 @@ module Aspera
|
|
|
77
81
|
# @param name [String, Symbol] Name of object dumped
|
|
78
82
|
# @param object [Hash, nil] Data to dump
|
|
79
83
|
# @param level [Symbol] Debug level
|
|
80
|
-
# @
|
|
84
|
+
# @yieldreturn [Object] Computed object to dump (alternative to object parameter)
|
|
81
85
|
def dump(name, object = nil, level: :debug, &block)
|
|
82
86
|
return unless instance.logger.send(:"#{level}?")
|
|
83
|
-
Aspera.assert(object.nil? || block.nil
|
|
87
|
+
Aspera.assert(object.nil? || block.nil?, 'Use either object, or block, not both')
|
|
84
88
|
object = yield if block_given?
|
|
85
89
|
instance.logger.send(level, obj_dump(name, object))
|
|
86
90
|
end
|
|
@@ -93,12 +97,13 @@ module Aspera
|
|
|
93
97
|
JSON.pretty_generate(object) rescue PP.pp(object, +'')
|
|
94
98
|
when :ruby
|
|
95
99
|
PP.pp(object, +'')
|
|
96
|
-
else error_unexpected_value(instance.dump_format){'dump format'}
|
|
100
|
+
else Aspera.error_unexpected_value(instance.dump_format){'dump format'}
|
|
97
101
|
end
|
|
98
102
|
"#{name.to_s.green}(#{instance.dump_format})#{object.class}=\n#{dump_text}"
|
|
99
103
|
end
|
|
100
104
|
|
|
101
105
|
# Capture the output of $stderr and log it at debug level
|
|
106
|
+
# @yieldreturn [void] Code block whose stderr output will be captured
|
|
102
107
|
def capture_stderr
|
|
103
108
|
real_stderr = $stderr
|
|
104
109
|
$stderr = StringIO.new
|
|
@@ -167,7 +172,7 @@ module Aspera
|
|
|
167
172
|
# Get symbol of debug level of underlying logger
|
|
168
173
|
# @return [Symbol] One of LEVELS
|
|
169
174
|
def level
|
|
170
|
-
Aspera.assert(Logger::SEVERITY_LABEL.key?(@logger.level))
|
|
175
|
+
Aspera.assert(Logger::SEVERITY_LABEL.key?(@logger.level), 'unexpected logger level value')
|
|
171
176
|
Logger::SEVERITY_LABEL[@logger.level].downcase
|
|
172
177
|
end
|
|
173
178
|
|
|
@@ -192,7 +197,7 @@ module Aspera
|
|
|
192
197
|
end
|
|
193
198
|
# Use `local2` facility, like other Aspera components
|
|
194
199
|
@logger = Syslog::Logger.new(@program_name, Syslog::LOG_LOCAL2)
|
|
195
|
-
else error_unexpected_value(new_log_type){"log type (#{LOG_TYPES.join(', ')})"}
|
|
200
|
+
else Aspera.error_unexpected_value(new_log_type){"log type (#{LOG_TYPES.join(', ')})"}
|
|
196
201
|
end
|
|
197
202
|
@logger.level = current_severity_integer
|
|
198
203
|
@logger_type = new_log_type
|
data/lib/aspera/markdown.rb
CHANGED
|
@@ -8,11 +8,14 @@ module Aspera
|
|
|
8
8
|
HTML_BREAK = '<br/>'
|
|
9
9
|
|
|
10
10
|
class << self
|
|
11
|
+
COL_WIDTH = 80
|
|
11
12
|
# Generate markdown from the provided 2D table
|
|
13
|
+
# @param table [Array<Array<String>>] 2D array of strings
|
|
14
|
+
# @return [String] markdown table
|
|
12
15
|
def table(table)
|
|
13
16
|
# get max width of each columns
|
|
14
17
|
col_widths = table.transpose.map do |col|
|
|
15
|
-
[col.flat_map{ |c| c.to_s.delete('`').split(HTML_BREAK).map(&:size)}.max,
|
|
18
|
+
[col.flat_map{ |c| c.to_s.delete('`').split(HTML_BREAK).map(&:size)}.max, COL_WIDTH].min
|
|
16
19
|
end
|
|
17
20
|
headings = table.shift
|
|
18
21
|
table.unshift(col_widths.map{ |col_width| '-' * col_width})
|
data/lib/aspera/nagios.rb
CHANGED
|
@@ -24,7 +24,7 @@ module Aspera
|
|
|
24
24
|
# Process results of a analysis and display status and exit with code
|
|
25
25
|
def process(data)
|
|
26
26
|
Aspera.assert_type(data, Array)
|
|
27
|
-
Aspera.assert(!data.empty
|
|
27
|
+
Aspera.assert(!data.empty?, 'data is empty')
|
|
28
28
|
%w[status component message].each do |c|
|
|
29
29
|
Aspera.assert(data.first.key?(c)){"result must have #{c}"}
|
|
30
30
|
end
|
|
@@ -79,7 +79,7 @@ module Aspera
|
|
|
79
79
|
# Readable status list
|
|
80
80
|
# @return [Array] of Hash
|
|
81
81
|
def status_list
|
|
82
|
-
Aspera.assert(!@data.empty
|
|
82
|
+
Aspera.assert(!@data.empty?, 'missing result')
|
|
83
83
|
@data.map{ |i| {'status' => LEVELS[i[:code]].to_s, 'component' => i[:comp], 'message' => i[:msg]}}
|
|
84
84
|
end
|
|
85
85
|
end
|
data/lib/aspera/oauth/base.rb
CHANGED
|
@@ -15,15 +15,15 @@ module Aspera
|
|
|
15
15
|
class Base
|
|
16
16
|
Aspera.require_method!(:create_token)
|
|
17
17
|
# @param params [Hash] Parameters for token creation (client_id, client_secret, scope, etc...)
|
|
18
|
-
# @param use_query [Boolean] Provide parameters in query instead of body
|
|
19
18
|
# @param path_token [String] API end point to create a token from base URL
|
|
19
|
+
# @param use_query [Boolean] Provide parameters in query instead of body
|
|
20
20
|
# @param token_field [String] Field in result that contains the token
|
|
21
21
|
# @param cache_ids [Array] List of unique identifiers for cache id generation
|
|
22
22
|
# @param **rest_params [Hash] Parameters for REST
|
|
23
23
|
def initialize(
|
|
24
24
|
params: {},
|
|
25
|
+
path_token: 'token',
|
|
25
26
|
use_query: false,
|
|
26
|
-
path_token: 'token',
|
|
27
27
|
token_field: Factory::TOKEN_FIELD,
|
|
28
28
|
cache_ids: [],
|
|
29
29
|
**rest_params
|
|
@@ -42,10 +42,10 @@ module Aspera
|
|
|
42
42
|
|
|
43
43
|
# The OAuth API Object
|
|
44
44
|
attr_reader :api
|
|
45
|
-
# Sub path to generate token
|
|
46
|
-
attr_reader :path_token
|
|
47
45
|
# Parameters to generate token
|
|
48
46
|
attr_reader :params
|
|
47
|
+
# Sub path to generate token
|
|
48
|
+
attr_reader :path_token
|
|
49
49
|
|
|
50
50
|
# Helper method to create token as per RFC
|
|
51
51
|
# @return [HTTPResponse]
|
|
@@ -122,7 +122,7 @@ module Aspera
|
|
|
122
122
|
token_data = JSON.parse(json_data)
|
|
123
123
|
Factory.instance.persist_mgr.put(@token_cache_id, json_data)
|
|
124
124
|
end
|
|
125
|
-
Aspera.assert(token_data.key?(@token_field)){"API error: No such field in answer: #{@token_field}"}
|
|
125
|
+
Aspera.assert(token_data.key?(@token_field)){"API error: No such field in answer: #{@token_field}"} unless token_data.nil?
|
|
126
126
|
# ok we shall have a token here
|
|
127
127
|
return token_data[@token_field]
|
|
128
128
|
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'aspera/oauth/base'
|
|
4
|
+
require 'aspera/assert'
|
|
5
|
+
require 'json'
|
|
6
|
+
|
|
7
|
+
module Aspera
|
|
8
|
+
module OAuth
|
|
9
|
+
# Token provider bootstrapped from an existing cookie (e.g. AoC browser cookie).
|
|
10
|
+
# Injects the bearer token and optional refresh token directly into the cache.
|
|
11
|
+
# Never generates a new token from scratch — raises if cache+refresh are both exhausted.
|
|
12
|
+
class Boot < Base
|
|
13
|
+
# @param cookie [String, nil] Raw cookie string (--password), nil to rely on existing cache
|
|
14
|
+
# @param username [String, nil] Expected subject; if provided, must match token's `sub` claim
|
|
15
|
+
# @param **base_params Forwarded to Base (base_url:, params: {client_id:, scope:}, etc.)
|
|
16
|
+
def initialize(cookie: nil, username: nil, **base_params)
|
|
17
|
+
if cookie.nil?
|
|
18
|
+
# No cookie: rely on existing cache, identified by username if provided
|
|
19
|
+
Aspera.assert(username, 'Provide --password (cookie) on first use, or --username for cache lookup', type: ParameterError)
|
|
20
|
+
super(**base_params, cache_ids: [username])
|
|
21
|
+
else
|
|
22
|
+
cookies = cookie.split('; ').map{ |p| p.split('=', 2)}.to_h
|
|
23
|
+
Aspera.assert(cookies.key?('aoc.token'), '--password cookie does not contain aoc.token', type: ParameterError)
|
|
24
|
+
token = cookies['aoc.token']
|
|
25
|
+
decoded = Factory.instance.decode_token(token)
|
|
26
|
+
Aspera.assert_type(decoded, Hash){'Boot: token is not a decodable JWT'}
|
|
27
|
+
sub = decoded['sub']
|
|
28
|
+
Aspera.assert(username.nil? || username.eql?(sub)){"Boot: --username #{username} does not match token subject #{sub}"}
|
|
29
|
+
super(**base_params, cache_ids: [sub])
|
|
30
|
+
token_data = {Factory::TOKEN_FIELD => token}
|
|
31
|
+
token_data['refresh_token'] = cookies['aoc.refresh'] if cookies.key?('aoc.refresh')
|
|
32
|
+
Factory.instance.persist_mgr.put(@token_cache_id, JSON.generate(token_data))
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Should never be reached: if cache and refresh are both exhausted, re-authenticate via browser
|
|
37
|
+
def create_token
|
|
38
|
+
Aspera.report_error(AssertError, 'Boot: token expired and no refresh available — re-authenticate in browser')
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
Factory.instance.register_token_creator(Boot)
|
|
42
|
+
end
|
|
43
|
+
end
|
data/lib/aspera/oauth/factory.rb
CHANGED
|
@@ -7,10 +7,14 @@ require 'base64'
|
|
|
7
7
|
module Aspera
|
|
8
8
|
module OAuth
|
|
9
9
|
# Factory to create tokens and manage their cache
|
|
10
|
+
#
|
|
11
|
+
# @!method self.instance
|
|
12
|
+
# Returns the singleton instance of Factory
|
|
13
|
+
# @return [Factory] the singleton instance
|
|
10
14
|
class Factory
|
|
11
15
|
include Singleton
|
|
12
16
|
|
|
13
|
-
#
|
|
17
|
+
# prefix for persistency of tokens (simplify garbage collect)
|
|
14
18
|
PERSIST_CATEGORY_TOKEN = 'token'
|
|
15
19
|
# prefix for bearer authorization when in header
|
|
16
20
|
SPACE_BEARER_AUTH_SCHEME = 'Bearer '
|
|
@@ -19,33 +23,40 @@ module Aspera
|
|
|
19
23
|
private_constant :PERSIST_CATEGORY_TOKEN, :SPACE_BEARER_AUTH_SCHEME
|
|
20
24
|
|
|
21
25
|
class << self
|
|
26
|
+
# Format a token for use in Authorization header
|
|
22
27
|
# @param token [String] The token alone
|
|
23
28
|
# @return [String] Value suitable for Authorization header
|
|
24
29
|
def bearer_authorization(token)
|
|
25
30
|
return "#{SPACE_BEARER_AUTH_SCHEME}#{token}"
|
|
26
31
|
end
|
|
27
32
|
|
|
28
|
-
#
|
|
33
|
+
# Check if the authorization contains a bearer token
|
|
34
|
+
# @param authorization [String] The authorization header value
|
|
35
|
+
# @return [Boolean] true if the authorization contains a bearer token, i.e. auth scheme is bearer
|
|
29
36
|
def bearer_auth?(authorization)
|
|
30
37
|
return authorization.start_with?(SPACE_BEARER_AUTH_SCHEME)
|
|
31
38
|
end
|
|
32
39
|
|
|
33
40
|
# Extract only token from Authorization (remove scheme)
|
|
41
|
+
# @param authorization [String] The authorization header value
|
|
42
|
+
# @return [String] The bearer token without the scheme prefix
|
|
34
43
|
def bearer_token(authorization)
|
|
35
44
|
Aspera.assert(bearer_auth?(authorization)){'not a bearer token, wrong prefix scheme'}
|
|
36
|
-
return authorization
|
|
45
|
+
return authorization.delete_prefix(SPACE_BEARER_AUTH_SCHEME)
|
|
37
46
|
end
|
|
38
47
|
|
|
39
48
|
# Generate a unique cache id for a token creator
|
|
40
49
|
# @param url [String] Base URL of the OAuth server
|
|
41
50
|
# @param creator_class [Class] Class of the token creator
|
|
42
51
|
# @param params [Array] List of parameters (can be nested) to uniquely identify the token
|
|
43
|
-
# @return a unique cache identifier
|
|
52
|
+
# @return [String] a unique cache identifier
|
|
44
53
|
def cache_id(url, creator_class, *params)
|
|
45
54
|
return IdGenerator.from_list(PERSIST_CATEGORY_TOKEN, url, Factory.class_to_id(creator_class), params)
|
|
46
55
|
end
|
|
47
56
|
|
|
48
|
-
#
|
|
57
|
+
# Convert a class name to snake_case symbol
|
|
58
|
+
# @param creator_class [Class] The class to convert
|
|
59
|
+
# @return [Symbol] snake_case version of class name
|
|
49
60
|
def class_to_id(creator_class)
|
|
50
61
|
return creator_class.name.split('::').last.capital_to_snake.to_sym
|
|
51
62
|
end
|
|
@@ -53,6 +64,7 @@ module Aspera
|
|
|
53
64
|
|
|
54
65
|
private
|
|
55
66
|
|
|
67
|
+
# Initialize the factory with default parameters and empty collections
|
|
56
68
|
def initialize
|
|
57
69
|
# persistency manager
|
|
58
70
|
@persist = nil
|
|
@@ -73,12 +85,16 @@ module Aspera
|
|
|
73
85
|
|
|
74
86
|
attr_reader :parameters
|
|
75
87
|
|
|
88
|
+
# Set the persistence manager for token caching
|
|
89
|
+
# @param manager [Object] The persistence manager instance
|
|
76
90
|
def persist_mgr=(manager)
|
|
77
91
|
@persist = manager
|
|
78
92
|
# cleanup expired tokens
|
|
79
93
|
@persist.garbage_collect(PERSIST_CATEGORY_TOKEN, @parameters[:token_cache_max_age])
|
|
80
94
|
end
|
|
81
95
|
|
|
96
|
+
# Get or initialize the persistence manager
|
|
97
|
+
# @return [Object] The persistence manager instance
|
|
82
98
|
def persist_mgr
|
|
83
99
|
if @persist.nil?
|
|
84
100
|
# use OAuth::Factory.instance.persist_mgr=PersistencyFolder.new)
|
|
@@ -91,11 +107,14 @@ module Aspera
|
|
|
91
107
|
return @persist
|
|
92
108
|
end
|
|
93
109
|
|
|
94
|
-
#
|
|
110
|
+
# Delete all existing tokens in cache
|
|
111
|
+
# @return [void]
|
|
95
112
|
def flush_tokens
|
|
96
113
|
persist_mgr.garbage_collect(PERSIST_CATEGORY_TOKEN)
|
|
97
114
|
end
|
|
98
115
|
|
|
116
|
+
# Retrieve all persisted tokens with their decoded information
|
|
117
|
+
# @return [Array<Hash>] Array of token information hashes
|
|
99
118
|
def persisted_tokens
|
|
100
119
|
data = persist_mgr.current_items(PERSIST_CATEGORY_TOKEN)
|
|
101
120
|
data.each.map do |k, v|
|
|
@@ -134,12 +153,16 @@ module Aspera
|
|
|
134
153
|
return info
|
|
135
154
|
end
|
|
136
155
|
|
|
137
|
-
#
|
|
156
|
+
# Register a bearer token decoder for inspecting token properties
|
|
157
|
+
# @param method [Proc] The decoder lambda/proc to register
|
|
158
|
+
# @return [void]
|
|
138
159
|
def register_decoder(method)
|
|
139
160
|
@decoders.push(method)
|
|
140
161
|
end
|
|
141
162
|
|
|
142
|
-
#
|
|
163
|
+
# Decode a token using all registered decoders
|
|
164
|
+
# @param token [String] The token to decode
|
|
165
|
+
# @return [Hash, nil] Decoded token data or nil if no decoder succeeded
|
|
143
166
|
def decode_token(token)
|
|
144
167
|
@decoders.each do |decoder|
|
|
145
168
|
result = decoder.call(token) rescue nil
|
|
@@ -148,10 +171,9 @@ module Aspera
|
|
|
148
171
|
return
|
|
149
172
|
end
|
|
150
173
|
|
|
151
|
-
#
|
|
152
|
-
# @param
|
|
153
|
-
# @
|
|
154
|
-
# @param id_create called to generate unique id for token, for cache
|
|
174
|
+
# Register a token creation method
|
|
175
|
+
# @param creator_class [Class] The token creator class to register
|
|
176
|
+
# @return [void]
|
|
155
177
|
def register_token_creator(creator_class)
|
|
156
178
|
Aspera.assert_type(creator_class, Class)
|
|
157
179
|
id = Factory.class_to_id(creator_class)
|
|
@@ -159,7 +181,9 @@ module Aspera
|
|
|
159
181
|
@token_type_classes[id] = creator_class
|
|
160
182
|
end
|
|
161
183
|
|
|
162
|
-
#
|
|
184
|
+
# Create a token creator instance for the specified grant method
|
|
185
|
+
# @param parameters [Hash] Parameters including :grant_method and creator-specific options
|
|
186
|
+
# @return [Object] An instance of the registered token creator class
|
|
163
187
|
def create(**parameters)
|
|
164
188
|
Aspera.assert_type(parameters, Hash)
|
|
165
189
|
id = parameters[:grant_method]
|
|
@@ -169,6 +193,6 @@ module Aspera
|
|
|
169
193
|
end
|
|
170
194
|
end
|
|
171
195
|
# JSON Web Signature (JWS) compact serialization: https://datatracker.ietf.org/doc/html/rfc7515
|
|
172
|
-
Factory.instance.register_decoder(lambda{ |token| parts = token.split('.'); Aspera.
|
|
196
|
+
Factory.instance.register_decoder(lambda{ |token| parts = token.split('.'); Aspera.assert_values(parts.length, [3]){'JWS token parts'}; JSON.parse(Base64.decode64(parts[1]))}) # rubocop:disable Style/Semicolon
|
|
173
197
|
end
|
|
174
198
|
end
|
data/lib/aspera/oauth/web.rb
CHANGED
|
@@ -22,8 +22,8 @@ module Aspera
|
|
|
22
22
|
@redirect_uri = redirect_uri
|
|
23
23
|
@path_authorize = path_authorize
|
|
24
24
|
uri = URI.parse(@redirect_uri)
|
|
25
|
-
Aspera.
|
|
26
|
-
Aspera.assert(!uri.port.nil
|
|
25
|
+
Aspera.assert_values(uri.scheme, %w[http https]){'redirect_uri scheme must be http or https'}
|
|
26
|
+
Aspera.assert(!uri.port.nil?, 'redirect_uri must have a port')
|
|
27
27
|
# TODO: we could check that host is localhost or local address, as we are going to listen locally
|
|
28
28
|
end
|
|
29
29
|
|
data/lib/aspera/oauth.rb
CHANGED
|
@@ -23,9 +23,9 @@ module Aspera
|
|
|
23
23
|
# @param :merge Optional merge data from file to current data
|
|
24
24
|
def initialize(manager:, data:, id:, delete: DELETE_DEFAULT, parse: PARSE_DEFAULT, format: FORMAT_DEFAULT, merge: MERGE_DEFAULT)
|
|
25
25
|
Aspera.assert(MANAGER_METHODS.all?{ |i| manager.respond_to?(i)}){"Manager must answer to #{MANAGER_METHODS}"}
|
|
26
|
-
Aspera.assert(!data.nil
|
|
26
|
+
Aspera.assert(!data.nil?, 'data must not be nil')
|
|
27
27
|
Aspera.assert_type(id, String)
|
|
28
|
-
Aspera.assert(!id.empty
|
|
28
|
+
Aspera.assert(!id.empty?, 'id must not be empty')
|
|
29
29
|
Aspera.assert_type(delete, Proc)
|
|
30
30
|
Aspera.assert_type(parse, Proc)
|
|
31
31
|
Aspera.assert_type(format, Proc)
|
|
@@ -8,6 +8,10 @@ require 'marcel'
|
|
|
8
8
|
module Aspera
|
|
9
9
|
module Preview
|
|
10
10
|
# function conversion_type returns one of the types: CONVERSION_TYPES
|
|
11
|
+
#
|
|
12
|
+
# @!method self.instance
|
|
13
|
+
# Returns the singleton instance of FileTypes
|
|
14
|
+
# @return [FileTypes] the singleton instance
|
|
11
15
|
class FileTypes
|
|
12
16
|
include Singleton
|
|
13
17
|
|