aspera-cli 4.2.1 → 4.5.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
- data/README.md +1580 -946
- data/bin/ascli +1 -1
- data/bin/asession +3 -5
- data/docs/Makefile +8 -11
- data/docs/README.erb.md +1521 -829
- data/docs/doc_tools.rb +58 -0
- data/docs/test_env.conf +3 -1
- data/examples/faspex4.rb +28 -19
- data/examples/transfer.rb +2 -2
- data/lib/aspera/aoc.rb +157 -134
- data/lib/aspera/cli/listener/progress_multi.rb +5 -5
- data/lib/aspera/cli/main.rb +106 -48
- data/lib/aspera/cli/manager.rb +19 -20
- data/lib/aspera/cli/plugin.rb +22 -7
- data/lib/aspera/cli/plugins/aoc.rb +260 -208
- data/lib/aspera/cli/plugins/ats.rb +11 -10
- data/lib/aspera/cli/plugins/bss.rb +2 -2
- data/lib/aspera/cli/plugins/config.rb +360 -189
- data/lib/aspera/cli/plugins/faspex.rb +119 -56
- data/lib/aspera/cli/plugins/faspex5.rb +32 -17
- data/lib/aspera/cli/plugins/node.rb +72 -31
- data/lib/aspera/cli/plugins/orchestrator.rb +5 -3
- data/lib/aspera/cli/plugins/preview.rb +94 -68
- data/lib/aspera/cli/plugins/server.rb +16 -5
- data/lib/aspera/cli/plugins/shares.rb +17 -0
- data/lib/aspera/cli/transfer_agent.rb +64 -82
- data/lib/aspera/cli/version.rb +1 -1
- data/lib/aspera/command_line_builder.rb +48 -31
- data/lib/aspera/cos_node.rb +4 -3
- data/lib/aspera/environment.rb +4 -4
- data/lib/aspera/fasp/{manager.rb → agent_base.rb} +7 -6
- data/lib/aspera/fasp/{connect.rb → agent_connect.rb} +46 -39
- data/lib/aspera/fasp/{local.rb → agent_direct.rb} +42 -38
- data/lib/aspera/fasp/{http_gw.rb → agent_httpgw.rb} +50 -29
- data/lib/aspera/fasp/{node.rb → agent_node.rb} +43 -4
- data/lib/aspera/fasp/agent_trsdk.rb +106 -0
- data/lib/aspera/fasp/default.rb +17 -0
- data/lib/aspera/fasp/installation.rb +64 -48
- data/lib/aspera/fasp/parameters.rb +78 -91
- data/lib/aspera/fasp/parameters.yaml +531 -0
- data/lib/aspera/fasp/uri.rb +1 -1
- data/lib/aspera/faspex_gw.rb +12 -11
- data/lib/aspera/id_generator.rb +22 -0
- data/lib/aspera/keychain/encrypted_hash.rb +120 -0
- data/lib/aspera/keychain/macos_security.rb +94 -0
- data/lib/aspera/log.rb +45 -32
- data/lib/aspera/node.rb +9 -4
- data/lib/aspera/oauth.rb +116 -100
- data/lib/aspera/persistency_action_once.rb +11 -7
- data/lib/aspera/persistency_folder.rb +6 -26
- data/lib/aspera/rest.rb +66 -50
- data/lib/aspera/sync.rb +40 -35
- data/lib/aspera/timer_limiter.rb +22 -0
- metadata +86 -29
- data/docs/transfer_spec.html +0 -99
- data/lib/aspera/api_detector.rb +0 -60
- data/lib/aspera/fasp/aoc.rb +0 -24
- data/lib/aspera/secrets.rb +0 -20
@@ -0,0 +1,120 @@
|
|
1
|
+
require 'openssl'
|
2
|
+
|
3
|
+
module Aspera
|
4
|
+
module Keychain
|
5
|
+
class SimpleCipher
|
6
|
+
def initialize(key)
|
7
|
+
@key=Digest::SHA1.hexdigest(key)[0..23]
|
8
|
+
@cipher = OpenSSL::Cipher.new('DES-EDE3-CBC')
|
9
|
+
end
|
10
|
+
|
11
|
+
def encrypt(value)
|
12
|
+
@cipher.encrypt
|
13
|
+
@cipher.key = @key
|
14
|
+
s = @cipher.update(value) + @cipher.final
|
15
|
+
s.unpack('H*').first
|
16
|
+
end
|
17
|
+
|
18
|
+
def decrypt(value)
|
19
|
+
@cipher.decrypt
|
20
|
+
@cipher.key = @key
|
21
|
+
s = [value].pack('H*').unpack('C*').pack('c*')
|
22
|
+
@cipher.update(s) + @cipher.final
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
# Manage secrets in a simple Hash
|
27
|
+
class EncryptedHash
|
28
|
+
SEPARATOR='%'
|
29
|
+
private_constant :SEPARATOR
|
30
|
+
def initialize(values)
|
31
|
+
raise "values shall be Hash" unless values.is_a?(Hash)
|
32
|
+
@all_secrets=values
|
33
|
+
end
|
34
|
+
|
35
|
+
def set(options)
|
36
|
+
raise "options shall be Hash" unless options.is_a?(Hash)
|
37
|
+
unsupported=options.keys-[:username,:url,:secret,:description]
|
38
|
+
raise "unsupported options: #{unsupported}" unless unsupported.empty?
|
39
|
+
username=options[:username]
|
40
|
+
raise "options shall have username" if username.nil?
|
41
|
+
url=options[:url]
|
42
|
+
raise "options shall have username" if url.nil?
|
43
|
+
secret=options[:secret]
|
44
|
+
raise "options shall have secret" if secret.nil?
|
45
|
+
key=[url,username].join(SEPARATOR)
|
46
|
+
raise "secret #{key} already exist, delete first" if @all_secrets.has_key?(key)
|
47
|
+
obj={username: username, url: url, secret: SimpleCipher.new(key).encrypt(secret)}
|
48
|
+
obj[:description]=options[:description] if options.has_key?(:description)
|
49
|
+
@all_secrets[key]=obj
|
50
|
+
nil
|
51
|
+
end
|
52
|
+
|
53
|
+
def list
|
54
|
+
result=[]
|
55
|
+
@all_secrets.each do |k,v|
|
56
|
+
case v
|
57
|
+
when String
|
58
|
+
o={username: k, url: '', description: ''}
|
59
|
+
when Hash
|
60
|
+
o=v.clone
|
61
|
+
o.delete(:secret)
|
62
|
+
o[:description]||=''
|
63
|
+
else raise "error"
|
64
|
+
end
|
65
|
+
o[:description]=v[:description] if v.is_a?(Hash) and v[:description].is_a?(String)
|
66
|
+
result.push(o)
|
67
|
+
end
|
68
|
+
return result
|
69
|
+
end
|
70
|
+
|
71
|
+
def delete(options)
|
72
|
+
raise "options shall be Hash" unless options.is_a?(Hash)
|
73
|
+
unsupported=options.keys-[:username,:url]
|
74
|
+
raise "unsupported options: #{unsupported}" unless unsupported.empty?
|
75
|
+
username=options[:username]
|
76
|
+
raise "options shall have username" if username.nil?
|
77
|
+
url=options[:url]
|
78
|
+
key=nil
|
79
|
+
if !url.nil?
|
80
|
+
extk=[url,username].join(SEPARATOR)
|
81
|
+
key=extk if @all_secrets.has_key?(extk)
|
82
|
+
end
|
83
|
+
# backward compatibility: TODO: remove in future ? (make url mandatory ?)
|
84
|
+
key=username if key.nil? and @all_secrets.has_key?(username)
|
85
|
+
raise "no such secret" if key.nil?
|
86
|
+
@all_secrets.delete(key)
|
87
|
+
end
|
88
|
+
|
89
|
+
def get(options)
|
90
|
+
raise "options shall be Hash" unless options.is_a?(Hash)
|
91
|
+
unsupported=options.keys-[:username,:url]
|
92
|
+
raise "unsupported options: #{unsupported}" unless unsupported.empty?
|
93
|
+
username=options[:username]
|
94
|
+
raise "options shall have username" if username.nil?
|
95
|
+
url=options[:url]
|
96
|
+
val=nil
|
97
|
+
if !url.nil?
|
98
|
+
val=@all_secrets[[url,username].join(SEPARATOR)]
|
99
|
+
end
|
100
|
+
# backward compatibility: TODO: remove in future ? (make url mandatory ?)
|
101
|
+
if val.nil?
|
102
|
+
val=@all_secrets[username]
|
103
|
+
end
|
104
|
+
result=options.clone
|
105
|
+
case val
|
106
|
+
when NilClass
|
107
|
+
raise "no such secret"
|
108
|
+
when String
|
109
|
+
result.merge!({secret: val, description: ''})
|
110
|
+
when Hash
|
111
|
+
key=[url,username].join(SEPARATOR)
|
112
|
+
plain=SimpleCipher.new(key).decrypt(val[:secret])
|
113
|
+
result.merge!({secret: plain, description: val[:description]})
|
114
|
+
else raise "error"
|
115
|
+
end
|
116
|
+
return result
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
@@ -0,0 +1,94 @@
|
|
1
|
+
require 'security'
|
2
|
+
|
3
|
+
# enhance the gem to support other keychains
|
4
|
+
module Security
|
5
|
+
class Keychain
|
6
|
+
class << self
|
7
|
+
def by_name(name)
|
8
|
+
keychains_from_output(`security list-keychains`).select{|kc|kc.filename.end_with?("/#{name}.keychain-db")}.first
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
class Password
|
14
|
+
class << self
|
15
|
+
# add some login to original method
|
16
|
+
alias_method :orig_flags_for_options, :flags_for_options
|
17
|
+
def flags_for_options(options = {})
|
18
|
+
keychain=options.delete(:keychain)
|
19
|
+
url=options.delete(:url)
|
20
|
+
if !url.nil?
|
21
|
+
uri=URI.parse(url)
|
22
|
+
raise 'only https' unless uri.scheme.eql?('https')
|
23
|
+
options[:r]='htps'
|
24
|
+
raise 'host required in URL' if uri.host.nil?
|
25
|
+
options[:s]=uri.host
|
26
|
+
options[:p]=uri.path unless ['','/'].include?(uri.path)
|
27
|
+
options[:P]=uri.port unless uri.port.eql?(443) and !url.include?(':443/')
|
28
|
+
end
|
29
|
+
flags=[orig_flags_for_options(options)]
|
30
|
+
flags.push(keychain.filename) unless keychain.nil?
|
31
|
+
flags.join(' ')
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
module Aspera
|
38
|
+
module Keychain
|
39
|
+
# keychain based on macOS keychain, using `security` cmmand line
|
40
|
+
class MacosSecurity
|
41
|
+
def initialize(name=nil)
|
42
|
+
if name.nil?
|
43
|
+
@keychain=Security::Keychain.default_keychain
|
44
|
+
else
|
45
|
+
@keychain=Security::Keychain.by_name(name)
|
46
|
+
end
|
47
|
+
raise "no such keychain #{name}" if @keychain.nil?
|
48
|
+
end
|
49
|
+
|
50
|
+
def set(options)
|
51
|
+
raise 'options shall be Hash' unless options.is_a?(Hash)
|
52
|
+
unsupported=options.keys-[:username,:url,:secret,:description]
|
53
|
+
raise "unsupported options: #{unsupported}" unless unsupported.empty?
|
54
|
+
username=options[:username]
|
55
|
+
raise 'options shall have username' if username.nil?
|
56
|
+
url=options[:url]
|
57
|
+
raise 'options shall have url' if url.nil?
|
58
|
+
secret=options[:secret]
|
59
|
+
raise 'options shall have secret' if secret.nil?
|
60
|
+
raise 'set not implemented'
|
61
|
+
self
|
62
|
+
end
|
63
|
+
|
64
|
+
def get(options)
|
65
|
+
raise 'options shall be Hash' unless options.is_a?(Hash)
|
66
|
+
unsupported=options.keys-[:username,:url]
|
67
|
+
raise "unsupported options: #{unsupported}" unless unsupported.empty?
|
68
|
+
username=options[:username]
|
69
|
+
raise 'options shall have username' if username.nil?
|
70
|
+
url=options[:url]
|
71
|
+
raise 'options shall have url' if url.nil?
|
72
|
+
info=Security::InternetPassword.find(keychain: @keychain, url: url, account: username)
|
73
|
+
raise 'not found' if info.nil?
|
74
|
+
result=options.clone
|
75
|
+
result.merge!({secret: info.password, description: info.attributes['icmt']})
|
76
|
+
return result
|
77
|
+
end
|
78
|
+
|
79
|
+
def list
|
80
|
+
raise 'list not implemented'
|
81
|
+
end
|
82
|
+
|
83
|
+
def delete(options)
|
84
|
+
raise 'options shall be Hash' unless options.is_a?(Hash)
|
85
|
+
unsupported=options.keys-[:username,:url]
|
86
|
+
raise "unsupported options: #{unsupported}" unless unsupported.empty?
|
87
|
+
username=options[:username]
|
88
|
+
raise 'options shall have username' if username.nil?
|
89
|
+
url=options[:url]
|
90
|
+
raise 'delete not implemented'
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
data/lib/aspera/log.rb
CHANGED
@@ -7,33 +7,41 @@ require 'singleton'
|
|
7
7
|
module Aspera
|
8
8
|
# Singleton object for logging
|
9
9
|
class Log
|
10
|
-
|
11
|
-
public
|
12
10
|
include Singleton
|
11
|
+
# class methods
|
12
|
+
class << self
|
13
|
+
# levels are :debug,:info,:warn,:error,fatal,:unknown
|
14
|
+
def levels; Logger::Severity.constants.sort{|a,b|Logger::Severity.const_get(a)<=>Logger::Severity.const_get(b)}.map{|c|c.downcase.to_sym};end
|
13
15
|
|
14
|
-
|
15
|
-
|
16
|
-
# levels are :debug,:info,:warn,:error,fatal,:unknown
|
17
|
-
def self.levels; Logger::Severity.constants.sort{|a,b|Logger::Severity.const_get(a)<=>Logger::Severity.const_get(b)}.map{|c|c.downcase.to_sym};end
|
18
|
-
|
19
|
-
# where logs are sent to
|
20
|
-
def self.logtypes; [:stderr,:stdout,:syslog];end
|
16
|
+
# where logs are sent to
|
17
|
+
def logtypes; [:stderr,:stdout,:syslog];end
|
21
18
|
|
22
|
-
|
23
|
-
|
19
|
+
# get the logger object of singleton
|
20
|
+
def log; instance.logger;end
|
24
21
|
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
22
|
+
# dump object in debug mode
|
23
|
+
# @param name string or symbol
|
24
|
+
# @param format either pp or json format
|
25
|
+
def dump(name,object,format=:json)
|
26
|
+
self.log.debug() do
|
27
|
+
result=case format
|
28
|
+
when :json
|
29
|
+
JSON.pretty_generate(object) rescue PP.pp(object,'')
|
30
|
+
when :ruby
|
31
|
+
PP.pp(object,'')
|
32
|
+
else
|
33
|
+
raise "wrong parameter, expect pp or json"
|
34
|
+
end
|
35
|
+
"#{name.to_s.green} (#{format})=\n#{result}"
|
36
|
+
end
|
33
37
|
end
|
34
|
-
self.log.debug("#{name.to_s.green} (#{format})=\n#{result}")
|
35
38
|
end
|
36
39
|
|
40
|
+
attr_reader :logger_type
|
41
|
+
attr_reader :logger
|
42
|
+
attr_writer :program_name
|
43
|
+
attr_accessor :log_passwords
|
44
|
+
|
37
45
|
# set log level of underlying logger given symbol level
|
38
46
|
def level=(new_level)
|
39
47
|
@logger.level=Logger::Severity.const_get(new_level.to_sym.upcase)
|
@@ -44,20 +52,15 @@ module Aspera
|
|
44
52
|
Logger::Severity.constants.each do |name|
|
45
53
|
return name.downcase.to_sym if @logger.level.eql?(Logger::Severity.const_get(name))
|
46
54
|
end
|
55
|
+
# should not happen
|
47
56
|
raise "error"
|
48
57
|
end
|
49
58
|
|
50
59
|
# change underlying logger, but keep log level
|
51
60
|
def logger_type=(new_logtype)
|
52
|
-
current_severity_integer
|
53
|
-
|
54
|
-
|
55
|
-
else
|
56
|
-
Logger::Severity::WARN
|
57
|
-
end
|
58
|
-
else
|
59
|
-
@logger.level
|
60
|
-
end
|
61
|
+
current_severity_integer=@logger.level unless @logger.nil?
|
62
|
+
current_severity_integer=ENV['AS_LOG_LEVEL'] if current_severity_integer.nil? and ENV.has_key?('AS_LOG_LEVEL')
|
63
|
+
current_severity_integer=Logger::Severity::WARN if current_severity_integer.nil?
|
61
64
|
case new_logtype
|
62
65
|
when :stderr
|
63
66
|
@logger = Logger.new(STDERR)
|
@@ -71,17 +74,27 @@ module Aspera
|
|
71
74
|
end
|
72
75
|
@logger.level=current_severity_integer
|
73
76
|
@logger_type=new_logtype
|
77
|
+
original_formatter = @logger.formatter || Logger::Formatter.new
|
78
|
+
# update formatter with password hiding
|
79
|
+
@logger.formatter=proc do |severity, datetime, progname, msg|
|
80
|
+
unless @log_passwords
|
81
|
+
msg=msg.gsub(/("[^"]*(password|secret|private_key)[^"]*"=>")([^"]+)(")/){"#{$1}***#{$4}"}
|
82
|
+
msg=msg.gsub(/("[^"]*(secret)[^"]*"=>{)([^}]+)(})/){"#{$1}***#{$4}"}
|
83
|
+
msg=msg.gsub(/((secrets)={)([^}]+)(})/){"#{$1}***#{$4}"}
|
84
|
+
end
|
85
|
+
original_formatter.call(severity, datetime, progname, msg)
|
86
|
+
end
|
74
87
|
end
|
75
88
|
|
76
|
-
attr_writer :program_name
|
77
|
-
|
78
89
|
private
|
79
90
|
|
80
91
|
def initialize
|
81
92
|
@logger=nil
|
82
93
|
@program_name='aspera'
|
83
|
-
|
94
|
+
@log_passwords=false
|
95
|
+
# this sets @logger and @logger_type (self needed to call method instead of local var)
|
84
96
|
self.logger_type=:stderr
|
97
|
+
raise "error logger shall be defined" if @logger.nil?
|
85
98
|
end
|
86
99
|
|
87
100
|
end
|
data/lib/aspera/node.rb
CHANGED
@@ -1,4 +1,6 @@
|
|
1
|
+
require 'aspera/fasp/default'
|
1
2
|
require 'aspera/rest'
|
3
|
+
require 'aspera/oauth'
|
2
4
|
require 'aspera/log'
|
3
5
|
require 'zlib'
|
4
6
|
require 'base64'
|
@@ -10,9 +12,12 @@ module Aspera
|
|
10
12
|
ACCESS_LEVELS=['delete','list','mkdir','preview','read','rename','write']
|
11
13
|
MATCH_EXEC_PREFIX='exec:'
|
12
14
|
|
13
|
-
#
|
14
|
-
|
15
|
-
|
15
|
+
# register node special token decoder
|
16
|
+
Oauth.register_decoder(lambda{|token|JSON.parse(Zlib::Inflate.inflate(Base64.decode64(token)).partition('==SIGNATURE==').first)})
|
17
|
+
|
18
|
+
def self.set_ak_basic_token(ts,ak,secret)
|
19
|
+
raise "ERROR: expected xfer" unless ts['remote_user'].eql?(Fasp::Default::ACCESS_KEY_TRANSFER_USER)
|
20
|
+
ts['token']="Basic #{Base64.strict_encode64("#{ak}:#{secret}")}"
|
16
21
|
end
|
17
22
|
|
18
23
|
# for access keys: provide expression to match entry in folder
|
@@ -22,7 +27,7 @@ module Aspera
|
|
22
27
|
def self.file_matcher(match_expression)
|
23
28
|
match_expression||="#{MATCH_EXEC_PREFIX}true"
|
24
29
|
if match_expression.start_with?(MATCH_EXEC_PREFIX)
|
25
|
-
return eval
|
30
|
+
return eval("lambda{|f|#{match_expression[MATCH_EXEC_PREFIX.length..-1]}}")
|
26
31
|
end
|
27
32
|
return lambda{|f|f['name'].match(/#{match_expression}/)}
|
28
33
|
end
|