aspera-cli 4.18.0 → 4.19.0

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 (60) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/CHANGELOG.md +23 -0
  4. data/CONTRIBUTING.md +5 -12
  5. data/README.md +152 -84
  6. data/examples/build_exec +85 -0
  7. data/examples/build_package.sh +28 -0
  8. data/lib/aspera/agent/alpha.rb +4 -4
  9. data/lib/aspera/agent/base.rb +2 -0
  10. data/lib/aspera/agent/connect.rb +3 -4
  11. data/lib/aspera/agent/direct.rb +108 -104
  12. data/lib/aspera/agent/httpgw.rb +1 -1
  13. data/lib/aspera/api/aoc.rb +2 -2
  14. data/lib/aspera/api/httpgw.rb +95 -57
  15. data/lib/aspera/api/node.rb +110 -77
  16. data/lib/aspera/ascp/installation.rb +47 -32
  17. data/lib/aspera/ascp/management.rb +4 -1
  18. data/lib/aspera/ascp/products.rb +2 -8
  19. data/lib/aspera/cli/extended_value.rb +27 -14
  20. data/lib/aspera/cli/formatter.rb +35 -28
  21. data/lib/aspera/cli/main.rb +11 -11
  22. data/lib/aspera/cli/manager.rb +109 -94
  23. data/lib/aspera/cli/plugin.rb +4 -7
  24. data/lib/aspera/cli/plugin_factory.rb +10 -1
  25. data/lib/aspera/cli/plugins/aoc.rb +15 -14
  26. data/lib/aspera/cli/plugins/config.rb +35 -29
  27. data/lib/aspera/cli/plugins/faspex.rb +5 -4
  28. data/lib/aspera/cli/plugins/faspex5.rb +16 -13
  29. data/lib/aspera/cli/plugins/node.rb +50 -41
  30. data/lib/aspera/cli/plugins/orchestrator.rb +3 -2
  31. data/lib/aspera/cli/plugins/preview.rb +1 -1
  32. data/lib/aspera/cli/plugins/server.rb +2 -2
  33. data/lib/aspera/cli/plugins/shares.rb +11 -7
  34. data/lib/aspera/cli/special_values.rb +13 -0
  35. data/lib/aspera/cli/sync_actions.rb +73 -32
  36. data/lib/aspera/cli/transfer_agent.rb +3 -2
  37. data/lib/aspera/cli/transfer_progress.rb +1 -1
  38. data/lib/aspera/cli/version.rb +1 -1
  39. data/lib/aspera/environment.rb +100 -7
  40. data/lib/aspera/faspex_gw.rb +1 -1
  41. data/lib/aspera/keychain/encrypted_hash.rb +2 -0
  42. data/lib/aspera/log.rb +1 -0
  43. data/lib/aspera/node_simulator.rb +1 -1
  44. data/lib/aspera/oauth/jwt.rb +1 -1
  45. data/lib/aspera/oauth/url_json.rb +2 -0
  46. data/lib/aspera/oauth/web.rb +7 -6
  47. data/lib/aspera/rest.rb +46 -15
  48. data/lib/aspera/secret_hider.rb +3 -2
  49. data/lib/aspera/ssh.rb +1 -1
  50. data/lib/aspera/transfer/faux_file.rb +7 -5
  51. data/lib/aspera/transfer/parameters.rb +27 -19
  52. data/lib/aspera/transfer/spec.rb +8 -10
  53. data/lib/aspera/transfer/sync.rb +52 -47
  54. data/lib/aspera/web_auth.rb +0 -1
  55. data/lib/aspera/web_server_simple.rb +24 -13
  56. data.tar.gz.sig +0 -0
  57. metadata +5 -4
  58. metadata.gz.sig +0 -0
  59. data/examples/rubyc +0 -24
  60. data/lib/aspera/open_application.rb +0 -69
@@ -7,7 +7,10 @@ module Aspera
7
7
  module Plugins
8
8
  # Plugin for Aspera Shares v1
9
9
  class Shares < Cli::BasicAuthPlugin
10
- NODE_API_PREFIX = 'node_api'
10
+ # path for node API after base url
11
+ NODE_API_PATH = 'node_api'
12
+ # path for node admin after base url
13
+ ADMIN_API_PATH = 'api/v1'
11
14
  class << self
12
15
  def detect(address_or_url)
13
16
  address_or_url = "https://#{address_or_url}" unless address_or_url.match?(%r{^[a-z]{1,6}://})
@@ -16,7 +19,7 @@ module Aspera
16
19
  begin
17
20
  # shall fail: shares requires auth, but we check error message
18
21
  # TODO: use ping instead ?
19
- api.read("#{NODE_API_PREFIX}/app")
22
+ api.read("#{NODE_API_PATH}/app")
20
23
  rescue RestCallError => e
21
24
  if e.response.code.to_s.eql?('401') && e.response.body.eql?('{"error":{"user_message":"API user authentication failed"}}')
22
25
  found = true
@@ -65,7 +68,7 @@ module Aspera
65
68
  nagios = Nagios.new
66
69
  begin
67
70
  res = Rest
68
- .new(base_url: "#{options.get_option(:url, mandatory: true)}/#{NODE_API_PREFIX}")
71
+ .new(base_url: "#{options.get_option(:url, mandatory: true)}/#{NODE_API_PATH}")
69
72
  .call(
70
73
  operation: 'GET',
71
74
  subpath: 'ping',
@@ -77,13 +80,13 @@ module Aspera
77
80
  end
78
81
  return nagios.result
79
82
  when :repository, :files
80
- api_shares_node = basic_auth_api(NODE_API_PREFIX)
83
+ api_shares_node = basic_auth_api(NODE_API_PATH)
81
84
  repo_command = options.get_next_command(Node::COMMANDS_SHARES)
82
85
  return Node
83
86
  .new(**init_params, api: api_shares_node)
84
87
  .execute_action(repo_command)
85
88
  when :admin
86
- api_shares_admin = basic_auth_api('api/v1')
89
+ api_shares_admin = basic_auth_api(ADMIN_API_PATH)
87
90
  admin_command = options.get_next_command(%i[node share transfer_settings user group].freeze)
88
91
  case admin_command
89
92
  when :node
@@ -92,8 +95,9 @@ module Aspera
92
95
  share_command = options.get_next_command(%i[user_permissions group_permissions].concat(Plugin::ALL_OPS))
93
96
  case share_command
94
97
  when *Plugin::ALL_OPS
95
- return entity_command(share_command, api_shares_admin, 'data/shares')
96
- # return {type: :object_list, data: all_shares, fields: %w[id name status status_message]}
98
+ return entity_command(
99
+ share_command, api_shares_admin, 'data/shares',
100
+ display_fields: %w[id name node_id directory percent_free])
97
101
  when :user_permissions, :group_permissions
98
102
  share_id = instance_identifier
99
103
  return entity_action(api_shares_admin, "data/shares/#{share_id}/#{share_command}")
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aspera
4
+ module Cli
5
+ # base class for plugins modules
6
+ module SpecialValues
7
+ # special values
8
+ INIT = 'INIT'
9
+ ALL = 'ALL'
10
+ DEF = 'DEF'
11
+ end
12
+ end
13
+ end
@@ -7,11 +7,30 @@ module Aspera
7
7
  module Cli
8
8
  # Module for sync actions
9
9
  module SyncActions
10
- SIMPLE_ARGUMENTS_SYNC = {
11
- direction: Transfer::Sync::DIRECTIONS,
12
- local_dir: String,
13
- remote_dir: String
14
- }.stringify_keys.freeze
10
+ # optional simple command line arguments for sync
11
+ # in Array to keep option order
12
+ # conf: key in option --conf
13
+ # args: key for command line args
14
+ # values: possible values for argument
15
+ # type: type for validation
16
+ SYNC_ARGUMENTS_INFO = [
17
+ {
18
+ conf: 'direction',
19
+ args: 'direction',
20
+ values: Transfer::Sync::DIRECTIONS
21
+ }, {
22
+ conf: 'remote.path',
23
+ args: 'remote_dir',
24
+ type: String
25
+ }, {
26
+ conf: 'local.path',
27
+ args: 'local_dir',
28
+ type: String
29
+ }
30
+ ].freeze
31
+ # name of minimal arguments required, also used to generate a session name
32
+ SYNC_SIMPLE_ARGS = SYNC_ARGUMENTS_INFO.map{|i|i[:conf]}.freeze
33
+ private_constant :SYNC_ARGUMENTS_INFO, :SYNC_SIMPLE_ARGS
15
34
 
16
35
  class << self
17
36
  def declare_options(options)
@@ -19,45 +38,67 @@ module Aspera
19
38
  end
20
39
  end
21
40
 
41
+ # Read command line arguments (3) and converts to sync_info format
42
+ def sync_args_to_params(async_params)
43
+ # sync session parameters can be provided on command line instead of sync_info
44
+ arguments = {}
45
+ SYNC_ARGUMENTS_INFO.each do |info|
46
+ value = options.get_next_argument(
47
+ info[:conf],
48
+ mandatory: false,
49
+ validation: info[:type],
50
+ accept_list: info[:values])
51
+ break if value.nil?
52
+ arguments[info[:conf]] = value.to_s
53
+ end
54
+ Log.log.debug{Log.dump('arguments', arguments)}
55
+ raise Cli::BadArgument, "Provide 0 or 3 arguments, not #{arguments.keys.length} for: #{SYNC_SIMPLE_ARGS.join(', ')}" unless
56
+ [0, 3].include?(arguments.keys.length)
57
+ if !arguments.empty?
58
+ session_info = async_params
59
+ param_path = :conf
60
+ if async_params.key?('sessions') || async_params.key?('instance')
61
+ async_params['sessions'] ||= [{}]
62
+ Aspera.assert(async_params['sessions'].length == 1){'Only one session is supported with arguments'}
63
+ session_info = async_params['sessions'][0]
64
+ param_path = :args
65
+ end
66
+ SYNC_ARGUMENTS_INFO.each do |info|
67
+ key_path = info[param_path].split('.')
68
+ hash_for_key = session_info
69
+ if key_path.length > 1
70
+ first = key_path.shift
71
+ async_params[first] ||= {}
72
+ hash_for_key = async_params[first]
73
+ end
74
+ raise "Parameter #{info[:conf]} is also set in sync_info, remove from sync_info" if hash_for_key.key?(key_path.last)
75
+ hash_for_key[key_path.last] = arguments[info[:conf]]
76
+ end
77
+ if !session_info.key?('name')
78
+ # if no name is specified, generate one from simple arguments
79
+ session_info['name'] = SYNC_SIMPLE_ARGS.map do |arg_name|
80
+ arguments[arg_name]&.gsub(/[^a-zA-Z0-9]/, '')
81
+ end.compact.reject(&:empty?).join('_')
82
+ end
83
+ end
84
+ end
85
+
22
86
  def execute_sync_action(&block)
23
87
  Aspera.assert(block){'No block given'}
24
88
  command = options.get_next_command(%i[start admin])
25
89
  # try to get 3 arguments as simple arguments
26
90
  case command
27
91
  when :start
28
- simple_session_args = {}
29
- SIMPLE_ARGUMENTS_SYNC.each do |arg, check|
30
- value = options.get_next_argument(
31
- arg,
32
- type: check.is_a?(Class) ? check : nil,
33
- expected: check.is_a?(Class) ? :single : check,
34
- mandatory: false)
35
- break if value.nil?
36
- simple_session_args[arg] = value.to_s
37
- end
38
- async_params = nil
39
- if simple_session_args.empty?
40
- async_params = options.get_option(:sync_info, mandatory: true)
41
- else
42
- raise Cli::BadArgument,
43
- "Provide zero or 3 arguments: #{SIMPLE_ARGUMENTS_SYNC.keys.join(',')}" unless simple_session_args.keys.sort == SIMPLE_ARGUMENTS_SYNC.keys.sort
44
- async_params = options.get_option(
45
- :sync_info,
46
- mandatory: false,
47
- default: {'sessions' => [{'name' => File.basename(simple_session_args['local_dir'])}]})
48
- Aspera.assert_type(async_params, Hash){'sync_info'}
49
- Aspera.assert_type(async_params['sessions'], Array){'sync_info[sessions]'}
50
- Aspera.assert_type(async_params['sessions'].first, Hash){'sync_info[sessions][0]'}
51
- async_params['sessions'].first.merge!(simple_session_args)
52
- end
53
- Log.log.debug{Log.dump('async_params', async_params)}
92
+ # possibilities are:
93
+ async_params = options.get_option(:sync_info, default: {})
94
+ sync_args_to_params(async_params)
54
95
  Transfer::Sync.start(async_params, &block)
55
96
  return Main.result_success
56
97
  when :admin
57
98
  command2 = options.get_next_command([:status])
58
99
  case command2
59
100
  when :status
60
- sync_session_name = options.get_next_argument('name of sync session', mandatory: false, type: String)
101
+ sync_session_name = options.get_next_argument('name of sync session', mandatory: false, validation: String)
61
102
  async_params = options.get_option(:sync_info, mandatory: true)
62
103
  return {type: :single_object, data: Transfer::Sync.admin_status(async_params, sync_session_name)}
63
104
  end
@@ -116,6 +116,7 @@ module Aspera
116
116
  # by default do not display ascp native progress bar
117
117
  agent_options[:quiet] = true unless agent_options.key?(:quiet)
118
118
  agent_options[:check_ignore_cb] = ->(host, port){@config.ignore_cert?(host, port)}
119
+ # JRuby
119
120
  agent_options[:trusted_certs] = @config.trusted_cert_locations unless agent_options.key?(:trusted_certs)
120
121
  when :httpgw
121
122
  unless agent_options.key?(:url) || @httpgw_url_lambda.nil?
@@ -175,7 +176,7 @@ module Aspera
175
176
  when nil, FILE_LIST_FROM_ARGS
176
177
  Log.log.debug('getting file list as parameters')
177
178
  # get remaining arguments
178
- file_list = @opt_mgr.get_next_argument('source file list', expected: :multiple)
179
+ file_list = @opt_mgr.get_next_argument('source file list', multiple: true)
179
180
  raise Cli::BadArgument, 'specify at least one file on command line or use ' \
180
181
  "--sources=#{FILE_LIST_FROM_TRANSFER_SPEC} to use transfer spec" if !file_list.is_a?(Array) || file_list.empty?
181
182
  when FILE_LIST_FROM_TRANSFER_SPEC
@@ -244,7 +245,7 @@ module Aspera
244
245
  updated_ts(transfer_spec)
245
246
  # if TS from app has content_protection (e.g. F5), that means content is protected: ask password if not provided
246
247
  if transfer_spec['content_protection'].eql?('decrypt') && !transfer_spec.key?('content_protection_password')
247
- transfer_spec['content_protection_password'] = @opt_mgr.prompt_user_input('content protection password', true)
248
+ transfer_spec['content_protection_password'] = @opt_mgr.prompt_user_input('content protection password', sensitive: true)
248
249
  end
249
250
  # create transfer agent
250
251
  agent_instance.start_transfer(transfer_spec, token_regenerator: rest_token)
@@ -25,7 +25,7 @@ module Aspera
25
25
  end
26
26
 
27
27
  def event(session_id:, type:, info: nil)
28
- Log.log.debug{"progress: #{type} #{session_id} #{info}"}
28
+ Log.log.trace1{"progress: #{type} #{session_id} #{info}"}
29
29
  Aspera.assert(!session_id.nil? || type.eql?(:pre_start)){'session_id is nil'}
30
30
  return if @completed
31
31
  if @progress_bar.nil?
@@ -4,6 +4,6 @@ module Aspera
4
4
  module Cli
5
5
  # for beta add extension : .beta1
6
6
  # for dev version add extension : .pre
7
- VERSION = '4.18.0'
7
+ VERSION = '4.19.0'
8
8
  end
9
9
  end
@@ -4,17 +4,22 @@
4
4
  require 'aspera/log'
5
5
  require 'aspera/assert'
6
6
  require 'rbconfig'
7
+ require 'singleton'
8
+ require 'English'
7
9
 
8
10
  # cspell:words MEBI mswin bccwin
9
11
 
10
12
  module Aspera
11
13
  # detect OS, architecture, and specific stuff
12
14
  class Environment
15
+ include Singleton
16
+ USER_INTERFACES = %i[text graphical].freeze
17
+
13
18
  OS_WINDOWS = :windows
14
- OS_X = :osx
19
+ OS_MACOS = :osx
15
20
  OS_LINUX = :linux
16
21
  OS_AIX = :aix
17
- OS_LIST = [OS_WINDOWS, OS_X, OS_LINUX, OS_AIX].freeze
22
+ OS_LIST = [OS_WINDOWS, OS_MACOS, OS_LINUX, OS_AIX].freeze
18
23
  CPU_X86_64 = :x86_64
19
24
  CPU_ARM64 = :arm64
20
25
  CPU_PPC64 = :ppc64
@@ -27,7 +32,6 @@ module Aspera
27
32
  BYTES_PER_MEBIBIT = MEBI / BITS_PER_BYTE
28
33
 
29
34
  class << self
30
- @terminal_supports_unicode = nil
31
35
  def ruby_version
32
36
  return RbConfig::CONFIG['RUBY_PROGRAM_VERSION']
33
37
  end
@@ -37,7 +41,7 @@ module Aspera
37
41
  when /mswin/, /msys/, /mingw/, /cygwin/, /bccwin/, /wince/, /emc/
38
42
  return OS_WINDOWS
39
43
  when /darwin/, /mac os/
40
- return OS_X
44
+ return OS_MACOS
41
45
  when /linux/
42
46
  return OS_LINUX
43
47
  when /aix/
@@ -62,10 +66,13 @@ module Aspera
62
66
  raise "Unknown CPU: #{RbConfig::CONFIG['host_cpu']}"
63
67
  end
64
68
 
69
+ # normalized architecture name
70
+ # see constants: OS_* and CPU_*
65
71
  def architecture
66
72
  return "#{os}-#{cpu}"
67
73
  end
68
74
 
75
+ # executable file extension for current OS
69
76
  def exe_extension
70
77
  return '.exe' if os.eql?(OS_WINDOWS)
71
78
  return ''
@@ -79,6 +86,7 @@ module Aspera
79
86
  Log.log.debug{"Windows: set HOME to USERPROFILE: #{Dir.home}"}
80
87
  end
81
88
 
89
+ # empty variable binding for secure eval
82
90
  def empty_binding
83
91
  return Kernel.binding
84
92
  end
@@ -88,7 +96,29 @@ module Aspera
88
96
  Kernel.send('lave'.reverse, code, empty_binding, file, line)
89
97
  end
90
98
 
91
- # value is provided in block
99
+ # start process in background, or raise exception
100
+ # caller can call Process.wait on returned value
101
+ def secure_spawn(env:, exec:, args:, log_only: false)
102
+ Log.log.debug do
103
+ [
104
+ 'execute:'.red,
105
+ env.map{|k, v| "#{k}=#{Shellwords.shellescape(v)}"},
106
+ Shellwords.shellescape(exec),
107
+ args.map{|a|Shellwords.shellescape(a)}
108
+ ].flatten.join(' ')
109
+ end
110
+ return if log_only
111
+ # start ascp in separate process
112
+ ascp_pid = Process.spawn(env, [exec, exec], *args, close_others: true)
113
+ Log.log.debug{"pid: #{ascp_pid}"}
114
+ return ascp_pid
115
+ end
116
+
117
+ # Write content to a file, with restricted access
118
+ # @param path [String] the file path
119
+ # @param force [Boolean] if true, overwrite the file
120
+ # @param mode [Integer] the file mode (permissions)
121
+ # @block [Proc] return the content to write to the file
92
122
  def write_file_restricted(path, force: false, mode: nil)
93
123
  Aspera.assert(block_given?, exception_class: Aspera::InternalError)
94
124
  if force || !File.exist?(path)
@@ -101,6 +131,7 @@ module Aspera
101
131
  return path
102
132
  end
103
133
 
134
+ # restrict access to a file or folder to user only
104
135
  def restrict_file_access(path, mode: nil)
105
136
  if mode.nil?
106
137
  # or FileUtils ?
@@ -117,15 +148,77 @@ module Aspera
117
148
  Log.log.warn(e.message)
118
149
  end
119
150
 
151
+ # @return true if we are in a terminal
120
152
  def terminal?
121
153
  $stdout.tty?
122
154
  end
123
155
 
124
- # @return true if we can display Unicode characters
156
+ # @return :text or :graphical depending on the environment
157
+ def default_gui_mode
158
+ # assume not remotely connected on macos and windows
159
+ return :graphical if [Environment::OS_WINDOWS, Environment::OS_MACOS].include?(Environment.os)
160
+ # unix family
161
+ return :graphical if ENV.key?('DISPLAY') && !ENV['DISPLAY'].empty?
162
+ return :text
163
+ end
164
+
165
+ # open a URI in a graphical browser
166
+ # command must be non blocking
167
+ def open_uri_graphical(uri)
168
+ case Environment.os
169
+ when Environment::OS_MACOS then return system('open', uri.to_s)
170
+ when Environment::OS_WINDOWS then return system('start', 'explorer', %Q{"#{uri}"})
171
+ when Environment::OS_LINUX then return system('xdg-open', uri.to_s)
172
+ else
173
+ raise "no graphical open method for #{Environment.os}"
174
+ end
175
+ end
176
+
177
+ # open a file in an editor
178
+ def open_editor(file_path)
179
+ if ENV.key?('EDITOR')
180
+ system(ENV['EDITOR'], file_path.to_s)
181
+ elsif Environment.os.eql?(Environment::OS_WINDOWS)
182
+ system('notepad.exe', %Q{"#{file_path}"})
183
+ else
184
+ open_uri_graphical(file_path.to_s)
185
+ end
186
+ end
187
+ end
188
+ attr_accessor :url_method
189
+
190
+ def initialize
191
+ @url_method = self.class.default_gui_mode
192
+ @terminal_supports_unicode = nil
193
+ end
194
+
195
+ # @return true if we can display Unicode characters
196
+ # https://www.gnu.org/software/libc/manual/html_node/Locale-Categories.html
197
+ # https://pubs.opengroup.org/onlinepubs/7908799/xbd/envvar.html
125
198
  def terminal_supports_unicode?
126
- @terminal_supports_unicode = terminal? && ENV.values_at('LC_ALL', 'LC_CTYPE', 'LANG').compact.first.include?('UTF-8') if @terminal_supports_unicode.nil?
199
+ @terminal_supports_unicode = self.class.terminal? && %w(LC_ALL LC_CTYPE LANG).any?{|var|ENV[var]&.include?('UTF-8')} if @terminal_supports_unicode.nil?
127
200
  return @terminal_supports_unicode
128
201
  end
202
+
203
+
204
+ # Allows a user to open a Url
205
+ # if method is "text", then URL is displayed on terminal
206
+ # if method is "graphical", then the URL will be opened with the default browser.
207
+ # this is non blocking
208
+ def open_uri(the_url)
209
+ case @url_method
210
+ when :graphical
211
+ self.class.open_uri_graphical(the_url)
212
+ when :text
213
+ case the_url.to_s
214
+ when /^http/
215
+ puts "USER ACTION: please enter this url in a browser:\n#{the_url.to_s.red}\n"
216
+ else
217
+ puts "USER ACTION: open this:\n#{the_url.to_s.red}\n"
218
+ end
219
+ else
220
+ raise StandardError, "unsupported url open method: #{@url_method}"
221
+ end
129
222
  end
130
223
  end
131
224
  end
@@ -29,7 +29,7 @@ module Aspera
29
29
  'recipients' => faspex_pkg_delivery['recipients'],
30
30
  'workspace_id' => @app_context
31
31
  }
32
- created_package = @app_api.create_package_simple(package_data, true, @new_user_option)
32
+ created_package = @app_api.create_package_simple(package_data, true, nil)
33
33
  # but we place it in a Faspex package creation response
34
34
  return {
35
35
  'links' => { 'status' => 'unused' },
@@ -16,10 +16,12 @@ module Aspera
16
16
  FILE_TYPE = 'encrypted_hash_vault'
17
17
  CONTENT_KEYS = %i[label username password url description].freeze
18
18
  FILE_KEYS = %w[version type cipher data].sort.freeze
19
+ private_constant :LEGACY_CIPHER_NAME, :DEFAULT_CIPHER_NAME, :FILE_TYPE, :CONTENT_KEYS, :FILE_KEYS
19
20
  def initialize(path, current_password)
20
21
  Aspera.assert_type(path, String){'path to vault file'}
21
22
  @path = path
22
23
  @all_secrets = {}
24
+ @cipher_name = DEFAULT_CIPHER_NAME
23
25
  vault_encrypted_data = nil
24
26
  if File.exist?(@path)
25
27
  vault_file = File.read(@path)
data/lib/aspera/log.rb CHANGED
@@ -6,6 +6,7 @@ require 'logger'
6
6
  require 'pp'
7
7
  require 'json'
8
8
  require 'singleton'
9
+ require 'stringio'
9
10
 
10
11
  old_verbose = $VERBOSE
11
12
  $VERBOSE = nil
@@ -39,7 +39,7 @@ module Aspera
39
39
  set_json_response(response, {
40
40
  application: 'node',
41
41
  current_time: Time.now.utc.iso8601(0),
42
- version: info['ascp_version'].gsub(/ .*$/, ''),
42
+ version: info['sdk_ascp_version'].gsub(/ .*$/, ''),
43
43
  license_expiration_date: info['expiration_date'],
44
44
  license_max_rate: info['maximum_bandwidth'],
45
45
  os: %x(uname -srv).chomp,
@@ -35,7 +35,7 @@ module Aspera
35
35
  def create_token
36
36
  require 'jwt'
37
37
  seconds_since_epoch = Time.new.to_i
38
- Log.log.info{"seconds=#{seconds_since_epoch}"}
38
+ Log.log.debug{"seconds_since_epoch=#{seconds_since_epoch}"}
39
39
  jwt_payload = {
40
40
  exp: seconds_since_epoch + OAuth::Factory.instance.parameters[:jwt_expiry_offset_sec], # expiration time
41
41
  nbf: seconds_since_epoch - OAuth::Factory.instance.parameters[:jwt_accepted_offset_sec], # not before
@@ -6,6 +6,8 @@ module Aspera
6
6
  module OAuth
7
7
  # This class is used to create a token using a JSON body and a URL
8
8
  class UrlJson < Base
9
+ # @param url URL to send the JSON body
10
+ # @param json JSON body to send
9
11
  def initialize(
10
12
  url:,
11
13
  json:,
@@ -1,15 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'aspera/oauth/base'
4
- require 'aspera/open_application'
4
+ require 'aspera/environment'
5
5
  require 'aspera/web_auth'
6
6
  require 'aspera/assert'
7
7
  module Aspera
8
8
  module OAuth
9
9
  # Authentication using Web browser
10
10
  class Web < Base
11
- # @param g_o:redirect_uri [M] for type :web
12
- # @param g_o:path_authorize [D] for type :web
11
+ # @param redirect_uri url to receive the code after auth (to be exchanged for token)
12
+ # @param path_authorize path to login page on web app
13
13
  def initialize(
14
14
  redirect_uri:,
15
15
  path_authorize: 'authorize',
@@ -21,11 +21,12 @@ module Aspera
21
21
  uri = URI.parse(@redirect_uri)
22
22
  Aspera.assert(%w[http https].include?(uri.scheme)){'redirect_uri scheme must be http or https'}
23
23
  Aspera.assert(!uri.port.nil?){'redirect_uri must have a port'}
24
- # TODO: we could check that host is localhost or local address
24
+ # TODO: we could check that host is localhost or local address, as we are going to listen locally
25
25
  end
26
26
 
27
27
  def create_token
28
- random_state = SecureRandom.uuid # used to check later
28
+ # generate secure state to check later
29
+ random_state = SecureRandom.uuid
29
30
  login_page_url = Rest.build_uri(
30
31
  "#{@base_url}/#{@path_authorize}",
31
32
  optional_scope_client_id.merge(response_type: 'code', redirect_uri: @redirect_uri, state: random_state))
@@ -34,7 +35,7 @@ module Aspera
34
35
  # start a web server to receive request code
35
36
  web_server = WebAuth.new(@redirect_uri)
36
37
  # start browser on login page
37
- OpenApplication.instance.uri(login_page_url)
38
+ Environment.instance.open_uri(login_page_url)
38
39
  # wait for code in request
39
40
  received_params = web_server.received_request
40
41
  Aspera.assert(random_state.eql?(received_params['state'])){'wrong received state'}
data/lib/aspera/rest.rb CHANGED
@@ -12,7 +12,7 @@ require 'json'
12
12
  require 'base64'
13
13
  require 'cgi'
14
14
 
15
- # add cancel method to http
15
+ # Cancel method for HTTP
16
16
  class Net::HTTP::Cancel < Net::HTTPRequest # rubocop:disable Style/ClassAndModuleChildren
17
17
  METHOD = 'CANCEL'
18
18
  REQUEST_HAS_BODY = false
@@ -24,12 +24,16 @@ module Aspera
24
24
  # rest call errors are raised as exception RestCallError
25
25
  # and error are analyzed in RestErrorAnalyzer
26
26
  class Rest
27
- # global settings also valid for any subclass
27
+ # Global settings also valid for any subclass
28
+ # @param user_agent [String] HTTP request header: 'User-Agent'
29
+ # @param download_partial_suffix [String] suffix for partial download
30
+ # @param session_cb [lambda] lambda called on new HTTP session. Takes the Net::HTTP as arg. Used to change parameters on creation.
31
+ # @param progress_bar [Object] progress bar object
28
32
  @@global = { # rubocop:disable Style/ClassVars
29
- user_agent: 'Ruby', # goes to HTTP request header: 'User-Agent'
30
- download_partial_suffix: '.http_partial', # suffix for partial download
31
- session_cb: nil, # a lambda which takes the Net::HTTP as arg, use this to change parameters
32
- progress_bar: nil # progress bar object
33
+ user_agent: 'RubyAsperaRest',
34
+ download_partial_suffix: '.http_partial',
35
+ session_cb: nil,
36
+ progress_bar: nil
33
37
  }
34
38
 
35
39
  # flag for array parameters prefixed with []
@@ -44,9 +48,10 @@ module Aspera
44
48
  JSON_DECODE = ['application/json', 'application/vnd.api+json', 'application/x-javascript'].freeze
45
49
 
46
50
  class << self
51
+ # @return [String] Basic auth token
47
52
  def basic_token(user, pass); return "Basic #{Base64.strict_encode64("#{user}:#{pass}")}"; end
48
53
 
49
- # used to build a parameter list prefixed with "[]"
54
+ # Build a parameter list prefixed with "[]"
50
55
  # @param values [Array] list of values
51
56
  def array_params(values)
52
57
  return [ARRAY_PARAMS].concat(values)
@@ -56,7 +61,7 @@ module Aspera
56
61
  return values.first.eql?(ARRAY_PARAMS)
57
62
  end
58
63
 
59
- # build URI from URL and parameters and check it is http or https, encode array [] parameters
64
+ # Build URI from URL and parameters and check it is http or https, encode array [] parameters
60
65
  def build_uri(url, query_hash=nil)
61
66
  uri = URI.parse(url)
62
67
  Aspera.assert(%w[http https].include?(uri.scheme)){"REST endpoint shall be http/s not #{uri.scheme}"}
@@ -82,8 +87,16 @@ module Aspera
82
87
  return uri
83
88
  end
84
89
 
90
+ # decode query string as hash
91
+ # Does not support arrays in query string, no standard, e.g. PHP's way is p[]=1&p[]=2
92
+ # @param query [String] query string
93
+ # @return [Hash] decoded query
85
94
  def decode_query(query)
86
- URI.decode_www_form(query).each_with_object({}){|v, h|h[v.first] = v.last }
95
+ URI.decode_www_form(query).each_with_object({}) do |pair, h|
96
+ key = pair.first
97
+ raise "Array not supported in query string: #{key}" if key.include?('[]') || h.key?(key)
98
+ h[key] = pair.last
99
+ end
87
100
  end
88
101
 
89
102
  # Start a HTTP/S session, also used for web sockets
@@ -141,6 +154,17 @@ module Aspera
141
154
  def user_agent
142
155
  return @@global[:user_agent]
143
156
  end
157
+
158
+ def parse_header(header)
159
+ type, *params = header.split(/;\s*/)
160
+ parameters = params.map do |param|
161
+ one = param.split(/=\s*/)
162
+ one[0] = one[0].to_sym
163
+ one[1] = one[1].gsub(/\A"|"\z/, '')
164
+ one
165
+ end.to_h
166
+ { type: type.downcase, parameters: parameters }
167
+ end
144
168
  end
145
169
 
146
170
  private
@@ -185,8 +209,12 @@ module Aspera
185
209
  headers: nil
186
210
  )
187
211
  Aspera.assert_type(base_url, String)
188
- # base url with max one trailing slashes (note: string may be frozen)
189
- @base_url = base_url.gsub(%r{//+$}, '/')
212
+ # base url with no trailing slashes (note: string may be frozen)
213
+ @base_url = base_url.gsub(%r{/+$}, '')
214
+ # remove trailing port if it is 443 and scheme is https
215
+ @base_url = @base_url.gsub(/:443$/, '') if @base_url.start_with?('https://')
216
+ @base_url = @base_url.gsub(/:80$/, '') if @base_url.start_with?('http://')
217
+ Log.log.debug{"Rest.new(#{@base_url})"}
190
218
  # default is no auth
191
219
  @auth_params = auth.nil? ? {type: :none} : auth
192
220
  Aspera.assert_type(@auth_params, Hash)
@@ -265,7 +293,7 @@ module Aspera
265
293
  begin
266
294
  # TODO: shall we percent encode subpath (spaces) test with access key delete with space in id
267
295
  # URI.escape()
268
- separator = !['', '/'].include?(subpath) || @base_url.end_with?('/') ? '/' : ''
296
+ separator = ['', '/'].include?(subpath) ? '' : '/'
269
297
  uri = self.class.build_uri("#{@base_url}#{separator}#{subpath}", query)
270
298
  Log.log.debug{"URI=#{uri}"}
271
299
  begin
@@ -305,7 +333,7 @@ module Aspera
305
333
  # make http request (pipelined)
306
334
  http_session.request(req) do |response|
307
335
  result[:http] = response
308
- result_mime = (result[:http]['Content-Type'] || 'text/plain').split(';').first.downcase
336
+ result_mime = self.class.parse_header(result[:http]['Content-Type'] || 'text/plain')[:type]
309
337
  # JSON data needs to be parsed, in case it contains an error code
310
338
  if !save_to_file.nil? &&
311
339
  result[:http].code.to_s.start_with?('2') &&
@@ -315,8 +343,11 @@ module Aspera
315
343
  Log.log.debug('before write file')
316
344
  target_file = save_to_file
317
345
  # override user's path to path in header
318
- if !response['Content-Disposition'].nil? && (m = response['Content-Disposition'].match(/filename="([^"]+)"/))
319
- target_file = File.join(File.dirname(target_file), m[1])
346
+ if !response['Content-Disposition'].nil?
347
+ disposition = self.class.parse_header(response['Content-Disposition'])
348
+ if disposition[:parameters].key?(:filename)
349
+ target_file = File.join(File.dirname(target_file), disposition[:parameters][:filename])
350
+ end
320
351
  end
321
352
  # download with temp filename
322
353
  target_file_tmp = "#{target_file}#{@@global[:download_partial_suffix]}"