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.
Files changed (59) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +1580 -946
  3. data/bin/ascli +1 -1
  4. data/bin/asession +3 -5
  5. data/docs/Makefile +8 -11
  6. data/docs/README.erb.md +1521 -829
  7. data/docs/doc_tools.rb +58 -0
  8. data/docs/test_env.conf +3 -1
  9. data/examples/faspex4.rb +28 -19
  10. data/examples/transfer.rb +2 -2
  11. data/lib/aspera/aoc.rb +157 -134
  12. data/lib/aspera/cli/listener/progress_multi.rb +5 -5
  13. data/lib/aspera/cli/main.rb +106 -48
  14. data/lib/aspera/cli/manager.rb +19 -20
  15. data/lib/aspera/cli/plugin.rb +22 -7
  16. data/lib/aspera/cli/plugins/aoc.rb +260 -208
  17. data/lib/aspera/cli/plugins/ats.rb +11 -10
  18. data/lib/aspera/cli/plugins/bss.rb +2 -2
  19. data/lib/aspera/cli/plugins/config.rb +360 -189
  20. data/lib/aspera/cli/plugins/faspex.rb +119 -56
  21. data/lib/aspera/cli/plugins/faspex5.rb +32 -17
  22. data/lib/aspera/cli/plugins/node.rb +72 -31
  23. data/lib/aspera/cli/plugins/orchestrator.rb +5 -3
  24. data/lib/aspera/cli/plugins/preview.rb +94 -68
  25. data/lib/aspera/cli/plugins/server.rb +16 -5
  26. data/lib/aspera/cli/plugins/shares.rb +17 -0
  27. data/lib/aspera/cli/transfer_agent.rb +64 -82
  28. data/lib/aspera/cli/version.rb +1 -1
  29. data/lib/aspera/command_line_builder.rb +48 -31
  30. data/lib/aspera/cos_node.rb +4 -3
  31. data/lib/aspera/environment.rb +4 -4
  32. data/lib/aspera/fasp/{manager.rb → agent_base.rb} +7 -6
  33. data/lib/aspera/fasp/{connect.rb → agent_connect.rb} +46 -39
  34. data/lib/aspera/fasp/{local.rb → agent_direct.rb} +42 -38
  35. data/lib/aspera/fasp/{http_gw.rb → agent_httpgw.rb} +50 -29
  36. data/lib/aspera/fasp/{node.rb → agent_node.rb} +43 -4
  37. data/lib/aspera/fasp/agent_trsdk.rb +106 -0
  38. data/lib/aspera/fasp/default.rb +17 -0
  39. data/lib/aspera/fasp/installation.rb +64 -48
  40. data/lib/aspera/fasp/parameters.rb +78 -91
  41. data/lib/aspera/fasp/parameters.yaml +531 -0
  42. data/lib/aspera/fasp/uri.rb +1 -1
  43. data/lib/aspera/faspex_gw.rb +12 -11
  44. data/lib/aspera/id_generator.rb +22 -0
  45. data/lib/aspera/keychain/encrypted_hash.rb +120 -0
  46. data/lib/aspera/keychain/macos_security.rb +94 -0
  47. data/lib/aspera/log.rb +45 -32
  48. data/lib/aspera/node.rb +9 -4
  49. data/lib/aspera/oauth.rb +116 -100
  50. data/lib/aspera/persistency_action_once.rb +11 -7
  51. data/lib/aspera/persistency_folder.rb +6 -26
  52. data/lib/aspera/rest.rb +66 -50
  53. data/lib/aspera/sync.rb +40 -35
  54. data/lib/aspera/timer_limiter.rb +22 -0
  55. metadata +86 -29
  56. data/docs/transfer_spec.html +0 -99
  57. data/lib/aspera/api_detector.rb +0 -60
  58. data/lib/aspera/fasp/aoc.rb +0 -24
  59. 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
- attr_reader :logger
15
- attr_reader :logger_type
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
- # get the logger object of singleton
23
- def self.log; self.instance.logger; end
19
+ # get the logger object of singleton
20
+ def log; instance.logger;end
24
21
 
25
- # dump object in debug mode
26
- # @param name string or symbol
27
- # @param format either pp or json format
28
- def self.dump(name,object,format=:json)
29
- result=case format
30
- when :ruby;PP.pp(object,'')
31
- when :json;JSON.pretty_generate(object) rescue PP.pp(object,'')
32
- else raise "wrong parameter, expect pp or json"
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=if @logger.nil?
53
- if ENV.has_key?('AS_LOG_LEVEL')
54
- ENV['AS_LOG_LEVEL']
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
- # this sets @logger and @logger_type
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
- # for information only
14
- def self.decode_bearer_token(token)
15
- return JSON.parse(Zlib::Inflate.inflate(Base64.decode64(token)).partition('==SIGNATURE==').first)
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 "lambda{|f|#{match_expression[MATCH_EXEC_PREFIX.length..-1]}}"
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