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.
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