aspera-cli 4.2.1 → 4.5.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|