aspera-cli 4.4.0 → 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 (45) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +1042 -787
  3. data/bin/ascli +1 -1
  4. data/bin/asession +3 -5
  5. data/docs/Makefile +4 -7
  6. data/docs/README.erb.md +988 -740
  7. data/examples/faspex4.rb +4 -6
  8. data/examples/transfer.rb +2 -2
  9. data/lib/aspera/aoc.rb +139 -118
  10. data/lib/aspera/cli/listener/progress_multi.rb +5 -5
  11. data/lib/aspera/cli/main.rb +64 -34
  12. data/lib/aspera/cli/manager.rb +19 -20
  13. data/lib/aspera/cli/plugin.rb +9 -1
  14. data/lib/aspera/cli/plugins/aoc.rb +156 -143
  15. data/lib/aspera/cli/plugins/ats.rb +11 -10
  16. data/lib/aspera/cli/plugins/bss.rb +2 -2
  17. data/lib/aspera/cli/plugins/config.rb +236 -112
  18. data/lib/aspera/cli/plugins/faspex.rb +29 -7
  19. data/lib/aspera/cli/plugins/faspex5.rb +21 -8
  20. data/lib/aspera/cli/plugins/node.rb +21 -9
  21. data/lib/aspera/cli/plugins/orchestrator.rb +5 -3
  22. data/lib/aspera/cli/plugins/preview.rb +2 -2
  23. data/lib/aspera/cli/plugins/server.rb +3 -3
  24. data/lib/aspera/cli/plugins/shares.rb +17 -0
  25. data/lib/aspera/cli/transfer_agent.rb +47 -85
  26. data/lib/aspera/cli/version.rb +1 -1
  27. data/lib/aspera/environment.rb +4 -4
  28. data/lib/aspera/fasp/{manager.rb → agent_base.rb} +7 -6
  29. data/lib/aspera/fasp/{connect.rb → agent_connect.rb} +46 -39
  30. data/lib/aspera/fasp/{local.rb → agent_direct.rb} +14 -17
  31. data/lib/aspera/fasp/{http_gw.rb → agent_httpgw.rb} +4 -4
  32. data/lib/aspera/fasp/{node.rb → agent_node.rb} +25 -8
  33. data/lib/aspera/fasp/agent_trsdk.rb +106 -0
  34. data/lib/aspera/fasp/default.rb +17 -0
  35. data/lib/aspera/fasp/installation.rb +64 -48
  36. data/lib/aspera/fasp/parameters.rb +7 -3
  37. data/lib/aspera/faspex_gw.rb +6 -6
  38. data/lib/aspera/keychain/encrypted_hash.rb +120 -0
  39. data/lib/aspera/keychain/macos_security.rb +94 -0
  40. data/lib/aspera/log.rb +45 -32
  41. data/lib/aspera/node.rb +3 -6
  42. data/lib/aspera/rest.rb +65 -49
  43. metadata +68 -27
  44. data/lib/aspera/api_detector.rb +0 -60
  45. data/lib/aspera/secrets.rb +0 -20
@@ -1,6 +1,6 @@
1
1
  require 'aspera/log'
2
2
  require 'aspera/aoc'
3
- require 'aspera/node'
3
+ require 'aspera/fasp/default'
4
4
  require 'aspera/cli/main'
5
5
  require 'webrick'
6
6
  require 'webrick/https'
@@ -76,9 +76,9 @@ module Aspera
76
76
  faspex_transfer_spec={
77
77
  'direction' => 'send',
78
78
  'remote_host' => node_info['host'],
79
- 'remote_user' => Node::ACCESS_KEY_TRANSFER_USER,
80
- 'ssh_port' => Node::SSH_PORT_DEFAULT,
81
- 'fasp_port' => Node::UDP_PORT_DEFAULT
79
+ 'remote_user' => Fasp::Default::ACCESS_KEY_TRANSFER_USER,
80
+ 'ssh_port' => Fasp::Default::SSH_PORT,
81
+ 'fasp_port' => Fasp::Default::UDP_PORT,
82
82
  'tags' => ts_tags,
83
83
  'token' => node_auth_bearer_token,
84
84
  'paths' => [{'destination' => '/'}],
@@ -95,8 +95,8 @@ module Aspera
95
95
  'lock_rate_policy' => true,
96
96
  'source_root' => '',
97
97
  'content_protection' => nil,
98
- 'target_rate_cap_kbps' => 20000, # TODO
99
- 'target_rate_kbps' => 10000, # TODO
98
+ 'target_rate_cap_kbps' => 20000, # TODO: is this value useful ?
99
+ 'target_rate_kbps' => 10000, # TODO: get from where?
100
100
  'cipher' => 'aes-128',
101
101
  'cipher_allowed' => nil,
102
102
  'http_fallback' => false,
@@ -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,3 +1,4 @@
1
+ require 'aspera/fasp/default'
1
2
  require 'aspera/rest'
2
3
  require 'aspera/oauth'
3
4
  require 'aspera/log'
@@ -10,16 +11,12 @@ module Aspera
10
11
  # permissions
11
12
  ACCESS_LEVELS=['delete','list','mkdir','preview','read','rename','write']
12
13
  MATCH_EXEC_PREFIX='exec:'
13
- # (public) default transfer username for access key based transfers
14
- ACCESS_KEY_TRANSFER_USER='xfer'
15
- SSH_PORT_DEFAULT=33001
16
- UDP_PORT_DEFAULT=33001
17
14
 
18
15
  # register node special token decoder
19
16
  Oauth.register_decoder(lambda{|token|JSON.parse(Zlib::Inflate.inflate(Base64.decode64(token)).partition('==SIGNATURE==').first)})
20
17
 
21
18
  def self.set_ak_basic_token(ts,ak,secret)
22
- raise "ERROR: expected xfer" unless ts['remote_user'].eql?(ACCESS_KEY_TRANSFER_USER)
19
+ raise "ERROR: expected xfer" unless ts['remote_user'].eql?(Fasp::Default::ACCESS_KEY_TRANSFER_USER)
23
20
  ts['token']="Basic #{Base64.strict_encode64("#{ak}:#{secret}")}"
24
21
  end
25
22
 
@@ -30,7 +27,7 @@ module Aspera
30
27
  def self.file_matcher(match_expression)
31
28
  match_expression||="#{MATCH_EXEC_PREFIX}true"
32
29
  if match_expression.start_with?(MATCH_EXEC_PREFIX)
33
- return eval "lambda{|f|#{match_expression[MATCH_EXEC_PREFIX.length..-1]}}"
30
+ return eval("lambda{|f|#{match_expression[MATCH_EXEC_PREFIX.length..-1]}}")
34
31
  end
35
32
  return lambda{|f|f['name'].match(/#{match_expression}/)}
36
33
  end
data/lib/aspera/rest.rb CHANGED
@@ -35,8 +35,14 @@ module Aspera
35
35
  @@insecure=false
36
36
  @@user_agent='Ruby'
37
37
  @@download_partial_suffix='.http_partial'
38
+ # a lambda which takes the Net::HTTP as arg, use this to change parameters
39
+ @@session_cb=nil
38
40
 
39
41
  public
42
+ def self.session_cb=(v); @@session_cb=v;Log.log.debug("session_cb => #{@@session_cb}".red);end
43
+
44
+ def self.session_cb; @@session_cb;end
45
+
40
46
  def self.insecure=(v); @@insecure=v;Log.log.debug("insecure => #{@@insecure}".red);end
41
47
 
42
48
  def self.insecure; @@insecure;end
@@ -85,12 +91,9 @@ module Aspera
85
91
  # this honors http_proxy env var
86
92
  @http_session=Net::HTTP.new(uri.host, uri.port)
87
93
  @http_session.use_ssl = uri.scheme.eql?('https')
88
- Log.log.debug("insecure=#{@@insecure}")
89
94
  @http_session.verify_mode = OpenSSL::SSL::VERIFY_NONE if @@insecure
90
95
  @http_session.set_debug_output($stdout) if @@debug
91
- if @params.has_key?(:session_cb)
92
- @params[:session_cb].call(@http_session)
93
- end
96
+ @@session_cb.call(@http_session) unless @@session_cb.nil?
94
97
  # manually start session for keep alive (if supported by server, else, session is closed every time)
95
98
  @http_session.start
96
99
  end
@@ -107,7 +110,6 @@ module Aspera
107
110
  # :username [:basic]
108
111
  # :password [:basic]
109
112
  # :url_creds [:url]
110
- # :session_cb a lambda which takes @http_session as arg, use this to change parameters
111
113
  # :* [:oauth2] see Oauth class
112
114
  def initialize(a_rest_params)
113
115
  raise "ERROR: expecting Hash" unless a_rest_params.is_a?(Hash)
@@ -129,47 +131,14 @@ module Aspera
129
131
  return @oauth.get_authorization(options)
130
132
  end
131
133
 
132
- # HTTP/S REST call
133
- # call_data has keys:
134
- # :auth
135
- # :operation
136
- # :subpath
137
- # :headers
138
- # :json_params
139
- # :url_params
140
- # :www_body_params
141
- # :text_body_params
142
- # :save_to_file (filepath)
143
- # :return_error (bool)
144
- def call(call_data)
145
- raise "Hash call parameter is required (#{call_data.class})" unless call_data.is_a?(Hash)
146
- Log.log.debug("accessing #{call_data[:subpath]}".red.bold.bg_green)
147
- call_data[:headers]||={}
148
- call_data[:headers]['User-Agent'] ||= @@user_agent
149
- # defaults from @params are overriden by call dataz
150
- call_data=@params.deep_merge(call_data)
151
- case call_data[:auth][:type]
152
- when :none
153
- # no auth
154
- when :basic
155
- Log.log.debug("using Basic auth")
156
- basic_auth_data=[call_data[:auth][:username],call_data[:auth][:password]]
157
- when :oauth2
158
- call_data[:headers]['Authorization']=oauth_token unless call_data[:headers].has_key?('Authorization')
159
- when :url
160
- call_data[:url_params]||={}
161
- call_data[:auth][:url_creds].each do |key, value|
162
- call_data[:url_params][key]=value
163
- end
164
- else raise "unsupported auth type: [#{call_data[:auth][:type]}]"
165
- end
134
+ def build_request(call_data)
166
135
  # TODO: shall we percent encode subpath (spaces) test with access key delete with space in id
167
136
  # URI.escape()
168
- uri=self.class.build_uri("#{@params[:base_url]}#{call_data[:subpath].nil? ? '' : '/'}#{call_data[:subpath]}",call_data[:url_params])
137
+ uri=self.class.build_uri("#{call_data[:base_url]}#{['','/'].include?(call_data[:subpath]) ? '' : '/'}#{call_data[:subpath]}",call_data[:url_params])
169
138
  Log.log.debug("URI=#{uri}")
170
139
  begin
171
140
  # instanciate request object based on string name
172
- req=Object::const_get('Net::HTTP::'+call_data[:operation].capitalize).new(uri.request_uri)
141
+ req=Object::const_get('Net::HTTP::'+call_data[:operation].capitalize).new(uri)#.request_uri
173
142
  rescue NameError => e
174
143
  raise "unsupported operation : #{call_data[:operation]}"
175
144
  end
@@ -196,20 +165,59 @@ module Aspera
196
165
  end
197
166
  end
198
167
  # :type = :basic
199
- req.basic_auth(*basic_auth_data) unless basic_auth_data.nil?
168
+ req.basic_auth(call_data[:auth][:username],call_data[:auth][:password]) if call_data[:auth][:type].eql?(:basic)
169
+ return req
170
+ end
200
171
 
172
+ # HTTP/S REST call
173
+ # call_data has keys:
174
+ # :auth
175
+ # :operation
176
+ # :subpath
177
+ # :headers
178
+ # :json_params
179
+ # :url_params
180
+ # :www_body_params
181
+ # :text_body_params
182
+ # :save_to_file (filepath)
183
+ # :return_error (bool)
184
+ # :redirect_max (int)
185
+ def call(call_data)
186
+ raise "Hash call parameter is required (#{call_data.class})" unless call_data.is_a?(Hash)
187
+ call_data[:subpath]='' if call_data[:subpath].nil?
188
+ Log.log.debug("accessing #{call_data[:subpath]}".red.bold.bg_green)
189
+ call_data[:headers]||={}
190
+ call_data[:headers]['User-Agent'] ||= @@user_agent
191
+ # defaults from @params are overriden by call data
192
+ call_data=@params.deep_merge(call_data)
193
+ case call_data[:auth][:type]
194
+ when :none
195
+ # no auth
196
+ when :basic
197
+ Log.log.debug("using Basic auth")
198
+ # done in build_req
199
+ when :oauth2
200
+ call_data[:headers]['Authorization']=oauth_token unless call_data[:headers].has_key?('Authorization')
201
+ when :url
202
+ call_data[:url_params]||={}
203
+ call_data[:auth][:url_creds].each do |key, value|
204
+ call_data[:url_params][key]=value
205
+ end
206
+ else raise "unsupported auth type: [#{call_data[:auth][:type]}]"
207
+ end
208
+ req=build_request(call_data)
201
209
  Log.log.debug("call_data = #{call_data}")
202
210
  result={:http=>nil}
203
211
  # start a block to be able to retry the actual HTTP request
204
212
  begin
205
213
  # we try the call, and will retry only if oauth, as we can, first with refresh, and then re-auth if refresh is bad
206
214
  oauth_tries ||= 2
207
- tries_remain_redirect||=4
215
+ tries_remain_redirect||=call_data[:redirect_max].nil? ? 0 : call_data[:redirect_max].to_i
208
216
  Log.log.debug("send request")
209
217
  # make http request (pipelined)
210
218
  http_session.request(req) do |response|
211
219
  result[:http] = response
212
- if call_data.has_key?(:save_to_file)
220
+ if call_data.has_key?(:save_to_file) and result[:http].code.to_s.start_with?('2')
213
221
  total_size=result[:http]['Content-Length'].to_i
214
222
  progress=ProgressBar.create(
215
223
  :format => '%a %B %p%% %r KB/sec %e',
@@ -238,7 +246,7 @@ module Aspera
238
246
  progress=nil
239
247
  end # save_to_file
240
248
  end
241
- # sometimes there is a ITF8 char (e.g. (c) )
249
+ # sometimes there is a UTF8 char (e.g. (c) )
242
250
  result[:http].body.force_encoding("UTF-8") if result[:http].body.is_a?(String)
243
251
  Log.log.debug("result: body=#{result[:http].body}")
244
252
  result_mime=(result[:http]['Content-Type']||'text/plain').split(';').first
@@ -270,9 +278,18 @@ module Aspera
270
278
  if tries_remain_redirect > 0
271
279
  tries_remain_redirect-=1
272
280
  Log.log.info("URL is moved: #{e.response['location']}")
273
- raise e
274
- # TODO: rebuild request with new location
275
- #retry
281
+ current_uri=URI.parse(call_data[:base_url])
282
+ redir_uri=URI.parse(e.response['location'])
283
+ call_data[:base_url]=e.response['location']
284
+ call_data[:subpath]=''
285
+ if current_uri.host.eql?(redir_uri.host) and current_uri.port.eql?(redir_uri.port)
286
+ req=build_request(call_data)
287
+ retry
288
+ else
289
+ # change host
290
+ Log.log.info("Redirect changes host: #{current_uri.host} -> #{redir_uri.host}")
291
+ return self.class.new(call_data).call(call_data)
292
+ end
276
293
  else
277
294
  raise "too many redirect"
278
295
  end
@@ -284,7 +301,6 @@ module Aspera
284
301
  end # begin request
285
302
  Log.log.debug("result=#{result}")
286
303
  return result
287
-
288
304
  end
289
305
 
290
306
  #