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.
- checksums.yaml +4 -4
- checksums.yaml.gz.sig +0 -0
- data/CHANGELOG.md +23 -0
- data/CONTRIBUTING.md +5 -12
- data/README.md +152 -84
- data/examples/build_exec +85 -0
- data/examples/build_package.sh +28 -0
- data/lib/aspera/agent/alpha.rb +4 -4
- data/lib/aspera/agent/base.rb +2 -0
- data/lib/aspera/agent/connect.rb +3 -4
- data/lib/aspera/agent/direct.rb +108 -104
- data/lib/aspera/agent/httpgw.rb +1 -1
- data/lib/aspera/api/aoc.rb +2 -2
- data/lib/aspera/api/httpgw.rb +95 -57
- data/lib/aspera/api/node.rb +110 -77
- data/lib/aspera/ascp/installation.rb +47 -32
- data/lib/aspera/ascp/management.rb +4 -1
- data/lib/aspera/ascp/products.rb +2 -8
- data/lib/aspera/cli/extended_value.rb +27 -14
- data/lib/aspera/cli/formatter.rb +35 -28
- data/lib/aspera/cli/main.rb +11 -11
- data/lib/aspera/cli/manager.rb +109 -94
- data/lib/aspera/cli/plugin.rb +4 -7
- data/lib/aspera/cli/plugin_factory.rb +10 -1
- data/lib/aspera/cli/plugins/aoc.rb +15 -14
- data/lib/aspera/cli/plugins/config.rb +35 -29
- data/lib/aspera/cli/plugins/faspex.rb +5 -4
- data/lib/aspera/cli/plugins/faspex5.rb +16 -13
- data/lib/aspera/cli/plugins/node.rb +50 -41
- data/lib/aspera/cli/plugins/orchestrator.rb +3 -2
- data/lib/aspera/cli/plugins/preview.rb +1 -1
- data/lib/aspera/cli/plugins/server.rb +2 -2
- data/lib/aspera/cli/plugins/shares.rb +11 -7
- data/lib/aspera/cli/special_values.rb +13 -0
- data/lib/aspera/cli/sync_actions.rb +73 -32
- data/lib/aspera/cli/transfer_agent.rb +3 -2
- data/lib/aspera/cli/transfer_progress.rb +1 -1
- data/lib/aspera/cli/version.rb +1 -1
- data/lib/aspera/environment.rb +100 -7
- data/lib/aspera/faspex_gw.rb +1 -1
- data/lib/aspera/keychain/encrypted_hash.rb +2 -0
- data/lib/aspera/log.rb +1 -0
- data/lib/aspera/node_simulator.rb +1 -1
- data/lib/aspera/oauth/jwt.rb +1 -1
- data/lib/aspera/oauth/url_json.rb +2 -0
- data/lib/aspera/oauth/web.rb +7 -6
- data/lib/aspera/rest.rb +46 -15
- data/lib/aspera/secret_hider.rb +3 -2
- data/lib/aspera/ssh.rb +1 -1
- data/lib/aspera/transfer/faux_file.rb +7 -5
- data/lib/aspera/transfer/parameters.rb +27 -19
- data/lib/aspera/transfer/spec.rb +8 -10
- data/lib/aspera/transfer/sync.rb +52 -47
- data/lib/aspera/web_auth.rb +0 -1
- data/lib/aspera/web_server_simple.rb +24 -13
- data.tar.gz.sig +0 -0
- metadata +5 -4
- metadata.gz.sig +0 -0
- data/examples/rubyc +0 -24
- 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
|
-
|
|
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("#{
|
|
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)}/#{
|
|
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(
|
|
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(
|
|
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(
|
|
96
|
-
|
|
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}")
|
|
@@ -7,11 +7,30 @@ module Aspera
|
|
|
7
7
|
module Cli
|
|
8
8
|
# Module for sync actions
|
|
9
9
|
module SyncActions
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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,
|
|
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',
|
|
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.
|
|
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?
|
data/lib/aspera/cli/version.rb
CHANGED
data/lib/aspera/environment.rb
CHANGED
|
@@ -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
|
-
|
|
19
|
+
OS_MACOS = :osx
|
|
15
20
|
OS_LINUX = :linux
|
|
16
21
|
OS_AIX = :aix
|
|
17
|
-
OS_LIST = [OS_WINDOWS,
|
|
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
|
|
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
|
-
#
|
|
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
|
|
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? &&
|
|
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
|
data/lib/aspera/faspex_gw.rb
CHANGED
|
@@ -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,
|
|
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
|
@@ -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['
|
|
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,
|
data/lib/aspera/oauth/jwt.rb
CHANGED
|
@@ -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.
|
|
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
|
data/lib/aspera/oauth/web.rb
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'aspera/oauth/base'
|
|
4
|
-
require 'aspera/
|
|
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
|
|
12
|
-
# @param
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
#
|
|
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
|
-
#
|
|
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: '
|
|
30
|
-
download_partial_suffix: '.http_partial',
|
|
31
|
-
session_cb: nil,
|
|
32
|
-
progress_bar: nil
|
|
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
|
-
#
|
|
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
|
-
#
|
|
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({})
|
|
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
|
|
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 =
|
|
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')
|
|
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?
|
|
319
|
-
|
|
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]}"
|