aspera-cli 4.9.0 → 4.11.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 (95) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/BUGS.md +20 -0
  4. data/CHANGELOG.md +509 -0
  5. data/CONTRIBUTING.md +118 -0
  6. data/README.md +1241 -916
  7. data/bin/ascli +4 -4
  8. data/bin/asession +11 -11
  9. data/docs/test_env.conf +32 -21
  10. data/examples/aoc.rb +4 -4
  11. data/examples/dascli +16 -9
  12. data/examples/faspex4.rb +8 -8
  13. data/examples/node.rb +12 -12
  14. data/examples/server.rb +10 -10
  15. data/lib/aspera/aoc.rb +273 -266
  16. data/lib/aspera/ascmd.rb +56 -54
  17. data/lib/aspera/ats_api.rb +4 -4
  18. data/lib/aspera/cli/basic_auth_plugin.rb +15 -12
  19. data/lib/aspera/cli/extended_value.rb +5 -5
  20. data/lib/aspera/cli/formater.rb +64 -64
  21. data/lib/aspera/cli/info.rb +2 -2
  22. data/lib/aspera/cli/listener/line_dump.rb +1 -1
  23. data/lib/aspera/cli/listener/logger.rb +1 -1
  24. data/lib/aspera/cli/listener/progress.rb +5 -6
  25. data/lib/aspera/cli/listener/progress_multi.rb +14 -19
  26. data/lib/aspera/cli/main.rb +66 -67
  27. data/lib/aspera/cli/manager.rb +112 -110
  28. data/lib/aspera/cli/plugin.rb +57 -36
  29. data/lib/aspera/cli/plugins/alee.rb +4 -4
  30. data/lib/aspera/cli/plugins/aoc.rb +309 -670
  31. data/lib/aspera/cli/plugins/ats.rb +44 -46
  32. data/lib/aspera/cli/plugins/bss.rb +10 -10
  33. data/lib/aspera/cli/plugins/config.rb +497 -378
  34. data/lib/aspera/cli/plugins/console.rb +12 -12
  35. data/lib/aspera/cli/plugins/cos.rb +18 -20
  36. data/lib/aspera/cli/plugins/faspex.rb +112 -114
  37. data/lib/aspera/cli/plugins/faspex5.rb +71 -46
  38. data/lib/aspera/cli/plugins/node.rb +379 -283
  39. data/lib/aspera/cli/plugins/orchestrator.rb +46 -46
  40. data/lib/aspera/cli/plugins/preview.rb +122 -114
  41. data/lib/aspera/cli/plugins/server.rb +137 -83
  42. data/lib/aspera/cli/plugins/shares.rb +30 -29
  43. data/lib/aspera/cli/plugins/sync.rb +13 -33
  44. data/lib/aspera/cli/transfer_agent.rb +60 -59
  45. data/lib/aspera/cli/version.rb +1 -1
  46. data/lib/aspera/colors.rb +3 -3
  47. data/lib/aspera/command_line_builder.rb +27 -27
  48. data/lib/aspera/cos_node.rb +22 -20
  49. data/lib/aspera/data_repository.rb +1 -1
  50. data/lib/aspera/environment.rb +35 -15
  51. data/lib/aspera/fasp/agent_base.rb +15 -15
  52. data/lib/aspera/fasp/agent_connect.rb +23 -21
  53. data/lib/aspera/fasp/agent_direct.rb +66 -64
  54. data/lib/aspera/fasp/agent_httpgw.rb +141 -78
  55. data/lib/aspera/fasp/agent_node.rb +23 -21
  56. data/lib/aspera/fasp/agent_trsdk.rb +20 -20
  57. data/lib/aspera/fasp/error.rb +3 -2
  58. data/lib/aspera/fasp/error_info.rb +11 -8
  59. data/lib/aspera/fasp/installation.rb +79 -79
  60. data/lib/aspera/fasp/listener.rb +1 -1
  61. data/lib/aspera/fasp/parameters.rb +86 -71
  62. data/lib/aspera/fasp/parameters.yaml +7 -4
  63. data/lib/aspera/fasp/resume_policy.rb +8 -8
  64. data/lib/aspera/fasp/transfer_spec.rb +35 -2
  65. data/lib/aspera/fasp/uri.rb +7 -7
  66. data/lib/aspera/faspex_gw.rb +7 -5
  67. data/lib/aspera/hash_ext.rb +3 -3
  68. data/lib/aspera/id_generator.rb +5 -5
  69. data/lib/aspera/keychain/encrypted_hash.rb +38 -105
  70. data/lib/aspera/keychain/macos_security.rb +128 -57
  71. data/lib/aspera/log.rb +7 -7
  72. data/lib/aspera/nagios.rb +19 -18
  73. data/lib/aspera/node.rb +209 -35
  74. data/lib/aspera/oauth.rb +37 -36
  75. data/lib/aspera/open_application.rb +19 -11
  76. data/lib/aspera/persistency_action_once.rb +4 -4
  77. data/lib/aspera/persistency_folder.rb +16 -15
  78. data/lib/aspera/preview/file_types.rb +8 -8
  79. data/lib/aspera/preview/generator.rb +67 -67
  80. data/lib/aspera/preview/utils.rb +27 -27
  81. data/lib/aspera/proxy_auto_config.js +41 -41
  82. data/lib/aspera/proxy_auto_config.rb +21 -14
  83. data/lib/aspera/rest.rb +72 -67
  84. data/lib/aspera/rest_call_error.rb +2 -1
  85. data/lib/aspera/rest_error_analyzer.rb +18 -17
  86. data/lib/aspera/rest_errors_aspera.rb +16 -16
  87. data/lib/aspera/secret_hider.rb +15 -13
  88. data/lib/aspera/ssh.rb +11 -10
  89. data/lib/aspera/sync.rb +158 -44
  90. data/lib/aspera/temp_file_manager.rb +2 -2
  91. data/lib/aspera/uri_reader.rb +4 -4
  92. data/lib/aspera/web_auth.rb +14 -13
  93. data.tar.gz.sig +0 -0
  94. metadata +11 -36
  95. metadata.gz.sig +0 -0
data/lib/aspera/nagios.rb CHANGED
@@ -10,32 +10,32 @@ module Aspera
10
10
  # date offset levels
11
11
  DATE_WARN_OFFSET = 2
12
12
  DATE_CRIT_OFFSET = 5
13
- private_constant :LEVELS,:ADD_PREFIX,:DATE_WARN_OFFSET,:DATE_CRIT_OFFSET
13
+ private_constant :LEVELS, :ADD_PREFIX, :DATE_WARN_OFFSET, :DATE_CRIT_OFFSET
14
14
 
15
15
  # add methods to add nagios error levels, each take component name and message
16
16
  LEVELS.each_index do |code|
17
17
  name = "#{ADD_PREFIX}#{LEVELS[code]}".to_sym
18
- define_method(name){|comp,msg|@data.push({code: code,comp: comp,msg: msg})}
18
+ define_method(name){|comp, msg|@data.push({code: code, comp: comp, msg: msg})}
19
19
  end
20
20
 
21
21
  class << self
22
22
  # process results of a analysis and display status and exit with code
23
23
  def process(data)
24
24
  raise 'INTERNAL ERROR, result must be list and not empty' unless data.is_a?(Array) && !data.empty?
25
- %w[status component message].each{|c|raise "INTERNAL ERROR, result must have #{c}" unless data.first.has_key?(c)}
25
+ %w[status component message].each{|c|raise "INTERNAL ERROR, result must have #{c}" unless data.first.key?(c)}
26
26
  res_errors = data.reject{|s|s['status'].eql?('ok')}
27
27
  # keep only errors in case of problem, other ok are assumed so
28
28
  data = res_errors unless res_errors.empty?
29
29
  # first is most critical
30
- data.sort!{|a,b|LEVELS.index(a['status'].to_sym) <=> LEVELS.index(b['status'].to_sym)}
30
+ data.sort!{|a, b|LEVELS.index(a['status'].to_sym) <=> LEVELS.index(b['status'].to_sym)}
31
31
  # build message: if multiple components: concatenate
32
- #message = data.map{|i|"#{i['component']}:#{i['message']}"}.join(', ').gsub("\n",' ')
33
- message = data.
34
- map{|i|i['component']}.
35
- uniq.
36
- map{|comp|comp + ':' + data.select{|d|d['component'].eql?(comp)}.map{|d|d['message']}.join(',')}.
37
- join(', ').
38
- tr("\n",' ')
32
+ # message = data.map{|i|"#{i['component']}:#{i['message']}"}.join(', ').gsub("\n",' ')
33
+ message = data
34
+ .map{|i|i['component']}
35
+ .uniq
36
+ .map{|comp|comp + ':' + data.select{|d|d['component'].eql?(comp)}.map{|d|d['message']}.join(',')}
37
+ .join(', ')
38
+ .tr("\n", ' ')
39
39
  status = data.first['status'].upcase
40
40
  # display status for nagios
41
41
  puts("#{status} - [#{message}]\n")
@@ -45,6 +45,7 @@ module Aspera
45
45
  end
46
46
 
47
47
  attr_reader :data
48
+
48
49
  def initialize
49
50
  @data = []
50
51
  end
@@ -55,26 +56,26 @@ module Aspera
55
56
  rtime = DateTime.strptime(remote_date)
56
57
  diff_time = (rtime - DateTime.now).abs
57
58
  diff_disp = diff_time.round(-2)
58
- Log.log.debug("DATE: #{remote_date} #{rtime} diff=#{diff_disp}")
59
+ Log.log.debug{"DATE: #{remote_date} #{rtime} diff=#{diff_disp}"}
59
60
  msg = "offset #{diff_disp} sec"
60
61
  if diff_time >= DATE_CRIT_OFFSET
61
- add_critical(component,msg)
62
+ add_critical(component, msg)
62
63
  elsif diff_time >= DATE_WARN_OFFSET
63
- add_warning(component,msg)
64
+ add_warning(component, msg)
64
65
  else
65
- add_ok(component,msg)
66
+ add_ok(component, msg)
66
67
  end
67
68
  end
68
69
 
69
70
  def check_product_version(component, _product, version)
70
- add_ok(component,"version #{version}")
71
- # TODO check on database if latest version
71
+ add_ok(component, "version #{version}")
72
+ # TODO: check on database if latest version
72
73
  end
73
74
 
74
75
  # translate for display
75
76
  def result
76
77
  raise 'missing result' if @data.empty?
77
- {type: :object_list,data: @data.map{|i|{'status' => LEVELS[i[:code]].to_s,'component' => i[:comp],'message' => i[:msg]}}}
78
+ {type: :object_list, data: @data.map{|i|{'status' => LEVELS[i[:code]].to_s, 'component' => i[:comp], 'message' => i[:msg]}}}
78
79
  end
79
80
  end
80
81
  end
data/lib/aspera/node.rb CHANGED
@@ -9,22 +9,23 @@ require 'zlib'
9
9
  require 'base64'
10
10
 
11
11
  module Aspera
12
- # Provides additional functions using node API.
13
- class Node < Rest
12
+ # Provides additional functions using node API with gen4 extensions (access keys)
13
+ class Node < Aspera::Rest
14
14
  # permissions
15
15
  ACCESS_LEVELS = %w[delete list mkdir preview read rename write].freeze
16
16
  # prefix for ruby code for filter
17
17
  MATCH_EXEC_PREFIX = 'exec:'
18
+ X_ASPERA_ACCESSKEY = 'X-Aspera-AccessKey'
19
+ PATH_SEPARATOR = '/'
18
20
 
19
21
  # register node special token decoder
20
22
  Oauth.register_decoder(lambda{|token|JSON.parse(Zlib::Inflate.inflate(Base64.decode64(token)).partition('==SIGNATURE==').first)})
21
23
 
24
+ # class instance variable, access with accessors on class
25
+ @use_standard_ports = true
26
+
22
27
  class << self
23
- def set_ak_basic_token(ts,ak,secret)
24
- Log.log.warn("Expected transfer user: #{Fasp::TransferSpec::ACCESS_KEY_TRANSFER_USER}, "\
25
- "but have #{ts['remote_user']}") unless ts['remote_user'].eql?(Fasp::TransferSpec::ACCESS_KEY_TRANSFER_USER)
26
- ts['token'] = Rest.basic_creds(ak,secret)
27
- end
28
+ attr_accessor :use_standard_ports
28
29
 
29
30
  # for access keys: provide expression to match entry in folder
30
31
  # if no prefix: regex
@@ -39,49 +40,222 @@ module Aspera
39
40
  end
40
41
  end
41
42
 
42
- # def initialize(rest_params)
43
- # super(rest_params)
44
- # end
43
+ REQUIRED_APP_INFO_FIELDS = %i[node_info app api plugin].freeze
44
+ REQUIRED_APP_API_METHODS = %i[node_id_to_api add_ts_tags].freeze
45
+ private_constant :REQUIRED_APP_INFO_FIELDS, :REQUIRED_APP_API_METHODS
46
+
47
+ attr_reader :app_info
48
+
49
+ # @param params [Hash] Rest parameters
50
+ # @param app_info [Hash,NilClass] special processing for AoC
51
+ def initialize(params:, app_info: nil, add_tspec: nil)
52
+ super(params)
53
+ @app_info = app_info
54
+ # this is added to transfer spec, for instance to add tags (COS)
55
+ @add_tspec = add_tspec
56
+ if !@app_info.nil?
57
+ REQUIRED_APP_INFO_FIELDS.each do |field|
58
+ raise "INTERNAL ERROR: app_info lacks field #{field}" unless @app_info.key?(field)
59
+ end
60
+ REQUIRED_APP_API_METHODS.each do |method|
61
+ raise "INTERNAL ERROR: #{@app_info[:api].class} lacks method #{method}" unless @app_info[:api].respond_to?(method)
62
+ end
63
+ end
64
+ end
65
+
66
+ # update transfer spec with special additional tags
67
+ def add_tspec_info(tspec)
68
+ tspec.deep_merge!(@add_tspec) unless @add_tspec.nil?
69
+ return tspec
70
+ end
71
+
72
+ # @returns [Aspera::Node] a Node or nil
73
+ def node_id_to_node(node_id)
74
+ return self if !@app_info.nil? && @app_info[:node_info]['id'].eql?(node_id)
75
+ return @app_info[:api].node_id_to_api(node_id) unless @app_info.nil?
76
+ Log.log.warn{"cannot resolve link with node id #{node_id}"}
77
+ return nil
78
+ end
45
79
 
46
- # recursively crawl in a folder.
80
+ # recursively browse in a folder (with non-recursive method)
47
81
  # subfolders a processed if the processing method returns true
48
- # @param processor must provide a method to process each entry
49
- # @param opt options
50
- # - top_file_id file id to start at (default = access key root file id)
51
- # - top_file_path path of top folder (default = /)
52
- # - method processing method (default= process_entry)
53
- def crawl(processor,opt={})
54
- Log.log.debug("crawl1 #{opt}")
55
- # not possible with bearer token
56
- opt[:top_file_id] ||= read('access_keys/self')[:data]['root_file_id']
57
- opt[:top_file_path] ||= '/'
58
- opt[:method] ||= :process_entry
59
- raise "processor must have #{opt[:method]}" unless processor.respond_to?(opt[:method])
60
- Log.log.debug("crawl #{opt}")
61
- #top_info=read("files/#{opt[:top_file_id]}")[:data]
62
- folders_to_explore = [{id: opt[:top_file_id], relpath: opt[:top_file_path]}]
63
- Log.dump(:folders_to_explore,folders_to_explore)
64
- while !folders_to_explore.empty?
82
+ # @param state [Object] state object sent to processing method
83
+ # @param method [Symbol] processing method name
84
+ # @param top_file_id [String] file id to start at (default = access key root file id)
85
+ # @param top_file_path [String] path of top folder (default = /)
86
+ def process_folder_tree(state:, method:, top_file_id:, top_file_path: '/')
87
+ raise 'INTERNAL ERROR: top_file_path not set' if top_file_path.nil?
88
+ raise "INTERNAL ERROR: Missing method #{method}" unless respond_to?(method)
89
+ folders_to_explore = [{id: top_file_id, relpath: top_file_path}]
90
+ Log.dump(:folders_to_explore, folders_to_explore)
91
+ until folders_to_explore.empty?
65
92
  current_item = folders_to_explore.shift
66
- Log.log.debug("searching #{current_item[:relpath]}".bg_green)
93
+ Log.log.debug{"searching #{current_item[:relpath]}".bg_green}
67
94
  # get folder content
68
95
  folder_contents =
69
96
  begin
70
97
  read("files/#{current_item[:id]}/files")[:data]
71
98
  rescue StandardError => e
72
- Log.log.warn("#{current_item[:relpath]}: #{e.class} #{e.message}")
99
+ Log.log.warn{"#{current_item[:relpath]}: #{e.class} #{e.message}"}
73
100
  []
74
101
  end
75
- Log.dump(:folder_contents,folder_contents)
102
+ Log.dump(:folder_contents, folder_contents)
76
103
  folder_contents.each do |entry|
77
- relative_path = File.join(current_item[:relpath],entry['name'])
78
- Log.log.debug("looking #{relative_path}".bg_green)
104
+ relative_path = File.join(current_item[:relpath], entry['name'])
105
+ Log.log.debug{"looking #{relative_path}".bg_green}
106
+ # continue only if method returns true
107
+ next unless send(method, entry, relative_path, state)
79
108
  # entry type is file, folder or link
80
- if processor.send(opt[:method],entry,relative_path) && entry['type'].eql?('folder')
81
- folders_to_explore.push({id: entry['id'],relpath: relative_path})
109
+ case entry['type']
110
+ when 'folder'
111
+ folders_to_explore.push({id: entry['id'], relpath: relative_path})
112
+ when 'link'
113
+ node_id_to_node(entry['target_node_id'])&.process_folder_tree(
114
+ state: state,
115
+ method: method,
116
+ top_file_id: entry['target_id'],
117
+ top_file_path: relative_path)
82
118
  end
83
119
  end
84
120
  end
121
+ end # process_folder_tree
122
+
123
+ # processing method to resolve a file path to id
124
+ # @returns true if processing need to continue
125
+ def process_resolve_node_path(entry, _path, state)
126
+ # stop digging here if not in right path
127
+ return false unless entry['name'].eql?(state[:path].first)
128
+ # ok it matches, so we remove the match
129
+ state[:path].shift
130
+ case entry['type']
131
+ when 'file'
132
+ # file must be terminal
133
+ raise "#{entry['name']} is a file, expecting folder to find: #{state[:path]}" unless state[:path].empty?
134
+ # it's terminal, we found it
135
+ state[:result] = {api: self, file_id: entry['id']}
136
+ return false
137
+ when 'folder'
138
+ if state[:path].empty?
139
+ # we found it
140
+ state[:result] = {api: self, file_id: entry['id']}
141
+ return false
142
+ end
143
+ when 'link'
144
+ if state[:path].empty?
145
+ # we found it
146
+ other_node = node_id_to_node(entry['target_node_id'])
147
+ raise 'cannot resolve link' if other_node.nil?
148
+ state[:result] = {api: other_node, file_id: entry['target_id']}
149
+ return false
150
+ end
151
+ else
152
+ Log.log.warn{"Unknown element type: #{entry['type']}"}
153
+ end
154
+ # continue to dig folder
155
+ return true
156
+ end
157
+
158
+ # Navigate the path from given file id
159
+ # @param id initial file id
160
+ # @param path file path
161
+ # @return {.api,.file_id}
162
+ def resolve_api_fid(top_file_id, path)
163
+ raise 'file id shall be String' unless top_file_id.is_a?(String)
164
+ path_elements = path.split(PATH_SEPARATOR).reject(&:empty?)
165
+ return {api: self, file_id: top_file_id} if path_elements.empty?
166
+ resolve_state = {path: path_elements, result: nil}
167
+ process_folder_tree(state: resolve_state, method: :process_resolve_node_path, top_file_id: top_file_id)
168
+ raise "entry not found: #{resolve_state[:path]}" if resolve_state[:result].nil?
169
+ return resolve_state[:result]
170
+ end
171
+
172
+ # add entry to list if test block is success
173
+ def process_find_files(entry, path, state)
174
+ begin
175
+ # add to result if match filter
176
+ state[:found].push(entry.merge({'path' => path})) if state[:test_block].call(entry)
177
+ # process link
178
+ if entry[:type].eql?('link')
179
+ other_node = node_id_to_node(entry['target_node_id'])
180
+ other_node.process_folder_tree(state: state, method: process_find_files, top_file_id: entry['target_id'], top_file_path: path)
181
+ end
182
+ rescue StandardError => e
183
+ Log.log.error{"#{path}: #{e.message}"}
184
+ end
185
+ # process all folders
186
+ return true
187
+ end
188
+
189
+ def find_files(top_file_id, test_block)
190
+ Log.log.debug{"find_files: fileid=#{top_file_id}"}
191
+ find_state = {found: [], test_block: test_block}
192
+ process_folder_tree(state: find_state, method: :process_find_files, top_file_id: top_file_id)
193
+ return find_state[:found]
194
+ end
195
+
196
+ # Create transfer spec for gen4
197
+ def transfer_spec_gen4(file_id, direction, ts_merge=nil)
198
+ ak_name = nil
199
+ ak_token = nil
200
+ case params[:auth][:type]
201
+ when :basic
202
+ ak_name = params[:auth][:username]
203
+ when :oauth2
204
+ ak_name = params[:headers][X_ASPERA_ACCESSKEY]
205
+ token_generation_lambda = lambda{|do_refresh|oauth_token(force_refresh: do_refresh)}
206
+ ak_token = token_generation_lambda.call(false) # first time, use cache
207
+ @app_info[:plugin].transfer.token_regenerator = token_generation_lambda unless @app_info.nil?
208
+ else raise "Unsupported auth method for node gen4: #{params[:auth][:type]}"
209
+ end
210
+ transfer_spec = {
211
+ 'direction' => direction,
212
+ 'token' => ak_token,
213
+ 'tags' => {
214
+ 'aspera' => {
215
+ 'node' => {
216
+ 'access_key' => ak_name,
217
+ 'file_id' => file_id
218
+ } # node
219
+ } # aspera
220
+ } # tags
221
+ }
222
+ # add specials tags (cos)
223
+ add_tspec_info(transfer_spec)
224
+ transfer_spec.deep_merge!(ts_merge) unless ts_merge.nil?
225
+ # add application specific tags (AoC)
226
+ the_app = app_info
227
+ the_app[:api].add_ts_tags(transfer_spec: transfer_spec, app_info: the_app) unless the_app.nil?
228
+ # add basic token
229
+ if transfer_spec['token'].nil?
230
+ ts_basic_token(transfer_spec)
231
+ end
232
+ # add remote host info
233
+ if self.class.use_standard_ports
234
+ # get default TCP/UDP ports and transfer user
235
+ transfer_spec.merge!(Fasp::TransferSpec::AK_TSPEC_BASE)
236
+ # by default: same address as node API
237
+ transfer_spec['remote_host'] = URI.parse(params[:base_url]).host
238
+ if !@app_info.nil? && !@app_info[:node_info]['transfer_url'].nil? && !@app_info[:node_info]['transfer_url'].empty?
239
+ transfer_spec['remote_host'] = @app_info[:node_info]['transfer_url']
240
+ end
241
+ else
242
+ # retrieve values from API
243
+ std_t_spec = create(
244
+ 'files/download_setup',
245
+ {transfer_requests: [{ transfer_request: {paths: [{'source' => '/'}] } }] }
246
+ )[:data]['transfer_specs'].first['transfer_spec']
247
+ # copy some parts
248
+ %w[remote_host remote_user ssh_port fasp_port wss_enabled wss_port].each {|i| transfer_spec[i] = std_t_spec[i] if std_t_spec.key?(i)}
249
+ end
250
+ return transfer_spec
251
+ end
252
+
253
+ # set basic token in transfer spec
254
+ def ts_basic_token(ts)
255
+ Log.log.warn{"Expected transfer user: #{Fasp::TransferSpec::ACCESS_KEY_TRANSFER_USER}, but have #{ts['remote_user']}"} \
256
+ unless ts['remote_user'].eql?(Fasp::TransferSpec::ACCESS_KEY_TRANSFER_USER)
257
+ raise 'ERROR: no secret in node object' unless params[:auth][:password]
258
+ ts['token'] = Rest.basic_creds(params[:auth][:username], params[:auth][:password])
85
259
  end
86
260
  end
87
261
  end
data/lib/aspera/oauth.rb CHANGED
@@ -35,7 +35,7 @@ module Aspera
35
35
  # a prefix for persistency of tokens (simplify garbage collect)
36
36
  PERSIST_CATEGORY_TOKEN = 'token'
37
37
 
38
- private_constant :JWT_NOTBEFORE_OFFSET_SEC,:JWT_EXPIRY_OFFSET_SEC,:TOKEN_CACHE_EXPIRY_SEC,:PERSIST_CATEGORY_TOKEN,:TOKEN_EXPIRATION_GUARD_SEC
38
+ private_constant :JWT_NOTBEFORE_OFFSET_SEC, :JWT_EXPIRY_OFFSET_SEC, :TOKEN_CACHE_EXPIRY_SEC, :PERSIST_CATEGORY_TOKEN, :TOKEN_EXPIRATION_GUARD_SEC
39
39
 
40
40
  # persistency manager
41
41
  @persist = nil
@@ -48,7 +48,7 @@ module Aspera
48
48
  def persist_mgr=(manager)
49
49
  @persist = manager
50
50
  # cleanup expired tokens
51
- @persist.garbage_collect(PERSIST_CATEGORY_TOKEN,TOKEN_CACHE_EXPIRY_SEC)
51
+ @persist.garbage_collect(PERSIST_CATEGORY_TOKEN, TOKEN_CACHE_EXPIRY_SEC)
52
52
  end
53
53
 
54
54
  def persist_mgr
@@ -56,7 +56,7 @@ module Aspera
56
56
  Log.log.debug('Not using persistency') # (use Aspera::Oauth.persist_mgr=Aspera::PersistencyFolder.new)
57
57
  # create NULL persistency class
58
58
  @persist = Class.new do
59
- def get(_x);nil;end;def delete(_x);nil;end;def put(_x,_y);nil;end;def garbage_collect(_x,_y);nil;end # rubocop:disable Layout/EmptyLineBetweenDefs
59
+ def get(_x); nil; end; def delete(_x); nil; end; def put(_x, _y); nil; end; def garbage_collect(_x, _y); nil; end # rubocop:disable Layout/EmptyLineBetweenDefs, Style/Semicolon, Layout/LineLength
60
60
  end.new
61
61
  end
62
62
  return @persist
@@ -64,7 +64,7 @@ module Aspera
64
64
 
65
65
  # delete all existing tokens
66
66
  def flush_tokens
67
- persist_mgr.garbage_collect(PERSIST_CATEGORY_TOKEN,nil)
67
+ persist_mgr.garbage_collect(PERSIST_CATEGORY_TOKEN, nil)
68
68
  end
69
69
 
70
70
  # register a bearer token decoder, mainly to inspect expiry date
@@ -94,24 +94,24 @@ module Aspera
94
94
 
95
95
  # @return one of the registered creators for the given create type
96
96
  def token_creator(id)
97
- raise "token creator type unknown: #{id}/#{id.class}" unless @create_handlers.has_key?(id)
97
+ raise "token creator type unknown: #{id}/#{id.class}" unless @create_handlers.key?(id)
98
98
  @create_handlers[id]
99
99
  end
100
100
 
101
101
  # list of identifiers foundn in creation parameters that can be used to uniquely identify the token
102
102
  def id_creator(id)
103
- raise "id creator type unknown: #{id}/#{id.class}" unless @id_handlers.has_key?(id)
103
+ raise "id creator type unknown: #{id}/#{id.class}" unless @id_handlers.key?(id)
104
104
  @id_handlers[id]
105
105
  end
106
106
  end # self
107
107
 
108
108
  # JSON Web Signature (JWS) compact serialization: https://datatracker.ietf.org/doc/html/rfc7515
109
- register_decoder lambda { |token| parts = token.split('.'); raise 'not aoc token' unless parts.length.eql?(3); JSON.parse(Base64.decode64(parts[1]))}
109
+ register_decoder lambda { |token| parts = token.split('.'); raise 'not aoc token' unless parts.length.eql?(3); JSON.parse(Base64.decode64(parts[1]))} # rubocop:disable Style/Semicolon, Layout/LineLength
110
110
 
111
111
  # generic token creation, parameters are provided in :generic
112
- register_token_creator :generic,lambda { |oauth|
112
+ register_token_creator :generic, lambda { |oauth|
113
113
  return oauth.create_token(oauth.sparams)
114
- },lambda { |oauth|
114
+ }, lambda { |oauth|
115
115
  return [
116
116
  oauth.sparams[:grant_type]&.split(':')&.last,
117
117
  oauth.sparams[:apikey],
@@ -120,12 +120,13 @@ module Aspera
120
120
  }
121
121
 
122
122
  # Authentication using Web browser
123
- register_token_creator :web,lambda { |oauth|
123
+ register_token_creator :web, lambda { |oauth|
124
124
  random_state = SecureRandom.uuid # used to check later
125
- login_page_url = Rest.build_uri("#{oauth.api[:base_url]}/#{oauth.sparams[:path_authorize]}",
125
+ login_page_url = Rest.build_uri(
126
+ "#{oauth.api.params[:base_url]}/#{oauth.sparams[:path_authorize]}",
126
127
  oauth.optional_scope_client_id.merge(response_type: 'code', redirect_uri: oauth.sparams[:redirect_uri], state: random_state))
127
128
  # here, we need a human to authorize on a web page
128
- Log.log.info("login_page_url=#{login_page_url}".bg_red.gray)
129
+ Log.log.info{"login_page_url=#{login_page_url}".bg_red.gray}
129
130
  # start a web server to receive request code
130
131
  webserver = WebAuth.new(oauth.sparams[:redirect_uri])
131
132
  # start browser on login page
@@ -138,17 +139,17 @@ module Aspera
138
139
  grant_type: 'authorization_code',
139
140
  code: received_params['code'],
140
141
  redirect_uri: oauth.sparams[:redirect_uri]))
141
- },lambda { |_oauth|
142
+ }, lambda { |_oauth|
142
143
  return []
143
144
  }
144
145
 
145
146
  # Authentication using private key
146
- register_token_creator :jwt,lambda { |oauth|
147
+ register_token_creator :jwt, lambda { |oauth|
147
148
  # https://tools.ietf.org/html/rfc7523
148
149
  # https://tools.ietf.org/html/rfc7519
149
150
  require 'jwt'
150
151
  seconds_since_epoch = Time.new.to_i
151
- Log.log.info("seconds=#{seconds_since_epoch}")
152
+ Log.log.info{"seconds=#{seconds_since_epoch}"}
152
153
  raise 'missing JWT payload' unless oauth.sparams[:payload].is_a?(Hash)
153
154
  jwt_payload = {
154
155
  exp: seconds_since_epoch + JWT_EXPIRY_OFFSET_SEC, # expiration time
@@ -156,14 +157,14 @@ module Aspera
156
157
  iat: seconds_since_epoch, # issued at
157
158
  jti: SecureRandom.uuid # JWT id
158
159
  }.merge(oauth.sparams[:payload])
159
- Log.log.debug("JWT jwt_payload=[#{jwt_payload}]")
160
+ Log.log.debug{"JWT jwt_payload=[#{jwt_payload}]"}
160
161
  rsa_private = oauth.sparams[:private_key_obj] # type: OpenSSL::PKey::RSA
161
- Log.log.debug("private=[#{rsa_private}]")
162
+ Log.log.debug{"private=[#{rsa_private}]"}
162
163
  assertion = JWT.encode(jwt_payload, rsa_private, 'RS256', oauth.sparams[:headers] || {})
163
- Log.log.debug("assertion=[#{assertion}]")
164
+ Log.log.debug{"assertion=[#{assertion}]"}
164
165
  return oauth.create_token(oauth.optional_scope_client_id.merge(grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer', assertion: assertion))
165
- },lambda { |oauth|
166
- return [oauth.sparams.dig(:payload,:sub)]
166
+ }, lambda { |oauth|
167
+ return [oauth.sparams.dig(:payload, :sub)]
167
168
  }
168
169
 
169
170
  attr_reader :gparams, :sparams, :api
@@ -186,14 +187,14 @@ module Aspera
186
187
  # :web:path_authorize [D] for type :web
187
188
  # :generic [M] for type :generic
188
189
  def initialize(a_params)
189
- Log.log.debug("auth=#{a_params}")
190
+ Log.log.debug{"auth=#{a_params}"}
190
191
  # replace default values
191
192
  @gparams = DEFAULT_CREATE_PARAMS.deep_merge(a_params)
192
193
  # check that type is known
193
194
  self.class.token_creator(@gparams[:crtype])
194
195
  # specific parameters for the creation type
195
- @sparams=@gparams[@gparams[:crtype]]
196
- if @gparams[:crtype].eql?(:web) && @sparams.has_key?(:redirect_uri)
196
+ @sparams = @gparams[@gparams[:crtype]]
197
+ if @gparams[:crtype].eql?(:web) && @sparams.key?(:redirect_uri)
197
198
  uri = URI.parse(@sparams[:redirect_uri])
198
199
  raise 'redirect_uri scheme must be http or https' unless %w[http https].include?(uri.scheme)
199
200
  raise 'redirect_uri must have a port' if uri.port.nil?
@@ -203,14 +204,14 @@ module Aspera
203
204
  base_url: @gparams[:base_url],
204
205
  redirect_max: 2
205
206
  }
206
- rest_params[:auth] = a_params[:auth] if a_params.has_key?(:auth)
207
+ rest_params[:auth] = a_params[:auth] if a_params.key?(:auth)
207
208
  @api = Rest.new(rest_params)
208
209
  # if needed use from api
209
210
  @gparams.delete(:base_url)
210
211
  @gparams.delete(:auth)
211
212
  @gparams.delete(@gparams[:crtype])
212
- Log.dump(:gparams,@gparams)
213
- Log.dump(:sparams,@sparams)
213
+ Log.dump(:gparams, @gparams)
214
+ Log.dump(:sparams, @sparams)
214
215
  end
215
216
 
216
217
  public
@@ -226,7 +227,7 @@ module Aspera
226
227
 
227
228
  # @return Hash with optional general parameters
228
229
  def optional_scope_client_id(add_secret: false)
229
- call_params={}
230
+ call_params = {}
230
231
  call_params[:scope] = @gparams[:scope] unless @gparams[:scope].nil?
231
232
  call_params[:client_id] = @gparams[:client_id] unless @gparams[:client_id].nil?
232
233
  call_params[:client_secret] = @gparams[:client_secret] if add_secret && !@gparams[:client_id].nil?
@@ -254,7 +255,7 @@ module Aspera
254
255
  # `direct` agent is equipped with refresh code
255
256
  if !use_refresh_token && !token_data.nil?
256
257
  decoded_token = self.class.decode_token(token_data[@gparams[:token_field]])
257
- Log.dump('decoded_token',decoded_token) unless decoded_token.nil?
258
+ Log.dump('decoded_token', decoded_token) unless decoded_token.nil?
258
259
  if decoded_token.is_a?(Hash)
259
260
  expires_at_sec =
260
261
  if decoded_token['expires_at'].is_a?(String) then DateTime.parse(decoded_token['expires_at']).to_time
@@ -262,13 +263,13 @@ module Aspera
262
263
  end
263
264
  # force refresh if we see a token too close from expiration
264
265
  use_refresh_token = true if expires_at_sec.is_a?(Time) && (expires_at_sec - Time.now) < TOKEN_EXPIRATION_GUARD_SEC
265
- Log.log.debug("Expiration: #{expires_at_sec} / #{use_refresh_token}")
266
+ Log.log.debug{"Expiration: #{expires_at_sec} / #{use_refresh_token}"}
266
267
  end
267
268
  end
268
269
 
269
270
  # an API was already called, but failed, we need to regenerate or refresh
270
271
  if use_refresh_token
271
- if token_data.is_a?(Hash) && token_data.has_key?('refresh_token')
272
+ if token_data.is_a?(Hash) && token_data.key?('refresh_token')
272
273
  # save possible refresh token, before deleting the cache
273
274
  refresh_token = token_data['refresh_token']
274
275
  end
@@ -277,17 +278,17 @@ module Aspera
277
278
  token_data = nil
278
279
  # lets try the existing refresh token
279
280
  if !refresh_token.nil?
280
- Log.log.info("refresh=[#{refresh_token}]".bg_green)
281
+ Log.log.info{"refresh=[#{refresh_token}]".bg_green}
281
282
  # try to refresh
282
283
  # note: AoC admin token has no refresh, and lives by default 1800secs
283
- resp = create_token(optional_scope_client_id.merge(grant_type: 'refresh_token',refresh_token: refresh_token))
284
+ resp = create_token(optional_scope_client_id.merge(grant_type: 'refresh_token', refresh_token: refresh_token))
284
285
  if resp[:http].code.start_with?('2')
285
286
  # save only if success
286
287
  json_data = resp[:http].body
287
288
  token_data = JSON.parse(json_data)
288
- self.class.persist_mgr.put(token_id,json_data)
289
+ self.class.persist_mgr.put(token_id, json_data)
289
290
  else
290
- Log.log.debug("refresh failed: #{resp[:http].body}".bg_red)
291
+ Log.log.debug{"refresh failed: #{resp[:http].body}".bg_red}
291
292
  end
292
293
  end
293
294
  end
@@ -297,9 +298,9 @@ module Aspera
297
298
  resp = self.class.token_creator(@gparams[:crtype]).call(self)
298
299
  json_data = resp[:http].body
299
300
  token_data = JSON.parse(json_data)
300
- self.class.persist_mgr.put(token_id,json_data)
301
+ self.class.persist_mgr.put(token_id, json_data)
301
302
  end # if ! in_cache
302
- raise "API error: No such field in answer: #{@gparams[:token_field]}" unless token_data.has_key?(@gparams[:token_field])
303
+ raise "API error: No such field in answer: #{@gparams[:token_field]}" unless token_data.key?(@gparams[:token_field])
303
304
  # ok we shall have a token here
304
305
  return 'Bearer ' + token_data[@gparams[:token_field]]
305
306
  end
@@ -12,30 +12,38 @@ module Aspera
12
12
  class OpenApplication
13
13
  include Singleton
14
14
  class << self
15
+ USER_INTERFACES = %i[text graphical].freeze
15
16
  # User Interfaces
16
- def user_interfaces; %i[text graphical]; end
17
+ def user_interfaces; USER_INTERFACES; end
17
18
 
18
19
  def default_gui_mode
19
- return :graphical if [Aspera::Environment::OS_WINDOWS,Aspera::Environment::OS_X].include?(Aspera::Environment.os)
20
+ return :graphical if [Aspera::Environment::OS_WINDOWS, Aspera::Environment::OS_X].include?(Aspera::Environment.os)
20
21
  # unix family
21
- return :graphical if ENV.has_key?('DISPLAY') && !ENV['DISPLAY'].empty?
22
+ return :graphical if ENV.key?('DISPLAY') && !ENV['DISPLAY'].empty?
22
23
  return :text
23
24
  end
24
25
 
25
26
  # command must be non blocking
26
27
  def uri_graphical(uri)
27
28
  case Aspera::Environment.os
28
- when Aspera::Environment::OS_X
29
- return system('open',uri.to_s)
30
- when Aspera::Environment::OS_WINDOWS
31
- return system('start explorer "' + uri.to_s + '"')
32
- when Aspera::Environment::OS_LINUX
33
- return system("xdg-open '#{uri}'")
29
+ when Aspera::Environment::OS_X then return system('open', uri.to_s)
30
+ when Aspera::Environment::OS_WINDOWS then return system('start', 'explorer', '"' + uri.to_s + '"')
31
+ when Aspera::Environment::OS_LINUX then return system('xdg-open', uri.to_s)
34
32
  else
35
33
  raise "no graphical open method for #{Aspera::Environment.os}"
36
34
  end
37
35
  end
38
- end
36
+
37
+ def editor(file_path)
38
+ if ENV.key?('EDITOR')
39
+ system(ENV['EDITOR'], file_path.to_s)
40
+ elsif Aspera::Environment.os.eql?(Aspera::Environment::OS_WINDOWS)
41
+ system('notepad.exe', '"' + file_path.to_s + '"')
42
+ else
43
+ uri_graphical(file_path.to_s)
44
+ end
45
+ end
46
+ end # self
39
47
 
40
48
  attr_accessor :url_method
41
49
 
@@ -56,7 +64,7 @@ module Aspera
56
64
  puts "USER ACTION: open this:\n" + the_url.to_s.red + "\n"
57
65
  end
58
66
  else
59
- raise StandardError,"unsupported url open method: #{@url_method}"
67
+ raise StandardError, "unsupported url open method: #{@url_method}"
60
68
  end
61
69
  end
62
70
  end # OpenApplication