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.
- checksums.yaml +7 -0
- data/README.md +3592 -0
- data/bin/ascli +7 -0
- data/bin/asession +89 -0
- data/docs/Makefile +59 -0
- data/docs/README.erb.md +3012 -0
- data/docs/README.md +13 -0
- data/docs/diagrams.txt +49 -0
- data/docs/secrets.make +38 -0
- data/docs/test_env.conf +117 -0
- data/docs/transfer_spec.html +99 -0
- data/examples/aoc.rb +17 -0
- data/examples/proxy.pac +60 -0
- data/examples/transfer.rb +115 -0
- data/lib/aspera/api_detector.rb +60 -0
- data/lib/aspera/ascmd.rb +151 -0
- data/lib/aspera/ats_api.rb +43 -0
- data/lib/aspera/cli/basic_auth_plugin.rb +38 -0
- data/lib/aspera/cli/extended_value.rb +88 -0
- data/lib/aspera/cli/formater.rb +238 -0
- data/lib/aspera/cli/listener/line_dump.rb +17 -0
- data/lib/aspera/cli/listener/logger.rb +20 -0
- data/lib/aspera/cli/listener/progress.rb +52 -0
- data/lib/aspera/cli/listener/progress_multi.rb +91 -0
- data/lib/aspera/cli/main.rb +304 -0
- data/lib/aspera/cli/manager.rb +440 -0
- data/lib/aspera/cli/plugin.rb +90 -0
- data/lib/aspera/cli/plugins/alee.rb +24 -0
- data/lib/aspera/cli/plugins/ats.rb +231 -0
- data/lib/aspera/cli/plugins/bss.rb +71 -0
- data/lib/aspera/cli/plugins/config.rb +806 -0
- data/lib/aspera/cli/plugins/console.rb +62 -0
- data/lib/aspera/cli/plugins/cos.rb +106 -0
- data/lib/aspera/cli/plugins/faspex.rb +377 -0
- data/lib/aspera/cli/plugins/faspex5.rb +93 -0
- data/lib/aspera/cli/plugins/node.rb +438 -0
- data/lib/aspera/cli/plugins/oncloud.rb +937 -0
- data/lib/aspera/cli/plugins/orchestrator.rb +169 -0
- data/lib/aspera/cli/plugins/preview.rb +464 -0
- data/lib/aspera/cli/plugins/server.rb +216 -0
- data/lib/aspera/cli/plugins/shares.rb +63 -0
- data/lib/aspera/cli/plugins/shares2.rb +114 -0
- data/lib/aspera/cli/plugins/sync.rb +65 -0
- data/lib/aspera/cli/plugins/xnode.rb +115 -0
- data/lib/aspera/cli/transfer_agent.rb +251 -0
- data/lib/aspera/cli/version.rb +5 -0
- data/lib/aspera/colors.rb +39 -0
- data/lib/aspera/command_line_builder.rb +137 -0
- data/lib/aspera/fasp/aoc.rb +24 -0
- data/lib/aspera/fasp/connect.rb +99 -0
- data/lib/aspera/fasp/error.rb +21 -0
- data/lib/aspera/fasp/error_info.rb +60 -0
- data/lib/aspera/fasp/http_gw.rb +81 -0
- data/lib/aspera/fasp/installation.rb +240 -0
- data/lib/aspera/fasp/listener.rb +11 -0
- data/lib/aspera/fasp/local.rb +377 -0
- data/lib/aspera/fasp/manager.rb +69 -0
- data/lib/aspera/fasp/node.rb +88 -0
- data/lib/aspera/fasp/parameters.rb +235 -0
- data/lib/aspera/fasp/resume_policy.rb +76 -0
- data/lib/aspera/fasp/uri.rb +51 -0
- data/lib/aspera/faspex_gw.rb +196 -0
- data/lib/aspera/hash_ext.rb +28 -0
- data/lib/aspera/log.rb +80 -0
- data/lib/aspera/nagios.rb +71 -0
- data/lib/aspera/node.rb +14 -0
- data/lib/aspera/oauth.rb +319 -0
- data/lib/aspera/on_cloud.rb +421 -0
- data/lib/aspera/open_application.rb +72 -0
- data/lib/aspera/persistency_action_once.rb +42 -0
- data/lib/aspera/persistency_folder.rb +91 -0
- data/lib/aspera/preview/file_types.rb +300 -0
- data/lib/aspera/preview/generator.rb +258 -0
- data/lib/aspera/preview/image_error.png +0 -0
- data/lib/aspera/preview/options.rb +35 -0
- data/lib/aspera/preview/utils.rb +131 -0
- data/lib/aspera/preview/video_error.png +0 -0
- data/lib/aspera/proxy_auto_config.erb.js +287 -0
- data/lib/aspera/proxy_auto_config.rb +34 -0
- data/lib/aspera/rest.rb +296 -0
- data/lib/aspera/rest_call_error.rb +13 -0
- data/lib/aspera/rest_error_analyzer.rb +98 -0
- data/lib/aspera/rest_errors_aspera.rb +58 -0
- data/lib/aspera/ssh.rb +53 -0
- data/lib/aspera/sync.rb +82 -0
- data/lib/aspera/temp_file_manager.rb +37 -0
- data/lib/aspera/uri_reader.rb +25 -0
- 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
|
data/lib/aspera/log.rb
ADDED
@@ -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
|
data/lib/aspera/node.rb
ADDED
@@ -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
|
data/lib/aspera/oauth.rb
ADDED
@@ -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
|