aspera-cli 4.27.0 → 4.27.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 (142) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/CHANGELOG.md +46 -1
  4. data/CONTRIBUTING.md +1 -7
  5. data/TODO.md +50 -0
  6. data/docs/README.md +709 -489
  7. data/lib/aspera/agent/base.rb +10 -2
  8. data/lib/aspera/agent/connect.rb +7 -7
  9. data/lib/aspera/agent/desktop.rb +3 -3
  10. data/lib/aspera/agent/direct.rb +48 -19
  11. data/lib/aspera/agent/factory.rb +3 -3
  12. data/lib/aspera/agent/httpgw.rb +55 -14
  13. data/lib/aspera/agent/node.rb +2 -2
  14. data/lib/aspera/agent/transferd.rb +12 -12
  15. data/lib/aspera/api/aoc.rb +28 -28
  16. data/lib/aspera/api/cos_node.rb +10 -9
  17. data/lib/aspera/api/faspex.rb +2 -2
  18. data/lib/aspera/api/httpgw.rb +20 -20
  19. data/lib/aspera/api/node.rb +31 -53
  20. data/lib/aspera/ascmd.rb +13 -13
  21. data/lib/aspera/ascp/installation.rb +43 -36
  22. data/lib/aspera/ascp/management.rb +6 -6
  23. data/lib/aspera/assert.rb +12 -13
  24. data/lib/aspera/cli/ascp_actions.rb +7 -7
  25. data/lib/aspera/cli/async_transfer_store.rb +49 -5
  26. data/lib/aspera/cli/bootstrapper.rb +9 -9
  27. data/lib/aspera/cli/command_registry.rb +1 -1
  28. data/lib/aspera/cli/command_spec.rb +6 -3
  29. data/lib/aspera/cli/context.rb +3 -3
  30. data/lib/aspera/cli/extended_value.rb +26 -26
  31. data/lib/aspera/cli/formatter.rb +76 -54
  32. data/lib/aspera/cli/gem_checker.rb +1 -1
  33. data/lib/aspera/cli/hints.rb +2 -2
  34. data/lib/aspera/cli/http.rb +22 -29
  35. data/lib/aspera/cli/mailer.rb +7 -7
  36. data/lib/aspera/cli/mcp_tool.rb +31 -4
  37. data/lib/aspera/cli/option_declarator.rb +77 -0
  38. data/lib/aspera/cli/options.schema.yaml +273 -3
  39. data/lib/aspera/cli/parser.rb +539 -280
  40. data/lib/aspera/cli/plugins/alee.rb +1 -1
  41. data/lib/aspera/cli/plugins/aoc.rb +303 -314
  42. data/lib/aspera/cli/plugins/ats.rb +25 -21
  43. data/lib/aspera/cli/plugins/base.rb +357 -186
  44. data/lib/aspera/cli/plugins/basic_auth.rb +0 -12
  45. data/lib/aspera/cli/plugins/config.rb +76 -73
  46. data/lib/aspera/cli/plugins/console.rb +8 -8
  47. data/lib/aspera/cli/plugins/cos.rb +4 -3
  48. data/lib/aspera/cli/plugins/factory.rb +2 -2
  49. data/lib/aspera/cli/plugins/faspex5.rb +259 -248
  50. data/lib/aspera/cli/plugins/faspio.rb +7 -2
  51. data/lib/aspera/cli/plugins/httpgw.rb +1 -1
  52. data/lib/aspera/cli/plugins/mcp.rb +11 -11
  53. data/lib/aspera/cli/plugins/node.rb +227 -283
  54. data/lib/aspera/cli/plugins/orchestrator.rb +23 -23
  55. data/lib/aspera/cli/plugins/preview.rb +61 -64
  56. data/lib/aspera/cli/plugins/server.rb +36 -31
  57. data/lib/aspera/cli/plugins/shares.rb +174 -135
  58. data/lib/aspera/cli/preset_actions.rb +39 -19
  59. data/lib/aspera/cli/preset_manager.rb +74 -32
  60. data/lib/aspera/cli/result.rb +18 -16
  61. data/lib/aspera/cli/runner.rb +75 -39
  62. data/lib/aspera/cli/secret_finder.rb +1 -1
  63. data/lib/aspera/cli/sync_actions.rb +36 -35
  64. data/lib/aspera/cli/terminal_formatter.rb +2 -2
  65. data/lib/aspera/cli/transfer_actions.rb +17 -10
  66. data/lib/aspera/cli/transfer_agent.rb +44 -44
  67. data/lib/aspera/cli/transfer_progress.rb +7 -7
  68. data/lib/aspera/cli/vault_manager.rb +34 -12
  69. data/lib/aspera/cli/version.rb +1 -1
  70. data/lib/aspera/cli/wizard.rb +17 -20
  71. data/lib/aspera/colors.rb +3 -3
  72. data/lib/aspera/command_line_builder.rb +17 -16
  73. data/lib/aspera/command_line_converter.rb +1 -1
  74. data/lib/aspera/coverage.rb +1 -2
  75. data/lib/aspera/data_repository.rb +1 -1
  76. data/lib/aspera/dot_container.rb +8 -8
  77. data/lib/aspera/environment.rb +13 -13
  78. data/lib/aspera/faspex_gw.rb +4 -4
  79. data/lib/aspera/faspex_postproc.rb +14 -9
  80. data/lib/aspera/graphql.rb +4 -2
  81. data/lib/aspera/hash_ext.rb +2 -2
  82. data/lib/aspera/json_rpc/client.rb +1 -1
  83. data/lib/aspera/keychain/base.rb +10 -3
  84. data/lib/aspera/keychain/encrypted_hash.rb +92 -17
  85. data/lib/aspera/keychain/factory.rb +17 -5
  86. data/lib/aspera/keychain/hashicorp_vault.rb +1 -1
  87. data/lib/aspera/keychain/macos_security.rb +23 -25
  88. data/lib/aspera/keychain/one_password_api.rb +86 -0
  89. data/lib/aspera/keychain/one_password_base.rb +34 -0
  90. data/lib/aspera/keychain/one_password_cli.rb +98 -0
  91. data/lib/aspera/link_header.rb +2 -2
  92. data/lib/aspera/log.rb +13 -13
  93. data/lib/aspera/markdown.rb +5 -5
  94. data/lib/aspera/nagios.rb +8 -8
  95. data/lib/aspera/node_simulator.rb +33 -27
  96. data/lib/aspera/oauth/base.rb +4 -5
  97. data/lib/aspera/oauth/boot.rb +3 -3
  98. data/lib/aspera/oauth/factory.rb +5 -5
  99. data/lib/aspera/oauth/jwt.rb +5 -7
  100. data/lib/aspera/oauth/web.rb +4 -4
  101. data/lib/aspera/persistency_action_once.rb +4 -4
  102. data/lib/aspera/persistency_folder.rb +8 -8
  103. data/lib/aspera/preview/file_types.rb +4 -4
  104. data/lib/aspera/preview/generator.rb +49 -46
  105. data/lib/aspera/preview/options.rb +3 -2
  106. data/lib/aspera/preview/terminal.rb +5 -5
  107. data/lib/aspera/preview/utils.rb +65 -18
  108. data/lib/aspera/products/connect.rb +2 -2
  109. data/lib/aspera/products/desktop.rb +1 -1
  110. data/lib/aspera/products/other.rb +2 -2
  111. data/lib/aspera/products/transferd.rb +3 -3
  112. data/lib/aspera/proxy_auto_config.rb +10 -10
  113. data/lib/aspera/rest.rb +149 -115
  114. data/lib/aspera/rest_error_analyzer.rb +3 -3
  115. data/lib/aspera/rest_list.rb +10 -9
  116. data/lib/aspera/schema/IBM Aspera Node API-4.4.6.yaml +6232 -0
  117. data/lib/aspera/schema/IBM Aspera faspio Gateway API-1.0.0.yaml +249 -0
  118. data/lib/aspera/schema/IBM Aspera on Cloud API-0.2.6-enhanced.yaml +39 -0
  119. data/lib/aspera/schema/IBM_Aspera_Shares.yaml +4653 -0
  120. data/lib/aspera/schema/documentation.rb +17 -11
  121. data/lib/aspera/schema/reader.rb +78 -4
  122. data/lib/aspera/schema/registry.rb +26 -7
  123. data/lib/aspera/secret_hider.rb +5 -5
  124. data/lib/aspera/ssh.rb +64 -31
  125. data/lib/aspera/ssl.rb +3 -3
  126. data/lib/aspera/sync/database.rb +2 -2
  127. data/lib/aspera/sync/operations.rb +13 -13
  128. data/lib/aspera/temp_file_manager.rb +24 -4
  129. data/lib/aspera/transfer/faux_file.rb +2 -2
  130. data/lib/aspera/transfer/parameters.rb +15 -15
  131. data/lib/aspera/transfer/resumer.rb +9 -9
  132. data/lib/aspera/transfer/spec.rb +1 -1
  133. data/lib/aspera/transfer/spec.schema.yaml +1 -0
  134. data/lib/aspera/uri_reader.rb +52 -15
  135. data/lib/aspera/web_auth.rb +6 -5
  136. data/lib/aspera/web_server_simple.rb +9 -9
  137. data/lib/aspera/yaml.rb +4 -4
  138. data.tar.gz.sig +0 -0
  139. metadata +10 -4
  140. metadata.gz.sig +0 -0
  141. data/lib/aspera/cli/plugins/faspex.rb +0 -576
  142. data/lib/aspera/transfer/uri.rb +0 -56
data/lib/aspera/log.rb CHANGED
@@ -20,13 +20,13 @@ class Logger
20
20
 
21
21
  # Add custom level to logger severity, below debug level
22
22
  module Severity
23
- 1.upto(TRACE_MAX).each{ |level| const_set(:"TRACE#{level}", - level)}
23
+ 1.upto(TRACE_MAX).each { |level| const_set(:"TRACE#{level}", - level) }
24
24
  end
25
25
 
26
26
  # Hash
27
27
  # key [Integer] Log level (e.g. 0 for DEBUG)
28
28
  # value [Symbol] Uppercase log level label (e.g. :DEBUG)
29
- SEVERITY_LABEL = Severity.constants.to_h{ |name| [Severity.const_get(name), name]}
29
+ SEVERITY_LABEL = Severity.constants.to_h { |name| [Severity.const_get(name), name] }
30
30
 
31
31
  # Override
32
32
  # @param severity [Integer] Log severity as int
@@ -42,14 +42,14 @@ class Logger
42
42
  def make_methods(str_level)
43
43
  int_level = ::Logger.const_get(str_level.upcase)
44
44
  method_base = str_level.downcase
45
- define_method(method_base, ->(message = nil, &block){add(int_level, message, &block)})
46
- define_method("#{method_base}?", ->{level <= int_level})
47
- define_method("#{method_base}!", ->{self.level = int_level})
45
+ define_method(method_base, ->(message = nil, &block) { add(int_level, message, &block) })
46
+ define_method("#{method_base}?", -> { level <= int_level })
47
+ define_method("#{method_base}!", -> { self.level = int_level })
48
48
  nil
49
49
  end
50
50
  end
51
51
  # Declare methods for all levels
52
- Logger::Severity.constants.each{ |severity| make_methods(severity)}
52
+ Logger::Severity.constants.each { |severity| make_methods(severity) }
53
53
  end
54
54
 
55
55
  # Restore warnings
@@ -68,7 +68,7 @@ module Aspera
68
68
  LOG_TYPES = %i[stderr stdout syslog].freeze
69
69
 
70
70
  # Levels are :trace2,:trace1,:debug,:info,:warn,:error,fatal,:unknown
71
- LEVELS = Logger::Severity.constants.sort{ |a, b| Logger::Severity.const_get(a) <=> Logger::Severity.const_get(b)}.map{ |c| c.downcase.to_sym}.freeze
71
+ LEVELS = Logger::Severity.constants.sort { |a, b| Logger::Severity.const_get(a) <=> Logger::Severity.const_get(b) }.map { |c| c.downcase.to_sym }.freeze
72
72
 
73
73
  # Class methods
74
74
  class << self
@@ -100,7 +100,7 @@ module Aspera
100
100
  JSON.pretty_generate(object) rescue PP.pp(object, +'')
101
101
  when :ruby
102
102
  PP.pp(object, +'')
103
- else Aspera.error_unexpected_value(instance.dump_format){'dump format'}
103
+ else Aspera.error_unexpected_value(instance.dump_format) { 'dump format' }
104
104
  end
105
105
  "#{name.to_s.green}(#{instance.dump_format})#{object.class}=\n#{dump_text}"
106
106
  end
@@ -119,7 +119,7 @@ module Aspera
119
119
  # Returns the last 2 containers (module/class) and method caller
120
120
  def caller_method
121
121
  stack = caller
122
- i = stack.rindex{ |line| line.include?('Logger')}
122
+ i = stack.rindex { |line| line.include?('Logger') }
123
123
  frame = stack[i + 1] if i && stack[i + 1]
124
124
  return '???' unless frame
125
125
  # Extract the "Class::Module::Method" or "Class#method" part
@@ -166,7 +166,7 @@ module Aspera
166
166
  # @return [nil]
167
167
  def formatter=(formatter)
168
168
  if formatter.is_a?(String)
169
- Aspera.assert(FORMATTER_LAMBDAS.key?(formatter.to_sym), type: Error){"Unknown formatter #{formatter}, use one of: #{FORMATTERS.join(', ')}"}
169
+ Aspera.assert(FORMATTER_LAMBDAS.key?(formatter.to_sym), type: Error) { "Unknown formatter #{formatter}, use one of: #{FORMATTERS.join(', ')}" }
170
170
  formatter = FORMATTER_LAMBDAS[formatter.to_sym]
171
171
  elsif !formatter.respond_to?(:call) && !formatter.is_a?(Logger::Formatter)
172
172
  raise Error, 'Formatter must be a String, a Logger::Formatter or a Proc'
@@ -210,7 +210,7 @@ module Aspera
210
210
  end
211
211
  # Use `local2` facility, like other Aspera components
212
212
  @logger = Syslog::Logger.new(@program_name, Syslog::LOG_LOCAL2)
213
- else Aspera.error_unexpected_value(new_log_type){"log type (#{LOG_TYPES.join(', ')})"}
213
+ else Aspera.error_unexpected_value(new_log_type) { "log type (#{LOG_TYPES.join(', ')})" }
214
214
  end
215
215
  @logger.level = current_severity_integer
216
216
  @logger_type = new_log_type
@@ -247,13 +247,13 @@ module Aspera
247
247
  [k, short_levl(k).apply(*v)]
248
248
  end.freeze
249
249
 
250
- DEFAULT_FORMATTER = ->(s, _d, _p, m){"#{LVL_COLOR[s]} #{m}\n"}
250
+ DEFAULT_FORMATTER = ->(s, _d, _p, m) { "#{LVL_COLOR[s]} #{m}\n" }
251
251
 
252
252
  # pre-defined formatters
253
253
  FORMATTER_LAMBDAS = {
254
254
  standard: Logger::Formatter.new,
255
255
  default: DEFAULT_FORMATTER,
256
- caller: ->(s, _d, _p, m){"#{LVL_COLOR[s]} #{Log.caller_method}\n#{m}\n"}
256
+ caller: ->(s, _d, _p, m) { "#{LVL_COLOR[s]} #{Log.caller_method}\n#{m}\n" }
257
257
  }.freeze
258
258
 
259
259
  FORMATTERS = FORMATTER_LAMBDAS.keys
@@ -80,12 +80,12 @@ module Aspera
80
80
  def table(table)
81
81
  # get max width of each columns
82
82
  col_widths = table.transpose.map do |col|
83
- [col.flat_map{ |c| c.to_s.delete('`').split(HTML_BREAK).map(&:size)}.max, COL_WIDTH].min
83
+ [col.flat_map { |c| c.to_s.delete('`').split(HTML_BREAK).map(&:size) }.max, COL_WIDTH].min
84
84
  end
85
85
  headings = table.shift
86
- table.unshift(col_widths.map{ |col_width| '-' * col_width})
86
+ table.unshift(col_widths.map { |col_width| '-' * col_width })
87
87
  table.unshift(headings)
88
- lines = table.map{ |line| "| #{line.map{ |i| i.to_s.gsub('\\', '\\\\').gsub('|', '\|')}.join(' | ')} |\n"}
88
+ lines = table.map { |line| "| #{line.map { |i| i.to_s.gsub('\\', '\\\\').gsub('|', '\|') }.join(' | ')} |\n" }
89
89
  lines[1] = lines[1].tr(' ', '-')
90
90
  return lines.join.chomp
91
91
  end
@@ -94,7 +94,7 @@ module Aspera
94
94
  # @param items [Array<String>] list of items
95
95
  # @return [String] markdown unordered list
96
96
  def list(items)
97
- items.map{ |i| "- #{i}"}.join("\n")
97
+ items.map { |i| "- #{i}" }.join("\n")
98
98
  end
99
99
 
100
100
  # Generate a markdown heading
@@ -110,7 +110,7 @@ module Aspera
110
110
  # @param type [String] admonition type: NOTE, CAUTION, WARNING, IMPORTANT, TIP, INFO
111
111
  # @return [String] markdown admonition block
112
112
  def admonition(lines, type: 'INFO')
113
- "> [!#{type}]\n#{lines.map{ |l| "> #{l}"}.join("\n")}\n\n"
113
+ "> [!#{type}]\n#{lines.map { |l| "> #{l}" }.join("\n")}\n\n"
114
114
  end
115
115
 
116
116
  # Generate a fenced code block
data/lib/aspera/nagios.rb CHANGED
@@ -17,7 +17,7 @@ module Aspera
17
17
  # add methods to add nagios error levels, each take component name and message
18
18
  LEVELS.each_index do |code|
19
19
  name = "#{ADD_PREFIX}#{LEVELS[code]}".to_sym
20
- define_method(name){ |comp, msg| @data.push({code: code, comp: comp, msg: msg})}
20
+ define_method(name) { |comp, msg| @data.push({code: code, comp: comp, msg: msg}) }
21
21
  end
22
22
 
23
23
  class << self
@@ -26,18 +26,18 @@ module Aspera
26
26
  Aspera.assert_type(data, Array)
27
27
  Aspera.assert(!data.empty?, 'data is empty')
28
28
  %w[status component message].each do |c|
29
- Aspera.assert(data.first.key?(c)){"result must have #{c}"}
29
+ Aspera.assert(data.first.key?(c)) { "result must have #{c}" }
30
30
  end
31
- res_errors = data.reject{ |s| s['status'].eql?('ok')}
31
+ res_errors = data.reject { |s| s['status'].eql?('ok') }
32
32
  # keep only errors in case of problem, other ok are assumed so
33
33
  data = res_errors unless res_errors.empty?
34
34
  # first is most critical
35
- data.sort!{ |a, b| LEVELS.index(a['status'].to_sym) <=> LEVELS.index(b['status'].to_sym)}
35
+ data.sort! { |a, b| LEVELS.index(a['status'].to_sym) <=> LEVELS.index(b['status'].to_sym) }
36
36
  # build message: if multiple components: concatenate
37
37
  # message = data.map{|i|"#{i['component']}:#{i['message']}"}.join(', ').gsub("\n",' ')
38
38
  message = data
39
- .group_by{ |d| d['component']}
40
- .map{ |comp, items| "#{comp}:#{items.map{ |d| d['message']}.join(',')}"}
39
+ .group_by { |d| d['component'] }
40
+ .map { |comp, items| "#{comp}:#{items.map { |d| d['message'] }.join(',')}" }
41
41
  .join(', ')
42
42
  .tr("\n", ' ')
43
43
  status = data.first['status'].upcase
@@ -60,7 +60,7 @@ module Aspera
60
60
  remote_time = Time.parse(remote_date)
61
61
  diff_time = (remote_time - Time.now).abs
62
62
  diff_rounded = diff_time.round(-2)
63
- Log.log.debug{"DATE: #{remote_date} #{remote_time} diff=#{diff_rounded}"}
63
+ Log.log.debug { "DATE: #{remote_date} #{remote_time} diff=#{diff_rounded}" }
64
64
  msg = "offset #{diff_rounded} sec"
65
65
  if diff_time >= DATE_CRIT_OFFSET
66
66
  add_critical(component, msg)
@@ -80,7 +80,7 @@ module Aspera
80
80
  # @return [Array] of Hash
81
81
  def status_list
82
82
  Aspera.assert(!@data.empty?, 'missing result')
83
- @data.map{ |i| {'status' => LEVELS[i[:code]].to_s, 'component' => i[:comp], 'message' => i[:msg]}}
83
+ @data.map { |i| {'status' => LEVELS[i[:code]].to_s, 'component' => i[:comp], 'message' => i[:msg]} }
84
84
  end
85
85
  end
86
86
  end
@@ -10,7 +10,7 @@ require 'json'
10
10
  module Aspera
11
11
  class NodeSimulator
12
12
  def initialize
13
- @agent = Agent::Direct.new(management_cb: ->(event){process_event(event)})
13
+ @agent = Agent::Direct.new(management_cb: ->(event) { process_event(event) })
14
14
  @sessions = {}
15
15
  end
16
16
 
@@ -19,7 +19,7 @@ module Aspera
19
19
  end
20
20
 
21
21
  def all_sessions
22
- @agent.sessions.map{ |session| session[:job_id]}.uniq.each.map{ |job_id| job_to_transfer(job_id)}
22
+ @agent.sessions.map { |session| session[:job_id] }.uniq.each.map { |job_id| job_to_transfer(job_id) }
23
23
  end
24
24
 
25
25
  # status: ('waiting', 'partially_completed', 'unknown', 'waiting(read error)',] 'running', 'completed', 'failed'
@@ -131,27 +131,27 @@ module Aspera
131
131
  def process_event(event)
132
132
  # Code duplicated on purpose, as implementation goes, blocks will be replaced
133
133
  case event['Type']
134
- when 'NOP' then Aspera.Log.debug{"event not managed: #{event['Type']}"}
134
+ when 'NOP' then Aspera.Log.debug { "event not managed: #{event['Type']}" }
135
135
  # rubocop:disable Lint/DuplicateBranch
136
- when 'START' then Aspera.Log.debug{"event not managed: #{event['Type']}"}
137
- when 'QUERY' then Aspera.Log.debug{"event not managed: #{event['Type']}"}
138
- when 'QUERYRSP' then Aspera.Log.debug{"event not managed: #{event['Type']}"}
139
- when 'STATS' then Aspera.Log.debug{"event not managed: #{event['Type']}"}
140
- when 'STOP' then Aspera.Log.debug{"event not managed: #{event['Type']}"}
141
- when 'ERROR' then Aspera.Log.debug{"event not managed: #{event['Type']}"}
142
- when 'CANCEL' then Aspera.Log.debug{"event not managed: #{event['Type']}"}
143
- when 'DONE' then Aspera.Log.debug{"event not managed: #{event['Type']}"}
144
- when 'RATE' then Aspera.Log.debug{"event not managed: #{event['Type']}"}
145
- when 'FILEERROR' then Aspera.Log.debug{"event not managed: #{event['Type']}"}
146
- when 'SESSION' then Aspera.Log.debug{"event not managed: #{event['Type']}"}
147
- when 'NOTIFICATION' then Aspera.Log.debug{"event not managed: #{event['Type']}"}
148
- when 'INIT' then Aspera.Log.debug{"event not managed: #{event['Type']}"}
149
- when 'VLINK' then Aspera.Log.debug{"event not managed: #{event['Type']}"}
150
- when 'PUT' then Aspera.Log.debug{"event not managed: #{event['Type']}"}
151
- when 'WRITE' then Aspera.Log.debug{"event not managed: #{event['Type']}"}
152
- when 'CLOSE' then Aspera.Log.debug{"event not managed: #{event['Type']}"}
153
- when 'SKIP' then Aspera.Log.debug{"event not managed: #{event['Type']}"}
154
- when 'ARGSTOP' then Aspera.Log.debug{"event not managed: #{event['Type']}"}
136
+ when 'START' then Aspera.Log.debug { "event not managed: #{event['Type']}" }
137
+ when 'QUERY' then Aspera.Log.debug { "event not managed: #{event['Type']}" }
138
+ when 'QUERYRSP' then Aspera.Log.debug { "event not managed: #{event['Type']}" }
139
+ when 'STATS' then Aspera.Log.debug { "event not managed: #{event['Type']}" }
140
+ when 'STOP' then Aspera.Log.debug { "event not managed: #{event['Type']}" }
141
+ when 'ERROR' then Aspera.Log.debug { "event not managed: #{event['Type']}" }
142
+ when 'CANCEL' then Aspera.Log.debug { "event not managed: #{event['Type']}" }
143
+ when 'DONE' then Aspera.Log.debug { "event not managed: #{event['Type']}" }
144
+ when 'RATE' then Aspera.Log.debug { "event not managed: #{event['Type']}" }
145
+ when 'FILEERROR' then Aspera.Log.debug { "event not managed: #{event['Type']}" }
146
+ when 'SESSION' then Aspera.Log.debug { "event not managed: #{event['Type']}" }
147
+ when 'NOTIFICATION' then Aspera.Log.debug { "event not managed: #{event['Type']}" }
148
+ when 'INIT' then Aspera.Log.debug { "event not managed: #{event['Type']}" }
149
+ when 'VLINK' then Aspera.Log.debug { "event not managed: #{event['Type']}" }
150
+ when 'PUT' then Aspera.Log.debug { "event not managed: #{event['Type']}" }
151
+ when 'WRITE' then Aspera.Log.debug { "event not managed: #{event['Type']}" }
152
+ when 'CLOSE' then Aspera.Log.debug { "event not managed: #{event['Type']}" }
153
+ when 'SKIP' then Aspera.Log.debug { "event not managed: #{event['Type']}" }
154
+ when 'ARGSTOP' then Aspera.Log.debug { "event not managed: #{event['Type']}" }
155
155
  # rubocop:enable Lint/DuplicateBranch
156
156
  else Aspera.error_unreachable_line
157
157
  end
@@ -164,19 +164,25 @@ module Aspera
164
164
  PATH_TRANSFERS = '/ops/transfers'
165
165
  PATH_ONE_TRANSFER = %r{/ops/transfers/(.+)$}
166
166
  PATH_BROWSE = '/files/browse'
167
- # @param app_api [Api::AoC]
168
- # @param app_context [String]
167
+ # @param credentials [Hash]
168
+ # @param simulator [NodeSimulator]
169
169
  def initialize(server, credentials, simulator)
170
170
  super(server)
171
171
  @credentials = credentials
172
172
  @simulator = simulator
173
+ # Resolve once at startup; default to current working directory
174
+ @browse_root = File.realpath(credentials[:browse_root] || Dir.pwd)
173
175
  end
174
176
 
175
177
  require 'json'
176
178
  require 'time'
177
179
 
178
180
  def folder_to_structure(folder_path)
179
- Aspera.assert(Dir.exist?(folder_path)){"Path does not exist or is not a directory: #{folder_path}"}
181
+ # Resolve and confine to browse_root (prevents path traversal via client-supplied path)
182
+ resolved = File.realpath(folder_path)
183
+ Aspera.assert(resolved.start_with?("#{@browse_root}/") || resolved.eql?(@browse_root)) { 'Browse path traversal attempt detected' }
184
+ Aspera.assert(Dir.exist?(resolved)) { "Path does not exist or is not a directory: #{resolved}" }
185
+ folder_path = resolved
180
186
 
181
187
  # Build self structure
182
188
  folder_stat = File.stat(folder_path)
@@ -242,7 +248,7 @@ module Aspera
242
248
  when PATH_BROWSE
243
249
  req = JSON.parse(request.body)
244
250
  # req['count']
245
- set_json_response(request, response, folder_to_structure(req['path']))
251
+ set_json_response(request, response, folder_to_structure(req['path'] || @browse_root))
246
252
  else
247
253
  set_json_response(request, response, [{error: 'Bad request'}], code: 400)
248
254
  end
@@ -332,7 +338,7 @@ module Aspera
332
338
  response.status = code
333
339
  response['Content-Type'] = Mime::JSON
334
340
  response.body = json.to_json
335
- Log.log.trace1{Log.obj_dump("response for #{request.request_method} #{request.path}", json)}
341
+ Log.log.trace1 { Log.obj_dump("response for #{request.request_method} #{request.path}", json) }
336
342
  end
337
343
  end
338
344
  end
@@ -51,7 +51,7 @@ module Aspera
51
51
  # @return [Net::HTTPResponse] raw HTTP response with token
52
52
  # @raise [RestCallError] if not 2XX code
53
53
  def create_token_call(creation_params)
54
- Log.log.debug{'Generating a new token'.bg_green}
54
+ Log.log.debug { 'Generating a new token'.bg_green }
55
55
  return @api.create(@path_token, nil, query: creation_params, ret: :resp) if @use_query
56
56
  return @api.create(@path_token, creation_params, content_type: Mime::WWW, ret: :resp)
57
57
  end
@@ -87,7 +87,7 @@ module Aspera
87
87
  # `direct` agent is equipped with refresh code
88
88
  # an API was already called, but failed, we need to regenerate or refresh
89
89
  if refresh || token_info[:expired]
90
- Log.log.trace1{"refresh: #{refresh} expired: #{token_info[:expired]}"}
90
+ Log.log.trace1 { "refresh: #{refresh} expired: #{token_info[:expired]}" }
91
91
  refresh_token = nil
92
92
  if token_data.key?('refresh_token') && !token_data['refresh_token'].eql?('not_supported')
93
93
  # save possible refresh token, before deleting the cache
@@ -99,7 +99,6 @@ module Aspera
99
99
  # lets try the existing refresh token
100
100
  # NOTE: AoC admin token has no refresh, and lives by default 1800secs
101
101
  if !refresh_token.nil?
102
- Log.log.debug{"refresh token=[#{refresh_token}]"}
103
102
  begin
104
103
  http = create_token_call(base_params(add_secret: true).merge(grant_type: 'refresh_token', refresh_token: refresh_token))
105
104
  # Save only if success
@@ -108,7 +107,7 @@ module Aspera
108
107
  Factory.instance.persist_mgr.put(@token_cache_id, json_data)
109
108
  rescue StandardError => e
110
109
  # Refresh token can fail.
111
- Log.log.warn{"Refresh failed: #{e}"}
110
+ Log.log.warn { "Refresh failed: #{e}" }
112
111
  end
113
112
  end
114
113
  end
@@ -122,7 +121,7 @@ module Aspera
122
121
  token_data = JSON.parse(json_data)
123
122
  Factory.instance.persist_mgr.put(@token_cache_id, json_data)
124
123
  end
125
- Aspera.assert(token_data.key?(@token_field)){"API error: No such field in answer: #{@token_field}"} unless token_data.nil?
124
+ Aspera.assert(token_data.key?(@token_field)) { "API error: No such field in answer: #{@token_field}" } unless token_data.nil?
126
125
  # ok we shall have a token here
127
126
  return token_data[@token_field]
128
127
  end
@@ -19,13 +19,13 @@ module Aspera
19
19
  Aspera.assert(username, 'Provide --password (cookie) on first use, or --username for cache lookup', type: ParameterError)
20
20
  super(**base_params, cache_ids: [username])
21
21
  else
22
- cookies = cookie.split('; ').map{ |p| p.split('=', 2)}.to_h
22
+ cookies = cookie.split('; ').map { |p| p.split('=', 2) }.to_h
23
23
  Aspera.assert(cookies.key?('aoc.token'), '--password cookie does not contain aoc.token', type: ParameterError)
24
24
  token = cookies['aoc.token']
25
25
  decoded = Factory.instance.decode_token(token)
26
- Aspera.assert_type(decoded, Hash){'Boot: token is not a decodable JWT'}
26
+ Aspera.assert_type(decoded, Hash) { 'Boot: token is not a decodable JWT' }
27
27
  sub = decoded['sub']
28
- Aspera.assert(username.nil? || username.eql?(sub)){"Boot: --username #{username} does not match token subject #{sub}"}
28
+ Aspera.assert(username.nil? || username.eql?(sub)) { "Boot: --username #{username} does not match token subject #{sub}" }
29
29
  super(**base_params, cache_ids: [sub])
30
30
  token_data = {Factory::TOKEN_FIELD => token}
31
31
  token_data['refresh_token'] = cookies['aoc.refresh'] if cookies.key?('aoc.refresh')
@@ -41,7 +41,7 @@ module Aspera
41
41
  # @param authorization [String] The authorization header value
42
42
  # @return [String] The bearer token without the scheme prefix
43
43
  def bearer_token(authorization)
44
- Aspera.assert(bearer_auth?(authorization)){'not a bearer token, wrong prefix scheme'}
44
+ Aspera.assert(bearer_auth?(authorization)) { 'not a bearer token, wrong prefix scheme' }
45
45
  return authorization.delete_prefix(SPACE_BEARER_AUTH_SCHEME)
46
46
  end
47
47
 
@@ -178,7 +178,7 @@ module Aspera
178
178
  def register_token_creator(creator_class)
179
179
  Aspera.assert_type(creator_class, Class)
180
180
  id = Factory.class_to_id(creator_class)
181
- Log.log.debug{"registering creator for #{id}"}
181
+ Log.log.debug { "registering creator for #{id}" }
182
182
  @token_type_classes[id] = creator_class
183
183
  nil
184
184
  end
@@ -189,12 +189,12 @@ module Aspera
189
189
  def create(**parameters)
190
190
  Aspera.assert_type(parameters, Hash)
191
191
  id = parameters[:grant_method]
192
- Aspera.assert(@token_type_classes.key?(id)){"token grant method unknown: '#{id}'"}
193
- create_parameters = parameters.reject{ |k, _v| k.eql?(:grant_method)}
192
+ Aspera.assert(@token_type_classes.key?(id)) { "token grant method unknown: '#{id}'" }
193
+ create_parameters = parameters.reject { |k, _v| k.eql?(:grant_method) }
194
194
  @token_type_classes[id].new(**create_parameters)
195
195
  end
196
196
  end
197
197
  # JSON Web Signature (JWS) compact serialization: https://datatracker.ietf.org/doc/html/rfc7515
198
- 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
198
+ 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
199
199
  end
200
200
  end
@@ -36,10 +36,10 @@ module Aspera
36
36
  cache_ids: [],
37
37
  **base_params
38
38
  )
39
- Aspera.assert_type(private_key_obj, OpenSSL::PKey::RSA){'private_key_obj'}
40
- Aspera.assert_type(payload, Hash){'payload'}
41
- Aspera.assert_type(headers, Hash){'headers'}
42
- Aspera.assert_type(cache_ids, Array){'cache ids'}
39
+ Aspera.assert_type(private_key_obj, OpenSSL::PKey::RSA) { 'private_key_obj' }
40
+ Aspera.assert_type(payload, Hash) { 'payload' }
41
+ Aspera.assert_type(headers, Hash) { 'headers' }
42
+ Aspera.assert_type(cache_ids, Array) { 'cache ids' }
43
43
  new_cache_ids = cache_ids.clone.push(payload[:sub])
44
44
  super(**base_params, cache_ids: new_cache_ids)
45
45
  @private_key_obj = private_key_obj
@@ -50,7 +50,7 @@ module Aspera
50
50
  def create_token
51
51
  require 'jwt'
52
52
  seconds_since_epoch = Time.now.to_i
53
- Log.log.debug{"seconds_since_epoch=#{seconds_since_epoch}"}
53
+ Log.log.debug { "seconds_since_epoch=#{seconds_since_epoch}" }
54
54
  jwt_payload = {
55
55
  exp: seconds_since_epoch + OAuth::Factory.instance.parameters[:jwt_expiry_offset_sec], # expiration time
56
56
  nbf: seconds_since_epoch - OAuth::Factory.instance.parameters[:jwt_accepted_offset_sec], # not before
@@ -58,9 +58,7 @@ module Aspera
58
58
  jti: SecureRandom.uuid # JWT id
59
59
  }.merge(@additional_payload)
60
60
  Log.dump(:jwt_payload, jwt_payload)
61
- Log.log.debug{"private=[#{@private_key_obj}]"}
62
61
  assertion = JWT.encode(jwt_payload, @private_key_obj, 'RS256', @headers)
63
- Log.log.debug{"assertion=[#{assertion}]"}
64
62
  return create_token_call(base_params.merge(grant_type: GRANT_TYPE, assertion: assertion))
65
63
  end
66
64
  end
@@ -22,9 +22,9 @@ module Aspera
22
22
  @redirect_uri = redirect_uri
23
23
  @path_authorize = path_authorize
24
24
  uri = URI.parse(@redirect_uri)
25
- Aspera.assert_values(uri.scheme, %w[http https]){'redirect_uri scheme must be http or https'}
25
+ Aspera.assert_values(uri.scheme, %w[http https]) { 'redirect_uri scheme must be http or https' }
26
26
  Aspera.assert(!uri.port.nil?, 'redirect_uri must have a port')
27
- # TODO: we could check that host is localhost or local address, as we are going to listen locally
27
+ Aspera.assert(%w[127.0.0.1 ::1 localhost].include?(uri.host)) { 'redirect_uri host must be localhost or loopback address' }
28
28
  end
29
29
 
30
30
  def create_token
@@ -35,14 +35,14 @@ module Aspera
35
35
  base_params.merge(response_type: 'code', redirect_uri: @redirect_uri, state: random_state)
36
36
  )
37
37
  # here, we need a human to authorize on a web page
38
- Log.log.debug{"login_page_url=#{login_page_url}"}
38
+ Log.log.debug { "login_page_url=#{login_page_url}" }
39
39
  # start a web server to receive request code
40
40
  web_server = WebAuth.new(@redirect_uri, self.class.additional_info)
41
41
  # start browser on login page
42
42
  Environment.instance.open_uri(login_page_url)
43
43
  # wait for code in request
44
44
  received_params = web_server.received_request
45
- Aspera.assert(random_state.eql?(received_params['state'])){'wrong received state'}
45
+ Aspera.assert(random_state.eql?(received_params['state'])) { 'wrong received state' }
46
46
  # exchange code for token
47
47
  return create_token_call(base_params(add_secret: true).merge(
48
48
  grant_type: 'authorization_code',
@@ -8,9 +8,9 @@ module Aspera
8
8
  # Persist data on file system
9
9
  class PersistencyActionOnce
10
10
  DELETE_DEFAULT = lambda(&:empty?)
11
- PARSE_DEFAULT = lambda{ |t| JSON.parse(t)}
12
- FORMAT_DEFAULT = lambda{ |h| JSON.generate(h)}
13
- MERGE_DEFAULT = lambda{ |current, file| current.concat(file).uniq rescue current}
11
+ PARSE_DEFAULT = lambda { |t| JSON.parse(t) }
12
+ FORMAT_DEFAULT = lambda { |h| JSON.generate(h) }
13
+ MERGE_DEFAULT = lambda { |current, file| current.concat(file).uniq rescue current }
14
14
  MANAGER_METHODS = %i[get put delete]
15
15
  private_constant :DELETE_DEFAULT, :PARSE_DEFAULT, :FORMAT_DEFAULT, :MERGE_DEFAULT, :MANAGER_METHODS
16
16
 
@@ -22,7 +22,7 @@ module Aspera
22
22
  # @param format [Proc] Optional dump method (default to JSON)
23
23
  # @param merge [Proc] 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
- Aspera.assert(MANAGER_METHODS.all?{ |i| manager.respond_to?(i)}){"Manager must answer to #{MANAGER_METHODS}"}
25
+ Aspera.assert(MANAGER_METHODS.all? { |i| manager.respond_to?(i) }) { "Manager must answer to #{MANAGER_METHODS}" }
26
26
  Aspera.assert(!data.nil?, 'data must not be nil')
27
27
  Aspera.assert_type(id, String)
28
28
  Aspera.assert(!id.empty?, 'id must not be empty')
@@ -15,19 +15,19 @@ module Aspera
15
15
  def initialize(folder)
16
16
  @cache = {}
17
17
  @folder = folder
18
- Log.log.debug{"persistency folder: #{@folder}"}
18
+ Log.log.debug { "persistency folder: #{@folder}" }
19
19
  end
20
20
 
21
21
  # Get value of persisted item
22
22
  # @param object_id [String] Identifier of persisted item
23
23
  # @return [String, nil] Persisted value, or nil if not found
24
24
  def get(object_id)
25
- Log.log.debug{"persistency get: #{object_id}"}
25
+ Log.log.debug { "persistency get: #{object_id}" }
26
26
  if @cache.key?(object_id)
27
27
  Log.log.debug('got from memory cache')
28
28
  else
29
29
  persist_filepath = id_to_filepath(object_id)
30
- Log.log.debug{"persistency = #{persist_filepath}"}
30
+ Log.log.debug { "persistency = #{persist_filepath}" }
31
31
  if File.exist?(persist_filepath)
32
32
  Log.log.debug('got from file cache')
33
33
  @cache[object_id] = File.read(persist_filepath)
@@ -43,7 +43,7 @@ module Aspera
43
43
  def put(object_id, value)
44
44
  Aspera.assert_type(value, String)
45
45
  persist_filepath = id_to_filepath(object_id)
46
- Log.log.debug{"persistency saving: #{persist_filepath}"}
46
+ Log.log.debug { "persistency saving: #{persist_filepath}" }
47
47
  FileUtils.rm_f(persist_filepath)
48
48
  File.write(persist_filepath, value)
49
49
  Environment.restrict_file_access(persist_filepath)
@@ -55,7 +55,7 @@ module Aspera
55
55
  # @param object_id [String] Identifier of persisted item
56
56
  def delete(object_id)
57
57
  persist_filepath = id_to_filepath(object_id)
58
- Log.log.debug{"persistency deleting: #{persist_filepath}"}
58
+ Log.log.debug { "persistency deleting: #{persist_filepath}" }
59
59
  FileUtils.rm_f(persist_filepath)
60
60
  @cache.delete(object_id)
61
61
  end
@@ -68,11 +68,11 @@ module Aspera
68
68
  garbage_files = current_files(persist_category)
69
69
  if !max_age_seconds.nil?
70
70
  current_time = Time.now
71
- garbage_files.select!{ |filepath| (current_time - File.stat(filepath).mtime).to_i > max_age_seconds}
71
+ garbage_files.select! { |filepath| (current_time - File.stat(filepath).mtime).to_i > max_age_seconds }
72
72
  end
73
73
  garbage_files.each do |filepath|
74
74
  File.delete(filepath)
75
- Log.log.debug{"persistency deleted expired: #{filepath}"}
75
+ Log.log.debug { "persistency deleted expired: #{filepath}" }
76
76
  end
77
77
  @cache.clear
78
78
  return garbage_files
@@ -86,7 +86,7 @@ module Aspera
86
86
  end
87
87
 
88
88
  def current_items(persist_category)
89
- current_files(persist_category).to_h{ |i| [File.basename(i, FILE_SUFFIX), File.read(i)]}
89
+ current_files(persist_category).to_h { |i| [File.basename(i, FILE_SUFFIX), File.read(i)] }
90
90
  end
91
91
 
92
92
  private
@@ -81,15 +81,15 @@ module Aspera
81
81
  # @return [Symbol] file type, one of enum CONVERSION_TYPES
82
82
  # @raise [RuntimeError] if no conversion type found
83
83
  def conversion_type(filepath, mimetype)
84
- Log.log.debug{"conversion_type(#{filepath},mime=#{mimetype},magic=#{@use_mimemagic})"}
84
+ Log.log.debug { "conversion_type(#{filepath},mime=#{mimetype},magic=#{@use_mimemagic})" }
85
85
  # Default type or empty means no type
86
86
  mimetype = TYPE_NOT_FOUND if mimetype.nil? || (mimetype.is_a?(String) && mimetype.empty?)
87
87
  mimetype = Marcel::MimeType.for(Pathname.new(filepath), name: File.basename(filepath), declared_type: mimetype)
88
88
  mimetype = 'text/plain' if mimetype.eql?(TYPE_NOT_FOUND) && ascii_text_file?(filepath)
89
- Aspera.assert(!mimetype.eql?(TYPE_NOT_FOUND)){"no MIME type found for #{File.basename(filepath)}"}
89
+ Aspera.assert(!mimetype.eql?(TYPE_NOT_FOUND)) { "no MIME type found for #{File.basename(filepath)}" }
90
90
  conversion_type = mime_to_type(mimetype)
91
- Aspera.assert(!conversion_type.nil?){"no conversion type found for #{File.basename(filepath)}"}
92
- Log.log.trace1{"conversion_type(#{File.basename(filepath)}): #{conversion_type.class.name} [#{conversion_type}]"}
91
+ Aspera.assert(!conversion_type.nil?) { "no conversion type found for #{File.basename(filepath)}" }
92
+ Log.log.trace1 { "conversion_type(#{File.basename(filepath)}): #{conversion_type.class.name} [#{conversion_type}]" }
93
93
  return conversion_type
94
94
  end
95
95