aspera-cli 4.0.0.pre1

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 (88) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +3592 -0
  3. data/bin/ascli +7 -0
  4. data/bin/asession +89 -0
  5. data/docs/Makefile +59 -0
  6. data/docs/README.erb.md +3012 -0
  7. data/docs/README.md +13 -0
  8. data/docs/diagrams.txt +49 -0
  9. data/docs/secrets.make +38 -0
  10. data/docs/test_env.conf +117 -0
  11. data/docs/transfer_spec.html +99 -0
  12. data/examples/aoc.rb +17 -0
  13. data/examples/proxy.pac +60 -0
  14. data/examples/transfer.rb +115 -0
  15. data/lib/aspera/api_detector.rb +60 -0
  16. data/lib/aspera/ascmd.rb +151 -0
  17. data/lib/aspera/ats_api.rb +43 -0
  18. data/lib/aspera/cli/basic_auth_plugin.rb +38 -0
  19. data/lib/aspera/cli/extended_value.rb +88 -0
  20. data/lib/aspera/cli/formater.rb +238 -0
  21. data/lib/aspera/cli/listener/line_dump.rb +17 -0
  22. data/lib/aspera/cli/listener/logger.rb +20 -0
  23. data/lib/aspera/cli/listener/progress.rb +52 -0
  24. data/lib/aspera/cli/listener/progress_multi.rb +91 -0
  25. data/lib/aspera/cli/main.rb +304 -0
  26. data/lib/aspera/cli/manager.rb +440 -0
  27. data/lib/aspera/cli/plugin.rb +90 -0
  28. data/lib/aspera/cli/plugins/alee.rb +24 -0
  29. data/lib/aspera/cli/plugins/ats.rb +231 -0
  30. data/lib/aspera/cli/plugins/bss.rb +71 -0
  31. data/lib/aspera/cli/plugins/config.rb +806 -0
  32. data/lib/aspera/cli/plugins/console.rb +62 -0
  33. data/lib/aspera/cli/plugins/cos.rb +106 -0
  34. data/lib/aspera/cli/plugins/faspex.rb +377 -0
  35. data/lib/aspera/cli/plugins/faspex5.rb +93 -0
  36. data/lib/aspera/cli/plugins/node.rb +438 -0
  37. data/lib/aspera/cli/plugins/oncloud.rb +937 -0
  38. data/lib/aspera/cli/plugins/orchestrator.rb +169 -0
  39. data/lib/aspera/cli/plugins/preview.rb +464 -0
  40. data/lib/aspera/cli/plugins/server.rb +216 -0
  41. data/lib/aspera/cli/plugins/shares.rb +63 -0
  42. data/lib/aspera/cli/plugins/shares2.rb +114 -0
  43. data/lib/aspera/cli/plugins/sync.rb +65 -0
  44. data/lib/aspera/cli/plugins/xnode.rb +115 -0
  45. data/lib/aspera/cli/transfer_agent.rb +251 -0
  46. data/lib/aspera/cli/version.rb +5 -0
  47. data/lib/aspera/colors.rb +39 -0
  48. data/lib/aspera/command_line_builder.rb +137 -0
  49. data/lib/aspera/fasp/aoc.rb +24 -0
  50. data/lib/aspera/fasp/connect.rb +99 -0
  51. data/lib/aspera/fasp/error.rb +21 -0
  52. data/lib/aspera/fasp/error_info.rb +60 -0
  53. data/lib/aspera/fasp/http_gw.rb +81 -0
  54. data/lib/aspera/fasp/installation.rb +240 -0
  55. data/lib/aspera/fasp/listener.rb +11 -0
  56. data/lib/aspera/fasp/local.rb +377 -0
  57. data/lib/aspera/fasp/manager.rb +69 -0
  58. data/lib/aspera/fasp/node.rb +88 -0
  59. data/lib/aspera/fasp/parameters.rb +235 -0
  60. data/lib/aspera/fasp/resume_policy.rb +76 -0
  61. data/lib/aspera/fasp/uri.rb +51 -0
  62. data/lib/aspera/faspex_gw.rb +196 -0
  63. data/lib/aspera/hash_ext.rb +28 -0
  64. data/lib/aspera/log.rb +80 -0
  65. data/lib/aspera/nagios.rb +71 -0
  66. data/lib/aspera/node.rb +14 -0
  67. data/lib/aspera/oauth.rb +319 -0
  68. data/lib/aspera/on_cloud.rb +421 -0
  69. data/lib/aspera/open_application.rb +72 -0
  70. data/lib/aspera/persistency_action_once.rb +42 -0
  71. data/lib/aspera/persistency_folder.rb +91 -0
  72. data/lib/aspera/preview/file_types.rb +300 -0
  73. data/lib/aspera/preview/generator.rb +258 -0
  74. data/lib/aspera/preview/image_error.png +0 -0
  75. data/lib/aspera/preview/options.rb +35 -0
  76. data/lib/aspera/preview/utils.rb +131 -0
  77. data/lib/aspera/preview/video_error.png +0 -0
  78. data/lib/aspera/proxy_auto_config.erb.js +287 -0
  79. data/lib/aspera/proxy_auto_config.rb +34 -0
  80. data/lib/aspera/rest.rb +296 -0
  81. data/lib/aspera/rest_call_error.rb +13 -0
  82. data/lib/aspera/rest_error_analyzer.rb +98 -0
  83. data/lib/aspera/rest_errors_aspera.rb +58 -0
  84. data/lib/aspera/ssh.rb +53 -0
  85. data/lib/aspera/sync.rb +82 -0
  86. data/lib/aspera/temp_file_manager.rb +37 -0
  87. data/lib/aspera/uri_reader.rb +25 -0
  88. metadata +288 -0
@@ -0,0 +1,196 @@
1
+ require 'aspera/log'
2
+ require 'aspera/on_cloud'
3
+ require 'aspera/cli/main'
4
+ require 'webrick'
5
+ require 'webrick/https'
6
+ require 'securerandom'
7
+ require 'openssl'
8
+ require 'json'
9
+
10
+ module Aspera
11
+ # this class answers the Faspex /send API and creates a package on Aspera on Cloud
12
+ class FaspexGW
13
+ class FxGwServlet < WEBrick::HTTPServlet::AbstractServlet
14
+ def initialize(server,a_aoc_api_user,a_workspace_id)
15
+ @aoc_api_user=a_aoc_api_user
16
+ @aoc_workspace_id=a_workspace_id
17
+ end
18
+
19
+ # parameters from user to Faspex API call
20
+ #{"delivery":{"use_encryption_at_rest":false,"note":"note","sources":[{"paths":["file1"]}],"title":"my title","recipients":["email1"],"send_upload_result":true}}
21
+ # {
22
+ # "delivery"=>{
23
+ # "use_encryption_at_rest"=>false,
24
+ # "note"=>"note",
25
+ # "sources"=>[{"paths"=>["file1"]}],
26
+ # "title"=>"my title",
27
+ # "recipients"=>["email1"],
28
+ # "send_upload_result"=>true
29
+ # }
30
+ # }
31
+ def process_faspex_send(request, response)
32
+ raise "no payload" if request.body.nil?
33
+ faspex_pkg_parameters=JSON.parse(request.body)
34
+ faspex_pkg_delivery=faspex_pkg_parameters['delivery']
35
+ Log.log.debug "faspex pkg create parameters=#{faspex_pkg_parameters}"
36
+
37
+ # get recipient ids
38
+ files_pkg_recipients=[]
39
+ faspex_pkg_delivery['recipients'].each do |recipient_email|
40
+ user_lookup=@aoc_api_user.read("contacts",{'current_workspace_id'=>@aoc_workspace_id,'q'=>recipient_email})[:data]
41
+ raise StandardError,"no such unique user: #{recipient_email} / #{user_lookup}" unless !user_lookup.nil? and user_lookup.length == 1
42
+ recipient_user_info=user_lookup.first
43
+ files_pkg_recipients.push({"id"=>recipient_user_info['source_id'],"type"=>recipient_user_info['source_type']})
44
+ end
45
+
46
+ # create a new package with one file
47
+ the_package=@aoc_api_user.create("packages",{
48
+ "file_names"=>faspex_pkg_delivery['sources'][0]['paths'],
49
+ "name"=>faspex_pkg_delivery['title'],
50
+ "note"=>faspex_pkg_delivery['note'],
51
+ "recipients"=>files_pkg_recipients,
52
+ "workspace_id"=>@aoc_workspace_id})[:data]
53
+
54
+ # get node information for the node on which package must be created
55
+ node_info=@aoc_api_user.read("nodes/#{the_package['node_id']}")[:data]
56
+
57
+ # get transfer token (for node)
58
+ node_auth_bearer_token=@aoc_api_user.oauth_token(scope: OnCloud.node_scope(node_info['access_key'],OnCloud::SCOPE_NODE_USER))
59
+
60
+ # tell Files what to expect in package: 1 transfer (can also be done after transfer)
61
+ @aoc_api_user.update("packages/#{the_package['id']}",{"sent"=>true,"transfers_expected"=>1})
62
+
63
+ if false
64
+ response.status=400
65
+ return "ERROR HERE"
66
+ end
67
+ # TODO: check about xfer_*
68
+ ts_tags={
69
+ "aspera" => {
70
+ "files" => { "package_id" => the_package['id'], "package_operation" => "upload" },
71
+ "node" => { "access_key" => node_info['access_key'], "file_id" => the_package['contents_file_id'] },
72
+ "xfer_id" => SecureRandom.uuid,
73
+ "xfer_retry" => 3600 } }
74
+ # this transfer spec is for transfer to AoC
75
+ faspex_transfer_spec={
76
+ 'direction' => 'send',
77
+ 'remote_user' => 'xfer',
78
+ 'remote_host' => node_info['host'],
79
+ 'ssh_port' => 33001,
80
+ 'fasp_port' => 33001,
81
+ 'tags' => ts_tags,
82
+ 'token' => node_auth_bearer_token,
83
+ 'paths' => [{'destination' => '/'}],
84
+ 'cookie' => 'unused',
85
+ 'create_dir' => true,
86
+ 'rate_policy' => 'fair',
87
+ 'rate_policy_allowed' => 'fixed',
88
+ 'min_rate_cap_kbps' => nil,
89
+ 'min_rate_kbps' => 0,
90
+ 'target_rate_percentage' => nil,
91
+ 'lock_target_rate' => nil,
92
+ 'fasp_url' => 'unused',
93
+ 'lock_min_rate' => true,
94
+ 'lock_rate_policy' => true,
95
+ 'source_root' => '',
96
+ 'content_protection' => nil,
97
+ 'target_rate_cap_kbps' => 20000, # TODO
98
+ 'target_rate_kbps' => 10000, # TODO
99
+ 'cipher' => 'aes-128',
100
+ 'cipher_allowed' => nil,
101
+ 'http_fallback' => false,
102
+ 'http_fallback_port' => nil,
103
+ 'https_fallback_port' => nil,
104
+ 'destination_root' => '/'
105
+ }
106
+ # but we place it in a Faspex package creation response
107
+ faspex_package_create_result={
108
+ 'links' => {'status' => 'unused'},
109
+ 'xfer_sessions' => [faspex_transfer_spec]
110
+ }
111
+ Log.log.info("faspex_package_create_result=#{faspex_package_create_result}")
112
+ response.status=200
113
+ response.content_type = "application/json"
114
+ response.body=JSON.generate(faspex_package_create_result)
115
+ end
116
+
117
+ def do_GET (request, response)
118
+ case request.path
119
+ when '/aspera/faspex/send'
120
+ process_faspex_send(request, response)
121
+ else
122
+ response.status=400
123
+ return "ERROR HERE"
124
+ raise "unsupported path: #{request.path}"
125
+ end
126
+ end
127
+ end # FxGwServlet
128
+
129
+ class NewUserServlet < WEBrick::HTTPServlet::AbstractServlet
130
+ def do_GET (request, response)
131
+ case request.path
132
+ when '/newuser'
133
+ response.status=200
134
+ response.content_type = "text/html"
135
+ response.body='<html><body>hello world</body></html>'
136
+ else
137
+ raise "unsupported path: [#{request.path}]"
138
+ end
139
+ end
140
+ end
141
+
142
+ def fill_self_signed_cert(options)
143
+ key = OpenSSL::PKey::RSA.new(4096)
144
+ cert = OpenSSL::X509::Certificate.new
145
+ cert.subject = cert.issuer = OpenSSL::X509::Name.parse("/C=FR/O=Test/OU=Test/CN=Test")
146
+ cert.not_before = Time.now
147
+ cert.not_after = Time.now + 365 * 24 * 60 * 60
148
+ cert.public_key = key.public_key
149
+ cert.serial = 0x0
150
+ cert.version = 2
151
+ ef = OpenSSL::X509::ExtensionFactory.new
152
+ ef.issuer_certificate = cert
153
+ ef.subject_certificate = cert
154
+ cert.extensions = [
155
+ ef.create_extension("basicConstraints","CA:TRUE", true),
156
+ ef.create_extension("subjectKeyIdentifier", "hash"),
157
+ # ef.create_extension("keyUsage", "cRLSign,keyCertSign", true),
158
+ ]
159
+ cert.add_extension(ef.create_extension("authorityKeyIdentifier","keyid:always,issuer:always"))
160
+ cert.sign(key, OpenSSL::Digest::SHA256.new)
161
+ options[:SSLPrivateKey] = key
162
+ options[:SSLCertificate] = cert
163
+ end
164
+
165
+ def initialize(a_aoc_api_user,a_workspace_id)
166
+ webrick_options = {
167
+ :app => FaspexGW,
168
+ :Port => 9443,
169
+ :Logger => Log.log,
170
+ #:DocumentRoot => Cli::Main.gem_root,
171
+ :SSLEnable => true,
172
+ :SSLVerifyClient => OpenSSL::SSL::VERIFY_NONE,
173
+ }
174
+ case 2
175
+ when 0
176
+ # generate self signed cert
177
+ webrick_options[:SSLCertName] = [ [ 'CN',WEBrick::Utils::getservername ] ]
178
+ Log.log.error(">>>#{webrick_options[:SSLCertName]}")
179
+ when 1
180
+ fill_self_signed_cert(webrick_options)
181
+ when 2
182
+ webrick_options[:SSLPrivateKey] =OpenSSL::PKey::RSA.new(File.read('/Users/laurent/workspace/Tools/certificate/myserver.key'))
183
+ webrick_options[:SSLCertificate] = OpenSSL::X509::Certificate.new(File.read('/Users/laurent/workspace/Tools/certificate/myserver.crt'))
184
+ end
185
+ Log.log.info("Server started on port #{webrick_options[:Port]}")
186
+ @server = WEBrick::HTTPServer.new(webrick_options)
187
+ @server.mount('/aspera/faspex', FxGwServlet,a_aoc_api_user,a_workspace_id)
188
+ @server.mount('/newuser', NewUserServlet)
189
+ trap('INT') {@server.shutdown}
190
+ end
191
+
192
+ def start_server
193
+ @server.start
194
+ end
195
+ end # FaspexGW
196
+ end # AsperaLm
@@ -0,0 +1,28 @@
1
+ # for older rubies
2
+ unless Hash.method_defined?(:dig)
3
+ class Hash
4
+ def dig(*path)
5
+ path.inject(self) do |location, key|
6
+ location.respond_to?(:keys) ? location[key] : nil
7
+ end
8
+ end
9
+ end
10
+ end
11
+
12
+ class ::Hash
13
+ def deep_merge(second)
14
+ self.merge(second){|key,v1,v2|Hash===v1&&Hash===v2 ? v1.deep_merge(v2) : v2}
15
+ end
16
+
17
+ def deep_merge!(second)
18
+ self.merge!(second){|key,v1,v2|Hash===v1&&Hash===v2 ? v1.deep_merge!(v2) : v2}
19
+ end
20
+ end
21
+
22
+ unless Hash.method_defined?(:symbolize_keys)
23
+ class Hash
24
+ def symbolize_keys
25
+ return self.inject({}){|memo,(k,v)| memo[k.to_sym] = v; memo}
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,80 @@
1
+ require 'aspera/colors'
2
+ require 'logger'
3
+ require 'pp'
4
+ require 'json'
5
+ require 'singleton'
6
+
7
+ module Aspera
8
+ # Singleton object for logging
9
+ class Log
10
+
11
+ public
12
+ include Singleton
13
+
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.map{|c| c.downcase.to_sym};end
18
+
19
+ # where logs are sent to
20
+ def self.logtypes; [:stderr,:stdout,:syslog];end
21
+
22
+ # get the logger object of singleton
23
+ def self.log; self.instance.logger; end
24
+
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"
33
+ end
34
+ self.log.debug("#{name.to_s.green} (#{format})=\n#{result}")
35
+ end
36
+
37
+ # set log level of underlying logger given symbol level
38
+ def level=(new_level)
39
+ @logger.level=Logger::Severity.const_get(new_level.to_sym.upcase)
40
+ end
41
+
42
+ # get symbol of debug level of underlying logger
43
+ def level
44
+ Logger::Severity.constants.each do |name|
45
+ return name.downcase.to_sym if @logger.level.eql?(Logger::Severity.const_get(name))
46
+ end
47
+ raise "error"
48
+ end
49
+
50
+ # change underlying logger, but keep log level
51
+ def logger_type=(new_logtype)
52
+ current_severity_integer=@logger.nil? ? Logger::Severity::WARN : @logger.level
53
+ case new_logtype
54
+ when :stderr
55
+ @logger = Logger.new(STDERR)
56
+ when :stdout
57
+ @logger = Logger.new(STDOUT)
58
+ when :syslog
59
+ require 'syslog/logger'
60
+ @logger = Syslog::Logger.new(@program_name)
61
+ else
62
+ raise "unknown log type: #{new_logtype.class} #{new_logtype}"
63
+ end
64
+ @logger.level=current_severity_integer
65
+ @logger_type=new_logtype
66
+ end
67
+
68
+ attr_writer :program_name
69
+
70
+ private
71
+
72
+ def initialize
73
+ @logger=nil
74
+ @program_name='aspera'
75
+ # this sets @logger and @logger_type
76
+ self.logger_type=:stderr
77
+ end
78
+
79
+ end
80
+ end
@@ -0,0 +1,71 @@
1
+ require 'date'
2
+
3
+ module Aspera
4
+ class Nagios
5
+ # nagios levels
6
+ LEVELS=[:ok,:warning,:critical,:unknown,:dependent]
7
+ ADD_PREFIX='add_'
8
+ # add methods to add nagios error levels, each take component name and message
9
+ LEVELS.each_index do |code|
10
+ name="#{ADD_PREFIX}#{LEVELS[code]}".to_sym
11
+ define_method(name){|comp,msg|@data.push({:code=>code,:comp=>comp,:msg=>msg})}
12
+ public name
13
+ end
14
+ # date offset levels
15
+ DATE_WARN_OFFSET=2
16
+ DATE_CRIT_OFFSET=5
17
+ private_constant :LEVELS,:ADD_PREFIX,:DATE_WARN_OFFSET,:DATE_CRIT_OFFSET
18
+
19
+ attr_reader :data
20
+ def initialize
21
+ @data=[]
22
+ end
23
+
24
+ # comparte remote time with local time
25
+ def check_time_offset( remote_date, component )
26
+ # check date if specified : 2015-10-13T07:32:01Z
27
+ rtime = DateTime.strptime(remote_date)
28
+ diff_time = (rtime - DateTime.now).abs
29
+ diff_disp=diff_time.round(-2)
30
+ Log.log.debug("DATE: #{remote_date} #{rtime} diff=#{diff_disp}")
31
+ msg="offset #{diff_disp} sec"
32
+ if diff_time >= DATE_CRIT_OFFSET
33
+ add_critical(component,msg)
34
+ elsif diff_time >= DATE_WARN_OFFSET
35
+ add_warning(component,msg)
36
+ else
37
+ add_ok(component,msg)
38
+ end
39
+ end
40
+
41
+ def check_product_version( component, product, version )
42
+ add_ok(component,"version #{version}")
43
+ # TODO check on database if latest version
44
+ end
45
+
46
+ # translate for display
47
+ def result
48
+ raise "missing result" if @data.empty?
49
+ {:type=>:object_list,:data=>@data.map{|i|{'status'=>LEVELS[i[:code]].to_s,'component'=>i[:comp],'message'=>i[:msg]}}}
50
+ end
51
+
52
+ # process results of a analysis and display status and exit with code
53
+ def self.process(data)
54
+ raise "INTERNAL ERROR, result must be list and not empty" unless data.is_a?(Array) and !data.empty?
55
+ ['status','component','message'].each{|c|raise "INTERNAL ERROR, result must have #{c}" unless data.first.has_key?(c)}
56
+ res_errors = data.select{|s|!s['status'].eql?('ok')}
57
+ # keep only errors in case of problem, other ok are assumed so
58
+ data = res_errors unless res_errors.empty?
59
+ # first is most critical
60
+ data.sort!{|a,b|LEVELS.index(a['status'].to_sym)<=>LEVELS.index(b['status'].to_sym)}
61
+ # build message: if multiple components: concatenate
62
+ #message = data.map{|i|"#{i['component']}:#{i['message']}"}.join(', ').gsub("\n",' ')
63
+ message = data.map{|i|i['component']}.uniq.map{|comp|comp+':'+data.select{|d|d['component'].eql?(comp)}.map{|d|d['message']}.join(',')}.join(', ').gsub("\n",' ')
64
+ status=data.first['status'].upcase
65
+ # display status for nagios
66
+ puts("#{status} - [#{message}]\n")
67
+ # provide exit code to nagios
68
+ Process.exit(LEVELS.index(data.first['status'].to_sym))
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,14 @@
1
+ require 'zlib'
2
+ require 'base64'
3
+ module Aspera
4
+ # Provides additional functions using node API.
5
+ class Node < Rest
6
+ def self.decode_bearer_token(token)
7
+ return JSON.parse(Zlib::Inflate.inflate(Base64.decode64(token)).partition('==SIGNATURE==').first)
8
+ end
9
+ def initialize(rest_params)
10
+ super(rest_params)
11
+ # specifics here
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,319 @@
1
+ require 'aspera/open_application'
2
+ require 'base64'
3
+ require 'date'
4
+ require 'socket'
5
+ require 'securerandom'
6
+
7
+ module Aspera
8
+ # implement OAuth 2 for the REST client and generate a bearer token
9
+ # call get_authorization() to get a token.
10
+ # bearer tokens are kept in memory and also in a file cache for later re-use
11
+ # if a token is expired (api returns 4xx), call again get_authorization({:refresh=>true})
12
+ class Oauth
13
+ private
14
+ # remove 5 minutes to account for time offset (TODO: configurable?)
15
+ JWT_NOTBEFORE_OFFSET=300
16
+ # one hour validity (TODO: configurable?)
17
+ JWT_EXPIRY_OFFSET=3600
18
+ PERSIST_CATEGORY_TOKEN='token'
19
+ private_constant :JWT_NOTBEFORE_OFFSET,:JWT_EXPIRY_OFFSET,:PERSIST_CATEGORY_TOKEN
20
+ class << self
21
+ # OAuth methods supported
22
+ def auth_types
23
+ [ :body_userpass, :header_userpass, :web, :jwt, :url_token, :ibm_apikey ]
24
+ end
25
+
26
+ def persist_mgr=(manager)
27
+ @persist=manager
28
+ end
29
+
30
+ def persist_mgr
31
+ raise "set persistency manager first" if @persist.nil?
32
+ return @persist
33
+ end
34
+
35
+ def flush_tokens
36
+ persist_mgr.flush_by_prefix(PERSIST_CATEGORY_TOKEN)
37
+ end
38
+ end
39
+
40
+ # for supported parameters, look in the code for @params
41
+ # parameters are provided all with oauth_ prefix :
42
+ # :base_url
43
+ # :client_id
44
+ # :client_secret
45
+ # :redirect_uri
46
+ # :jwt_audience
47
+ # :jwt_private_key_obj
48
+ # :jwt_subject
49
+ # :path_authorize (default: 'authorize')
50
+ # :path_token (default: 'token')
51
+ # :scope (optional)
52
+ # :grant (one of returned by self.auth_types)
53
+ # :url_token
54
+ # :user_name
55
+ # :user_pass
56
+ # :token_type
57
+ def initialize(auth_params)
58
+ Log.log.debug "auth=#{auth_params}"
59
+ @params=auth_params.clone
60
+ # default values
61
+ # name of field to take as token from result of call to /token
62
+ @params[:token_field]||='access_token'
63
+ # default endpoint for /token
64
+ @params[:path_token]||='token'
65
+ # default endpoint for /authorize
66
+ @params[:path_authorize]||='authorize'
67
+ rest_params={:base_url => @params[:base_url]}
68
+ if @params.has_key?(:client_id)
69
+ rest_params.merge!({:auth => {
70
+ :type => :basic,
71
+ :username => @params[:client_id],
72
+ :password => @params[:client_secret]
73
+ }})
74
+ end
75
+ @token_auth_api=Rest.new(rest_params)
76
+ if @params.has_key?(:redirect_uri)
77
+ uri=URI.parse(@params[:redirect_uri])
78
+ raise "redirect_uri scheme must be http" unless uri.scheme.start_with?('http')
79
+ raise "redirect_uri must have a port" if uri.port.nil?
80
+ # we could check that host is localhost or local address
81
+ end
82
+ end
83
+
84
+ THANK_YOU_HTML = "<html><head><title>Ok</title></head><body><h1>Thank you !</h1><p>You can close this window.</p></body></html>"
85
+
86
+ # open the login page, wait for code and check_code, then return code
87
+ def goto_page_and_get_code(login_page_url,check_code)
88
+ request_params=self.class.goto_page_and_get_request(@params[:redirect_uri],login_page_url)
89
+ Log.log.error("state does not match") if !check_code.eql?(request_params['state'])
90
+ code=request_params['code']
91
+ return code
92
+ end
93
+
94
+ def create_token_advanced(rest_params)
95
+ return @token_auth_api.call({
96
+ :operation => 'POST',
97
+ :subpath => @params[:path_token],
98
+ :headers => {'Accept'=>'application/json'}}.merge(rest_params))
99
+ end
100
+
101
+ # shortcut for create_token_advanced
102
+ def create_token_www_body(creation_params)
103
+ return create_token_advanced({:www_body_params=>creation_params})
104
+ end
105
+
106
+ # @return Array list of unique identifiers of token
107
+ def token_cache_ids(api_scope)
108
+ oauth_uri=URI.parse(@params[:base_url])
109
+ parts=[PERSIST_CATEGORY_TOKEN,oauth_uri.host.downcase.gsub(/[^a-z]+/,'_'),oauth_uri.path.downcase.gsub(/[^a-z]+/,'_'),@params[:grant]]
110
+ parts.push(api_scope) unless api_scope.nil?
111
+ parts.push(@params[:jwt_subject]) if @params.has_key?(:jwt_subject)
112
+ parts.push(@params[:user_name]) if @params.has_key?(:user_name)
113
+ parts.push(@params[:url_token]) if @params.has_key?(:url_token)
114
+ parts.push(@params[:api_key]) if @params.has_key?(:api_key)
115
+ return parts
116
+ end
117
+
118
+ public
119
+
120
+ # @param options : :scope and :refresh
121
+ def get_authorization(options={})
122
+ # api scope can be overriden to get auth for other scope
123
+ api_scope=options[:scope] || @params[:scope]
124
+ # as it is optional in many place: create struct
125
+ p_scope={}
126
+ p_scope[:scope] = api_scope unless api_scope.nil?
127
+ p_client_id_and_scope=p_scope.clone
128
+ p_client_id_and_scope[:client_id] = @params[:client_id] if @params.has_key?(:client_id)
129
+ use_refresh_token=options[:refresh]
130
+
131
+ # generate token identifier to use with cache
132
+ token_ids=token_cache_ids(api_scope)
133
+
134
+ # get token_data from cache (or nil), token_data is what is returned by /token
135
+ token_data=self.class.persist_mgr.get(token_ids)
136
+ token_data=JSON.parse(token_data) unless token_data.nil?
137
+
138
+ # Optional optimization: check if node token is expired, then force refresh
139
+ # in case the transfer agent cannot refresh himself
140
+ # else, anyway, faspmanager is equipped with refresh code
141
+ if !token_data.nil?
142
+ decoded_node_token = Node.decode_bearer_token(token_data['access_token']) rescue nil
143
+ if decoded_node_token.is_a?(Hash) and decoded_node_token['expires_at'].is_a?(String)
144
+ Log.dump('decoded_node_token',decoded_node_token)
145
+ expires_at=DateTime.parse(decoded_node_token['expires_at'])
146
+ one_hour_as_day_fraction=Rational(1,24)
147
+ use_refresh_token=true if DateTime.now > (expires_at-one_hour_as_day_fraction)
148
+ end
149
+ end
150
+
151
+ # an API was already called, but failed, we need to regenerate or refresh
152
+ if use_refresh_token
153
+ if token_data.is_a?(Hash) and token_data.has_key?('refresh_token')
154
+ # save possible refresh token, before deleting the cache
155
+ refresh_token=token_data['refresh_token']
156
+ end
157
+ # delete caches
158
+ self.class.persist_mgr.delete(token_ids)
159
+ token_data=nil
160
+ # lets try the existing refresh token
161
+ if !refresh_token.nil?
162
+ Log.log.info("refresh=[#{refresh_token}]".bg_green)
163
+ # try to refresh
164
+ # note: admin token has no refresh, and lives by default 1800secs
165
+ # Note: scope is mandatory in Files, and we can either provide basic auth, or client_Secret in data
166
+ resp=create_token_www_body(p_client_id_and_scope.merge({
167
+ :grant_type =>'refresh_token',
168
+ :refresh_token=>refresh_token}))
169
+ if resp[:http].code.start_with?('2') then
170
+ # save only if success ?
171
+ json_data=resp[:http].body
172
+ token_data=JSON.parse(json_data)
173
+ self.class.persist_mgr.put(token_ids,json_data)
174
+ else
175
+ Log.log.debug("refresh failed: #{resp[:http].body}".bg_red)
176
+ end
177
+ end
178
+ end
179
+
180
+ # no cache
181
+ if token_data.nil? then
182
+ resp=nil
183
+ case @params[:grant]
184
+ when :web
185
+ # AoC Web based Auth
186
+ check_code=SecureRandom.uuid
187
+ login_page_url=Rest.build_uri(
188
+ "#{@params[:base_url]}/#{@params[:path_authorize]}",
189
+ p_client_id_and_scope.merge({
190
+ :response_type => 'code',
191
+ :redirect_uri => @params[:redirect_uri],
192
+ :client_secret => @params[:client_secret],
193
+ :state => check_code
194
+ }))
195
+ # here, we need a human to authorize on a web page
196
+ code=goto_page_and_get_code(login_page_url,check_code)
197
+ # exchange code for token
198
+ resp=create_token_www_body(p_client_id_and_scope.merge({
199
+ :grant_type => 'authorization_code',
200
+ :code => code,
201
+ :redirect_uri => @params[:redirect_uri]
202
+ }))
203
+ when :jwt
204
+ # https://tools.ietf.org/html/rfc7519
205
+ # https://tools.ietf.org/html/rfc7523
206
+ require 'jwt'
207
+ seconds_since_epoch=Time.new.to_i
208
+ Log.log.info("seconds=#{seconds_since_epoch}")
209
+
210
+ payload = {
211
+ :iss => @params[:client_id], # issuer
212
+ :sub => @params[:jwt_subject], # subject
213
+ :aud => @params[:jwt_audience], # audience
214
+ :nbf => seconds_since_epoch-JWT_NOTBEFORE_OFFSET, # not before
215
+ :exp => seconds_since_epoch+JWT_EXPIRY_OFFSET # expiration
216
+ }
217
+
218
+ # non standard, only for global ids
219
+ payload.merge!(@params[:jwt_add]) if @params.has_key?(:jwt_add)
220
+
221
+ rsa_private=@params[:jwt_private_key_obj] # type: OpenSSL::PKey::RSA
222
+
223
+ Log.log.debug("private=[#{rsa_private}]")
224
+
225
+ Log.log.debug("JWT assertion=[#{payload}]")
226
+ assertion = JWT.encode(payload, rsa_private, 'RS256')
227
+
228
+ Log.log.debug("assertion=[#{assertion}]")
229
+
230
+ resp=create_token_www_body(p_scope.merge({
231
+ :grant_type => 'urn:ietf:params:oauth:grant-type:jwt-bearer',
232
+ :assertion => assertion
233
+ }))
234
+ when :url_token
235
+ # AoC Public Link
236
+ resp=create_token_advanced({
237
+ :json_params => {:url_token=>@params[:url_token]},
238
+ :url_params => p_scope.merge({
239
+ :grant_type => 'url_token'
240
+ })})
241
+ when :ibm_apikey
242
+ # ATS
243
+ resp=create_token_www_body({
244
+ 'grant_type' => 'urn:ibm:params:oauth:grant-type:apikey',
245
+ 'response_type' => 'cloud_iam',
246
+ 'apikey' => @params[:api_key]
247
+ })
248
+ when :delegated_refresh
249
+ # COS
250
+ resp=create_token_www_body({
251
+ 'grant_type' => 'urn:ibm:params:oauth:grant-type:apikey',
252
+ 'response_type' => 'delegated_refresh_token',
253
+ 'apikey' => @params[:api_key],
254
+ 'receiver_client_ids' => 'aspera_ats'
255
+ })
256
+ when :header_userpass
257
+ # used in Faspex apiv4 and shares2
258
+ resp=create_token_advanced({
259
+ :auth => {
260
+ :type => :basic,
261
+ :username => @params[:user_name],
262
+ :password => @params[:user_pass]},
263
+ :json_params => p_client_id_and_scope.merge({:grant_type => 'password'}), #:www_body_params also works
264
+ })
265
+ when :body_userpass
266
+ # legacy, not used
267
+ resp=create_token_www_body(p_client_id_and_scope.merge({
268
+ :grant_type => 'password',
269
+ :username => @params[:user_name],
270
+ :password => @params[:user_pass]
271
+ }))
272
+ when :body_data
273
+ # used in Faspex apiv5
274
+ resp=create_token_advanced({
275
+ :auth => {:type => :none},
276
+ :json_params => @params[:userpass_body],
277
+ })
278
+ else
279
+ raise "auth grant type unknown: #{@params[:grant]}"
280
+ end
281
+ # TODO: test return code ?
282
+ json_data=resp[:http].body
283
+ token_data=JSON.parse(json_data)
284
+ self.class.persist_mgr.put(token_ids,json_data)
285
+ end # if ! in_cache
286
+
287
+ # ok we shall have a token here
288
+ return 'Bearer '+token_data[@params[:token_field]]
289
+ end
290
+
291
+ # open the login page, wait for code and return parameters
292
+ def self.goto_page_and_get_request(redirect_uri,login_page_url,html_page=THANK_YOU_HTML)
293
+ Log.log.info "login_page_url=#{login_page_url}".bg_red().gray()
294
+ # browser start is not blocking, we hope here that starting is slower than opening port
295
+ OpenApplication.instance.uri(login_page_url)
296
+ port=URI.parse(redirect_uri).port
297
+ Log.log.info "listening on port #{port}"
298
+ request_params=nil
299
+ TCPServer.open('127.0.0.1', port) { |webserver|
300
+ Log.log.info "server=#{webserver}"
301
+ websession = webserver.accept
302
+ sleep 1 # TODO: sometimes: returns nil ? use webrick ?
303
+ line = websession.gets.chomp
304
+ Log.log.info "line=#{line}"
305
+ if ! line.start_with?('GET /?') then
306
+ raise "unexpected request"
307
+ end
308
+ request = line.partition('?').last.partition(' ').first
309
+ data=URI.decode_www_form(request)
310
+ request_params=data.to_h
311
+ Log.log.debug "request_params=#{request_params}"
312
+ websession.print "HTTP/1.1 200/OK\r\nContent-type:text/html\r\n\r\n#{html_page}"
313
+ websession.close
314
+ }
315
+ return request_params
316
+ end
317
+
318
+ end # OAuth
319
+ end # Aspera