aspera-cli 4.17.0 → 4.18.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (81) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +2 -4
  3. data/CHANGELOG.md +23 -0
  4. data/CONTRIBUTING.md +15 -1
  5. data/README.md +620 -378
  6. data/bin/ascli +5 -0
  7. data/bin/asession +2 -2
  8. data/lib/aspera/agent/alpha.rb +6 -4
  9. data/lib/aspera/agent/base.rb +9 -6
  10. data/lib/aspera/agent/connect.rb +4 -4
  11. data/lib/aspera/agent/direct.rb +56 -37
  12. data/lib/aspera/agent/httpgw.rb +23 -324
  13. data/lib/aspera/agent/node.rb +19 -20
  14. data/lib/aspera/agent/trsdk.rb +19 -20
  15. data/lib/aspera/api/aoc.rb +17 -14
  16. data/lib/aspera/api/cos_node.rb +4 -4
  17. data/lib/aspera/api/httpgw.rb +339 -0
  18. data/lib/aspera/api/node.rb +34 -21
  19. data/lib/aspera/ascmd.rb +4 -3
  20. data/lib/aspera/ascp/installation.rb +15 -7
  21. data/lib/aspera/ascp/management.rb +2 -2
  22. data/lib/aspera/cli/basic_auth_plugin.rb +5 -9
  23. data/lib/aspera/cli/extended_value.rb +12 -6
  24. data/lib/aspera/cli/formatter.rb +155 -65
  25. data/lib/aspera/cli/hints.rb +18 -0
  26. data/lib/aspera/cli/main.rb +22 -29
  27. data/lib/aspera/cli/manager.rb +53 -36
  28. data/lib/aspera/cli/plugin.rb +26 -17
  29. data/lib/aspera/cli/plugin_factory.rb +31 -20
  30. data/lib/aspera/cli/plugins/alee.rb +14 -2
  31. data/lib/aspera/cli/plugins/aoc.rb +141 -131
  32. data/lib/aspera/cli/plugins/ats.rb +1 -1
  33. data/lib/aspera/cli/plugins/config.rb +52 -46
  34. data/lib/aspera/cli/plugins/console.rb +8 -5
  35. data/lib/aspera/cli/plugins/faspex.rb +27 -19
  36. data/lib/aspera/cli/plugins/faspex5.rb +222 -149
  37. data/lib/aspera/cli/plugins/faspio.rb +85 -0
  38. data/lib/aspera/cli/plugins/httpgw.rb +55 -0
  39. data/lib/aspera/cli/plugins/node.rb +86 -29
  40. data/lib/aspera/cli/plugins/orchestrator.rb +31 -29
  41. data/lib/aspera/cli/plugins/preview.rb +6 -2
  42. data/lib/aspera/cli/plugins/server.rb +5 -5
  43. data/lib/aspera/cli/plugins/shares.rb +16 -14
  44. data/lib/aspera/cli/sync_actions.rb +6 -6
  45. data/lib/aspera/cli/transfer_agent.rb +5 -4
  46. data/lib/aspera/cli/version.rb +1 -1
  47. data/lib/aspera/environment.rb +7 -6
  48. data/lib/aspera/faspex_gw.rb +5 -4
  49. data/lib/aspera/faspex_postproc.rb +2 -2
  50. data/lib/aspera/log.rb +6 -3
  51. data/lib/aspera/node_simulator.rb +2 -2
  52. data/lib/aspera/oauth/base.rb +31 -19
  53. data/lib/aspera/oauth/factory.rb +12 -13
  54. data/lib/aspera/oauth/generic.rb +1 -0
  55. data/lib/aspera/oauth/jwt.rb +18 -15
  56. data/lib/aspera/oauth/url_json.rb +8 -6
  57. data/lib/aspera/open_application.rb +5 -7
  58. data/lib/aspera/persistency_folder.rb +2 -2
  59. data/lib/aspera/preview/generator.rb +3 -3
  60. data/lib/aspera/preview/options.rb +3 -3
  61. data/lib/aspera/preview/terminal.rb +4 -4
  62. data/lib/aspera/preview/utils.rb +3 -3
  63. data/lib/aspera/proxy_auto_config.rb +5 -1
  64. data/lib/aspera/rest.rb +60 -74
  65. data/lib/aspera/rest_call_error.rb +1 -1
  66. data/lib/aspera/rest_error_analyzer.rb +2 -2
  67. data/lib/aspera/rest_errors_aspera.rb +1 -1
  68. data/lib/aspera/resumer.rb +1 -1
  69. data/lib/aspera/secret_hider.rb +2 -4
  70. data/lib/aspera/ssh.rb +1 -1
  71. data/lib/aspera/transfer/parameters.rb +39 -36
  72. data/lib/aspera/transfer/spec.rb +2 -0
  73. data/lib/aspera/transfer/sync.rb +2 -1
  74. data/lib/aspera/transfer/uri.rb +1 -1
  75. data/lib/aspera/uri_reader.rb +5 -4
  76. data/lib/aspera/web_auth.rb +1 -1
  77. data/lib/aspera/web_server_simple.rb +4 -3
  78. data.tar.gz.sig +0 -0
  79. metadata +5 -3
  80. metadata.gz.sig +0 -0
  81. data/lib/aspera/cli/plugins/bss.rb +0 -71
@@ -0,0 +1,339 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'aspera/log'
4
+ require 'aspera/rest'
5
+ require 'aspera/transfer/faux_file'
6
+ require 'aspera/assert'
7
+ require 'securerandom'
8
+ require 'websocket'
9
+ require 'base64'
10
+ require 'json'
11
+
12
+ module Aspera
13
+ module Api
14
+ # Start a transfer using Aspera HTTP Gateway, using web socket secure for uploads
15
+ # ref: https://api.ibm.com/explorer/catalog/aspera/product/ibm-aspera/api/http-gateway-api/doc/guides-toc
16
+ # https://developer.ibm.com/apis/catalog?search=%22aspera%20http%22
17
+ # HTTP GW Upload protocol:
18
+ # # type Contents Ack Counter
19
+ # v1
20
+ # 0 JSON.transfer_spec Transfer Spec "end upload" sent_general
21
+ # 1.. JSON.slice_upload File base64 chunks "end upload" sent_general
22
+ # v2
23
+ # 0 JSON.transfer_spec Transfer Spec "end upload" sent_general
24
+ # 1 JSON.slice_upload File start "end_slice_upload" sent_v2_delimiter
25
+ # 2.. Binary File binary chunks "end upload" sent_general
26
+ # last JSON.slice_upload File end "end_slice_upload" sent_v2_delimiter
27
+ class Httpgw < Aspera::Rest
28
+ DEFAULT_BASE_PATH = '/aspera/http-gwy'
29
+ INFO_ENDPOINT = 'info'
30
+ MSG_SEND_TRANSFER_SPEC = 'transfer_spec'
31
+ MSG_SEND_SLICE_UPLOAD = 'slice_upload'
32
+ MSG_RECV_DATA_RECEIVED_SIGNAL = 'end upload'
33
+ MSG_RECV_SLICE_UPLOAD_SIGNAL = 'end_slice_upload'
34
+ # upload API versions
35
+ API_V1 = 'v1'
36
+ API_V2 = 'v2'
37
+ THR_RECV = 'recv'
38
+ LOG_WS_SEND = 'ws: send: '.red
39
+ LOG_WS_RECV = "ws: #{THR_RECV}: ".green
40
+ private_constant :MSG_RECV_DATA_RECEIVED_SIGNAL, :MSG_RECV_SLICE_UPLOAD_SIGNAL
41
+ # send message on http gw web socket
42
+ def ws_snd_json(msg_type, payload)
43
+ if msg_type.eql?(MSG_SEND_SLICE_UPLOAD) && @upload_version.eql?(API_V2)
44
+ @shared_info[:count][:sent_v2_delimiter] += 1
45
+ else
46
+ @shared_info[:count][:sent_general] += 1
47
+ end
48
+ Log.log.debug do
49
+ log_data = payload.dup
50
+ log_data[:data] = "[data #{log_data[:data].length} bytes]" if log_data.key?(:data)
51
+ "#{LOG_WS_SEND}json: #{msg_type}: #{JSON.generate(log_data)}"
52
+ end
53
+ ws_send(ws_type: :text, data: JSON.generate({msg_type => payload}))
54
+ end
55
+
56
+ # send data on http gw web socket
57
+ def ws_send(ws_type:, data:)
58
+ Log.log.debug{"#{LOG_WS_SEND}sending: #{ws_type} (#{data&.length || 0} bytes)"}
59
+ @shared_info[:count][:sent_general] += 1 if ws_type.eql?(:binary)
60
+ frame_generator = ::WebSocket::Frame::Outgoing::Client.new(data: data, type: ws_type, version: @ws_handshake.version)
61
+ @ws_io.write(frame_generator.to_s)
62
+ if @synchronous
63
+ @shared_info[:mutex].synchronize do
64
+ # if read thread exited, there will be no more updates
65
+ # we allow for 1 of difference else it stays blocked
66
+ while @ws_read_thread.alive? &&
67
+ @shared_info[:read_exception].nil? &&
68
+ (((@shared_info[:count][:sent_general] - @shared_info[:count][:received_general]) > 1) ||
69
+ ((@shared_info[:count][:received_v2_delimiter] - @shared_info[:count][:sent_v2_delimiter]) > 1))
70
+ if !@shared_info[:cond_var].wait(@shared_info[:mutex], 2.0)
71
+ Log.log.debug{"#{LOG_WS_SEND}#{'timeout'.blue}: #{@shared_info[:count]}"}
72
+ end
73
+ end
74
+ end
75
+ end
76
+ raise @shared_info[:read_exception] unless @shared_info[:read_exception].nil?
77
+ Log.log.debug{"#{LOG_WS_SEND}counts: #{@shared_info[:count]}"}
78
+ end
79
+
80
+ # message processing for read thread
81
+ def process_received_message(message)
82
+ Log.log.debug{"#{LOG_WS_RECV}message: [#{message}] (#{message.class})"}
83
+ if message.eql?(MSG_RECV_DATA_RECEIVED_SIGNAL)
84
+ @shared_info[:mutex].synchronize do
85
+ @shared_info[:count][:received_general] += 1
86
+ @shared_info[:cond_var].signal
87
+ end
88
+ elsif message.eql?(MSG_RECV_SLICE_UPLOAD_SIGNAL)
89
+ @shared_info[:mutex].synchronize do
90
+ @shared_info[:count][:received_v2_delimiter] += 1
91
+ @shared_info[:cond_var].signal
92
+ end
93
+ else
94
+ message.chomp!
95
+ error_message =
96
+ if message.start_with?('"') && message.end_with?('"')
97
+ # remove double quotes : 1..-2
98
+ JSON.parse(Base64.strict_decode64(message.chomp[1..-2]))['message']
99
+ elsif message.start_with?('{') && message.end_with?('}')
100
+ JSON.parse(message)['message']
101
+ else
102
+ "unknown message from gateway: [#{message}]"
103
+ end
104
+ raise error_message
105
+ end
106
+ end
107
+
108
+ # main function of read thread
109
+ def process_read_thread
110
+ Log.log.debug{"#{LOG_WS_RECV}read thread started"}
111
+ frame_parser = ::WebSocket::Frame::Incoming::Client.new(version: @ws_handshake.version)
112
+ until @ws_io.eof?
113
+ begin # rubocop:disable Style/RedundantBegin
114
+ # ready byte by byte until frame is ready
115
+ # blocking read
116
+ byte = @ws_io.read(1)
117
+ Log.log.trace1{"#{LOG_WS_RECV}read: #{byte} (#{byte.class}) eof=#{@ws_io.eof?}"}
118
+ frame_parser << byte
119
+ frame_ok = frame_parser.next
120
+ next if frame_ok.nil?
121
+ process_received_message(frame_ok.data.to_s)
122
+ Log.log.debug{"#{LOG_WS_RECV}counts: #{@shared_info[:count]}"}
123
+ rescue => e
124
+ Log.log.debug{"#{LOG_WS_RECV}Exception: #{e}"}
125
+ @shared_info[:mutex].synchronize do
126
+ @shared_info[:read_exception] = e
127
+ @shared_info[:cond_var].signal
128
+ end
129
+ break
130
+ end
131
+ end
132
+ Log.log.debug do
133
+ "#{LOG_WS_RECV}exception: #{@shared_info[:read_exception]},cls=#{@shared_info[:read_exception].class})"
134
+ end unless @shared_info[:read_exception].nil?
135
+ Log.log.debug{"#{LOG_WS_RECV}read thread stopped (ws eof=#{@ws_io.eof?})"}
136
+ end
137
+
138
+ def upload(transfer_spec)
139
+ # identify this session uniquely
140
+ session_id = SecureRandom.uuid
141
+ @notify_cb&.call(session_id: nil, type: :pre_start, info: 'starting')
142
+ # total size of all files
143
+ total_bytes_to_transfer = 0
144
+ # we need to keep track of actual file path because transfer spec is modified to be sent in web socket
145
+ files_to_read = []
146
+ # get source root or nil
147
+ source_root = transfer_spec.key?('source_root') && !transfer_spec['source_root'].empty? ? transfer_spec['source_root'] : nil
148
+ # source root is ignored by GW, used only here
149
+ transfer_spec.delete('source_root')
150
+ # compute total size of files to upload (for progress)
151
+ # modify transfer spec to be suitable for GW
152
+ transfer_spec['paths'].each do |item|
153
+ # save actual file location to be able read contents later
154
+ file_to_add = Transfer::FauxFile.open(item['source'])
155
+ if file_to_add
156
+ item['source'] = file_to_add.path
157
+ item['file_size'] = file_to_add.size
158
+ else
159
+ file_to_add = item['source']
160
+ # add source root if needed
161
+ file_to_add = File.join(source_root, file_to_add) unless source_root.nil?
162
+ # GW expects a simple file name in 'source' but if user wants to change the name, we take it
163
+ item['source'] = File.basename(item['destination'].nil? ? item['source'] : item['destination'])
164
+ item['file_size'] = File.size(file_to_add)
165
+ end
166
+ # save so that we can actually read the file later
167
+ files_to_read.push(file_to_add)
168
+ total_bytes_to_transfer += item['file_size']
169
+ end
170
+ # TODO: check that this is available in endpoints: @api_info['endpoints']
171
+ upload_url = File.join(@gw_root_url, @upload_version, 'upload')
172
+ @notify_cb&.call(session_id: nil, type: :pre_start, info: 'connecting wss')
173
+ # open web socket to end point (equivalent to Net::HTTP.start)
174
+ http_session = Rest.start_http_session(upload_url)
175
+ # get the underlying socket i/o
176
+ @ws_io = Rest.io_http_session(http_session)
177
+ @ws_handshake = ::WebSocket::Handshake::Client.new(url: upload_url, headers: {})
178
+ @ws_io.write(@ws_handshake.to_s)
179
+ sleep(0.1)
180
+ @ws_handshake << @ws_io.readuntil("\r\n\r\n")
181
+ Aspera.assert(@ws_handshake.finished?){'Error in websocket handshake'}
182
+ Log.log.debug{"#{LOG_WS_SEND}handshake success"}
183
+ # start read thread after handshake
184
+ @ws_read_thread = Thread.new {process_read_thread}
185
+ @notify_cb&.call(session_id: session_id, type: :session_start)
186
+ @notify_cb&.call(session_id: session_id, type: :session_size, info: total_bytes_to_transfer)
187
+ sleep(1)
188
+ # data shared between main thread and read thread
189
+ @shared_info = {
190
+ read_exception: nil, # error message if any in callback
191
+ count: {
192
+ sent_general: 0,
193
+ received_general: 0,
194
+ sent_v2_delimiter: 0,
195
+ received_v2_delimiter: 0
196
+ },
197
+ mutex: Mutex.new,
198
+ cond_var: ConditionVariable.new
199
+ }
200
+ # notify progress bar
201
+ @notify_cb&.call(type: :session_size, session_id: session_id, info: total_bytes_to_transfer)
202
+ # first step send transfer spec
203
+ Log.log.debug{Log.dump(:ws_spec, transfer_spec)}
204
+ ws_snd_json(MSG_SEND_TRANSFER_SPEC, transfer_spec)
205
+ # current file index
206
+ file_index = 0
207
+ # aggregate size sent
208
+ session_sent_bytes = 0
209
+ # process each file
210
+ transfer_spec['paths'].each do |item|
211
+ slice_info = {
212
+ name: nil,
213
+ # TODO: get mime type?
214
+ type: 'application/octet-stream',
215
+ size: item['file_size'],
216
+ slice: 0, # current slice index
217
+ # index of last slice (i.e number of slices - 1)
218
+ last_slice: (item['file_size'] - 1) / @upload_chunk_size,
219
+ fileIndex: file_index
220
+ }
221
+ file = files_to_read[file_index]
222
+ if file.is_a?(Transfer::FauxFile)
223
+ slice_info[:name] = file.path
224
+ else
225
+ file = File.open(file)
226
+ slice_info[:name] = File.basename(item[item['destination'].nil? ? 'source' : 'destination'])
227
+ end
228
+ begin
229
+ until file.eof?
230
+ slice_bin_data = file.read(@upload_chunk_size)
231
+ # interrupt main thread if read thread failed
232
+ raise @shared_info[:read_exception] unless @shared_info[:read_exception].nil?
233
+ begin
234
+ if @upload_version.eql?(API_V1)
235
+ slice_info[:data] = Base64.strict_encode64(slice_bin_data)
236
+ ws_snd_json(MSG_SEND_SLICE_UPLOAD, slice_info)
237
+ else
238
+ # send once, before data, at beginning
239
+ ws_snd_json(MSG_SEND_SLICE_UPLOAD, slice_info) if slice_info[:slice].eql?(0)
240
+ ws_send(ws_type: :binary, data: slice_bin_data)
241
+ Log.log.debug{"#{LOG_WS_SEND}buffer: file: #{file_index}, slice: #{slice_info[:slice]}/#{slice_info[:last_slice]}"}
242
+ # send once, after data, at end
243
+ ws_snd_json(MSG_SEND_SLICE_UPLOAD, slice_info) if slice_info[:slice].eql?(slice_info[:last_slice])
244
+ end
245
+ rescue Errno::EPIPE => e
246
+ raise @shared_info[:read_exception] unless @shared_info[:read_exception].nil?
247
+ raise e
248
+ rescue Net::ReadTimeout => e
249
+ Log.log.warn{'A timeout condition using HTTPGW may signal a permission problem on destination. Check ascp logs on httpgw.'}
250
+ raise e
251
+ end
252
+ session_sent_bytes += slice_bin_data.length
253
+ @notify_cb&.call(type: :transfer, session_id: session_id, info: session_sent_bytes)
254
+ slice_info[:slice] += 1
255
+ end
256
+ ensure
257
+ file.close
258
+ end
259
+ file_index += 1
260
+ end
261
+ # throttling may have skipped last one
262
+ @notify_cb&.call(type: :transfer, session_id: session_id, info: session_sent_bytes)
263
+ @notify_cb&.call(type: :end, session_id: session_id)
264
+ ws_send(ws_type: :close, data: nil)
265
+ Log.log.debug("Finished upload, waiting for end of #{THR_RECV} thread.")
266
+ @ws_read_thread.join
267
+ Log.log.debug{'Read thread joined'}
268
+ # session no more used
269
+ @ws_io = nil
270
+ http_session&.finish
271
+ end
272
+
273
+ def download(transfer_spec)
274
+ transfer_spec['zip_required'] ||= false
275
+ transfer_spec['source_root'] ||= '/'
276
+ # is normally provided by application, like package name
277
+ if !transfer_spec.key?('download_name')
278
+ # by default it is the name of first file
279
+ download_name = File.basename(transfer_spec['paths'].first['source'], '.*')
280
+ # ands add indication of number of files if there is more than one
281
+ if transfer_spec['paths'].length > 1
282
+ download_name += " #{transfer_spec['paths'].length} Files"
283
+ end
284
+ transfer_spec['download_name'] = download_name
285
+ end
286
+ creation = create('download', {'transfer_spec' => transfer_spec})[:data]
287
+ transfer_uuid = creation['url'].split('/').last
288
+ file_name =
289
+ if transfer_spec['zip_required'] || transfer_spec['paths'].length > 1
290
+ # it is a zip file if zip is required or there is more than 1 file
291
+ transfer_spec['download_name'] + '.zip'
292
+ else
293
+ # it is a plain file if we don't require zip and there is only one file
294
+ File.basename(transfer_spec['paths'].first['source'])
295
+ end
296
+ file_path = File.join(transfer_spec['destination_root'], file_name)
297
+ call(operation: 'GET', subpath: "download/#{transfer_uuid}", save_to_file: file_path)
298
+ end
299
+
300
+ def info
301
+ return @api_info
302
+ end
303
+
304
+ # @param url [String] URL of the HTTP Gateway, without version
305
+ def initialize(
306
+ url:,
307
+ api_version: API_V2,
308
+ upload_chunk_size: 64_000,
309
+ synchronous: false,
310
+ notify_cb: nil,
311
+ **opts
312
+ )
313
+ # add scheme if missing
314
+ url = "https://#{url}" unless url.match?(%r{^[a-z]{1,6}://})
315
+ raise 'GW URL shall be with scheme https' unless url.start_with?('https://')
316
+ # remove trailing slash and version if any
317
+ url = url.gsub(%r{/+$}, '').gsub(%r{/#{API_V1}$}o, '')
318
+ url = File.join(url, DEFAULT_BASE_PATH) unless url.end_with?(DEFAULT_BASE_PATH)
319
+ @gw_root_url = url
320
+ super(base_url: "#{@gw_root_url}/#{API_V1}", **opts)
321
+ @upload_version = api_version
322
+ @upload_chunk_size = upload_chunk_size
323
+ @synchronous = synchronous
324
+ @notify_cb = notify_cb
325
+ # get API info
326
+ @api_info = read('info')[:data].freeze
327
+ Log.log.debug{Log.dump(:api_info, @api_info)}
328
+ # web socket endpoint: by default use v2 (newer gateways), without base64 encoding
329
+ # is the latest supported? else revert to old api
330
+ if !@upload_version.eql?(API_V1)
331
+ if !@api_info['endpoints'].any?{|i|i.include?(@upload_version)}
332
+ Log.log.warn{"API version #{@upload_version} not supported, reverting to #{API_V1}"}
333
+ @upload_version = API_V1
334
+ end
335
+ end
336
+ end
337
+ end
338
+ end
339
+ end
@@ -14,21 +14,23 @@ module Aspera
14
14
  module Api
15
15
  # Provides additional functions using node API with gen4 extensions (access keys)
16
16
  class Node < Aspera::Rest
17
- # permissions
17
+ # node api permissions
18
18
  ACCESS_LEVELS = %w[delete list mkdir preview read rename write].freeze
19
+ HEADER_X_ASPERA_ACCESS_KEY = 'X-Aspera-AccessKey'
20
+ SCOPE_SEPARATOR = ':'
21
+ SCOPE_USER = 'user:all'
22
+ SCOPE_ADMIN = 'admin:all'
23
+ SCOPE_NODE_PREFIX = 'node.'
19
24
  # prefix for ruby code for filter (deprecated)
20
25
  MATCH_EXEC_PREFIX = 'exec:'
21
26
  MATCH_TYPES = [String, Proc, Regexp, NilClass].freeze
22
- HEADER_X_ASPERA_ACCESS_KEY = 'X-Aspera-AccessKey'
23
27
  PATH_SEPARATOR = '/'
24
- TS_FIELDS_TO_COPY = %w[remote_host remote_user ssh_port fasp_port wss_enabled wss_port].freeze
25
- SCOPE_USER = 'user:all'
26
- SCOPE_ADMIN = 'admin:all'
27
- SCOPE_PREFIX = 'node.'
28
- SCOPE_SEPARATOR = ':'
29
28
  SIGNATURE_DELIMITER = '==SIGNATURE=='
30
29
  BEARER_TOKEN_VALIDITY_DEFAULT = 86400
31
30
  BEARER_TOKEN_SCOPE_DEFAULT = SCOPE_USER
31
+ private_constant :MATCH_EXEC_PREFIX, :MATCH_TYPES,
32
+ :SIGNATURE_DELIMITER, :BEARER_TOKEN_VALIDITY_DEFAULT, :BEARER_TOKEN_SCOPE_DEFAULT,
33
+ :SCOPE_SEPARATOR, :SCOPE_NODE_PREFIX
32
34
 
33
35
  # register node special token decoder
34
36
  OAuth::Factory.instance.register_decoder(lambda{|token|Node.decode_bearer_token(token)})
@@ -62,14 +64,14 @@ module Aspera
62
64
 
63
65
  # node API scopes
64
66
  def token_scope(access_key, scope)
65
- return [SCOPE_PREFIX, access_key, SCOPE_SEPARATOR, scope].join('')
67
+ return [SCOPE_NODE_PREFIX, access_key, SCOPE_SEPARATOR, scope].join('')
66
68
  end
67
69
 
68
70
  def decode_scope(scope)
69
71
  items = scope.split(SCOPE_SEPARATOR, 2)
70
72
  Aspera.assert(items.length.eql?(2)){"invalid scope: #{scope}"}
71
- Aspera.assert(items[0].start_with?(SCOPE_PREFIX)){"invalid scope: #{scope}"}
72
- return {access_key: items[0][SCOPE_PREFIX.length..-1], scope: items[1]}
73
+ Aspera.assert(items[0].start_with?(SCOPE_NODE_PREFIX)){"invalid scope: #{scope}"}
74
+ return {access_key: items[0][SCOPE_NODE_PREFIX.length..-1], scope: items[1]}
73
75
  end
74
76
 
75
77
  # Create an Aspera Node bearer token
@@ -134,6 +136,7 @@ module Aspera
134
136
  @app_info = app_info
135
137
  # this is added to transfer spec, for instance to add tags (COS)
136
138
  @add_tspec = add_tspec
139
+ @std_t_spec_cache = nil
137
140
  if !@app_info.nil?
138
141
  REQUIRED_APP_INFO_FIELDS.each do |field|
139
142
  Aspera.assert(@app_info.key?(field)){"app_info lacks field #{field}"}
@@ -205,7 +208,7 @@ module Aspera
205
208
  end
206
209
  end
207
210
  end
208
- end # process_folder_tree
211
+ end
209
212
 
210
213
  # Navigate the path from given file id
211
214
  # @param top_file_id [String] id initial file id
@@ -271,7 +274,23 @@ module Aspera
271
274
  end
272
275
 
273
276
  def refreshed_transfer_token
274
- return oauth_token(force_refresh: true)
277
+ return oauth.token(refresh: true)
278
+ end
279
+
280
+ # @return part of transfer spec with transport parameters only
281
+ def transport_params
282
+ if @std_t_spec_cache.nil?
283
+ # retrieve values from API (and keep a copy/cache)
284
+ full_spec = create(
285
+ 'files/download_setup',
286
+ {transfer_requests: [{transfer_request: {paths: [{source: '/'}]}}]}
287
+ )[:data]['transfer_specs'].first['transfer_spec']
288
+ # set available fields
289
+ @std_t_spec_cache = Transfer::Spec::TRANSPORT_FIELDS.each_with_object({}) do |i, h|
290
+ h[i] = full_spec[i] if full_spec.key?(i)
291
+ end
292
+ end
293
+ return @std_t_spec_cache
275
294
  end
276
295
 
277
296
  # Create transfer spec for gen4
@@ -285,9 +304,9 @@ module Aspera
285
304
  ak_token = Rest.basic_token(auth_params[:username], auth_params[:password])
286
305
  when :oauth2
287
306
  ak_name = params[:headers][HEADER_X_ASPERA_ACCESS_KEY]
288
- # TODO: token_generation_lambda = lambda{|do_refresh|oauth_token(force_refresh: do_refresh)}
307
+ # TODO: token_generation_lambda = lambda{|do_refresh|oauth.token(refresh: do_refresh)}
289
308
  # get bearer token, possibly use cache
290
- ak_token = oauth_token(force_refresh: false)
309
+ ak_token = oauth.token(refresh: false)
291
310
  else Aspera.error_unexpected_value(auth_params[:type])
292
311
  end
293
312
  transfer_spec = {
@@ -327,13 +346,7 @@ module Aspera
327
346
  transfer_spec[i] = settings[i] if settings.key?(i)
328
347
  end if settings.is_a?(Hash)
329
348
  else
330
- # retrieve values from API (and keep a copy/cache)
331
- @std_t_spec_cache ||= create(
332
- 'files/download_setup',
333
- {transfer_requests: [{ transfer_request: {paths: [{'source' => '/'}] } }] }
334
- )[:data]['transfer_specs'].first['transfer_spec']
335
- # copy some parts
336
- TS_FIELDS_TO_COPY.each {|i| transfer_spec[i] = @std_t_spec_cache[i] if @std_t_spec_cache.key?(i)}
349
+ transfer_spec.merge!(transport_params)
337
350
  end
338
351
  Log.log.warn{"Expected transfer user: #{Transfer::Spec::ACCESS_KEY_TRANSFER_USER}, but have #{transfer_spec['remote_user']}"} \
339
352
  unless transfer_spec['remote_user'].eql?(Transfer::Spec::ACCESS_KEY_TRANSFER_USER)
data/lib/aspera/ascmd.rb CHANGED
@@ -22,6 +22,7 @@ module Aspera
22
22
  mv: 2,
23
23
  rm: 1
24
24
  }.freeze
25
+ private_constant :OPS_ARGS
25
26
  # list of supported actions
26
27
  OPERATIONS = OPS_ARGS.keys.freeze
27
28
 
@@ -94,7 +95,7 @@ module Aspera
94
95
  raise Error.new(result[:errno], result[:errstr], action_sym, arguments) if
95
96
  result.is_a?(Hash) && (result.keys.sort == TYPES_DESCR[:error][:fields].map{|i|i[:name]}.sort)
96
97
  return result
97
- end # execute_single
98
+ end
98
99
 
99
100
  # This exception is raised when +ascmd+ returns an error.
100
101
  class Error < StandardError
@@ -103,7 +104,7 @@ module Aspera
103
104
 
104
105
  def message; "ascmd: #{@errstr} (#{@errno})"; end
105
106
  def extended_message; "ascmd: errno=#{@errno} errstr=\"#{@errstr}\" command=#{@command} arguments=#{@arguments&.join(',')}"; end
106
- end # Error
107
+ end
107
108
 
108
109
  # description of result structures (see ascmdtypes.h). Base types are big endian
109
110
  # key = name of type
@@ -211,7 +212,7 @@ module Aspera
211
212
  end
212
213
  end
213
214
  else Aspera.error_unexpected_value(type_descr[:decode])
214
- end # is_a
215
+ end
215
216
  return result
216
217
  end
217
218
  end
@@ -173,8 +173,7 @@ module Aspera
173
173
  return exe_version
174
174
  end
175
175
 
176
- def ascp_info
177
- data = file_paths
176
+ def ascp_add_pvcl(data)
178
177
  # read PATHs from ascp directly, and pvcl modules as well
179
178
  Open3.popen3(data['ascp'], '-DDL-') do |_stdin, _stdout, stderr, thread|
180
179
  last_line = ''
@@ -202,15 +201,24 @@ module Aspera
202
201
  raise last_line
203
202
  end
204
203
  end
205
- # ascp's openssl directory
204
+ end
205
+
206
+ # extract some stings from ascp binary
207
+ def ascp_add_openssl(data)
206
208
  ascp_file = data['ascp']
207
- File.binread(ascp_file).scan(/[\x20-\x7E]{4,}/) do |match|
208
- if (m = match.match(/OPENSSLDIR.*"(.*)"/))
209
+ File.binread(ascp_file).scan(/[\x20-\x7E]{10,}/) do |bin_string|
210
+ if (m = bin_string.match(/OPENSSLDIR.*"(.*)"/))
209
211
  data['openssldir'] = m[1]
212
+ elsif (m = bin_string.match(/OpenSSL (\d[^ -]+)/))
213
+ data['openssl_version'] = m[1]
210
214
  end
211
215
  end if File.file?(ascp_file)
212
- # log is "-" no need to display
213
- data.delete('log')
216
+ end
217
+
218
+ def ascp_info
219
+ data = file_paths
220
+ ascp_add_pvcl(data)
221
+ ascp_add_openssl(data)
214
222
  return data
215
223
  end
216
224
 
@@ -209,7 +209,7 @@ module Aspera
209
209
  h[new_name] = value
210
210
  end
211
211
  end
212
- end # class << self
212
+ end
213
213
 
214
214
  def initialize
215
215
  # current event being parsed line by line
@@ -236,7 +236,7 @@ module Aspera
236
236
  return @last_event
237
237
  else
238
238
  raise "mgt port: unexpected line: [#{line}]"
239
- end # case
239
+ end
240
240
  return nil
241
241
  end
242
242
  end
@@ -8,10 +8,7 @@ module Aspera
8
8
  # base class for applications supporting basic authentication
9
9
  class BasicAuthPlugin < Cli::Plugin
10
10
  class << self
11
- #@@basic_options_declared = false # rubocop:disable Style/ClassVars
12
- def declare_options(options) # , force: false
13
- #return if @@basic_options_declared && !force
14
- #@@basic_options_declared = true # rubocop:disable Style/ClassVars
11
+ def declare_options(options)
15
12
  options.declare(:url, 'URL of application, e.g. https://faspex.example.com/aspera/faspex')
16
13
  options.declare(:username, "User's name to log in")
17
14
  options.declare(:password, "User's password")
@@ -21,14 +18,13 @@ module Aspera
21
18
 
22
19
  def initialize(basic_options: true, **env)
23
20
  super(**env)
24
- # , force: env[:all_manuals]
25
21
  BasicAuthPlugin.declare_options(options) if basic_options
26
22
  end
27
23
 
28
24
  # returns a Rest object with basic auth
29
25
  def basic_auth_params(subpath=nil)
30
26
  api_url = options.get_option(:url, mandatory: true)
31
- api_url = api_url + '/' + subpath unless subpath.nil?
27
+ api_url = "#{api_url}/#{subpath}" unless subpath.nil?
32
28
  return {
33
29
  base_url: api_url,
34
30
  auth: {
@@ -41,6 +37,6 @@ module Aspera
41
37
  def basic_auth_api(subpath=nil)
42
38
  return Rest.new(**basic_auth_params(subpath))
43
39
  end
44
- end # BasicAuthPlugin
45
- end # Cli
46
- end # Aspera
40
+ end
41
+ end
42
+ end
@@ -22,6 +22,10 @@ module Aspera
22
22
  ALL = 'ALL'
23
23
  DEF = 'DEF'
24
24
 
25
+ MARKER_START = '@'
26
+ MARKER_END = ':'
27
+ MARKER_IN_END = '@'
28
+
25
29
  class << self
26
30
  # decode comma separated table text
27
31
  def decode_csvt(value)
@@ -61,10 +65,11 @@ module Aspera
61
65
  list: lambda{|v|v[1..-1].split(v[0])},
62
66
  none: lambda{|v|ExtendedValue.assert_no_value(v, :none); nil}, # rubocop:disable Style/Semicolon
63
67
  path: lambda{|v|File.expand_path(v)},
64
- re: lambda{|v|Regexp.new(v)},
68
+ re: lambda{|v|Regexp.new(v, Regexp::MULTILINE)},
65
69
  ruby: lambda{|v|Environment.secure_eval(v, __FILE__, __LINE__)},
66
70
  secret: lambda{|v|prompt = v.empty? ? 'secret' : v; $stdin.getpass("#{prompt}> ")}, # rubocop:disable Style/Semicolon
67
71
  stdin: lambda{|v|ExtendedValue.assert_no_value(v, :stdin); $stdin.read}, # rubocop:disable Style/Semicolon
72
+ stdbin: lambda{|v|ExtendedValue.assert_no_value(v, :stdin); $stdin.binmode.read}, # rubocop:disable Style/Semicolon
68
73
  yaml: lambda{|v|YAML.load(v)},
69
74
  zlib: lambda{|v|Zlib::Inflate.inflate(v)},
70
75
  extend: lambda{|v|ExtendedValue.instance.evaluate_all(v)}
@@ -84,14 +89,14 @@ module Aspera
84
89
 
85
90
  # Regex to match an extended value
86
91
  def ext_re
87
- "@(#{modifiers.join('|')}):"
92
+ "#{MARKER_START}(#{modifiers.join('|')})#{MARKER_END}"
88
93
  end
89
94
 
90
95
  # parse an option value if it is a String using supported extended value modifiers
91
96
  # other value types are returned as is
92
97
  def evaluate(value)
93
98
  return value unless value.is_a?(String)
94
- regex = Regexp.new("^#{ext_re}(.*)$")
99
+ regex = Regexp.new("^#{ext_re}(.*)$", Regexp::MULTILINE)
95
100
  # first determine decoders, in reversed order
96
101
  handlers_reversed = []
97
102
  while (m = value.match(regex))
@@ -101,18 +106,19 @@ module Aspera
101
106
  # stop processing if handler is extend (it will be processed later)
102
107
  break if handler.eql?(:extend)
103
108
  end
109
+ Log.log.trace1{"evaluating: #{handlers_reversed}, value: #{value}"}
104
110
  handlers_reversed.each do |handler|
105
111
  value = @handlers[handler].call(value)
106
112
  end
107
113
  return value
108
- end # evaluate
114
+ end
109
115
 
110
116
  # find inner extended values
111
117
  def evaluate_all(value)
112
- regex = Regexp.new("^(.*)#{ext_re}([^@]*)@(.*)$")
118
+ regex = Regexp.new("^(.*)#{ext_re}([^#{MARKER_IN_END}]*)#{MARKER_IN_END}(.*)$", Regexp::MULTILINE)
113
119
  while (m = value.match(regex))
114
120
  sub_value = "@#{m[2]}:#{m[3]}"
115
- Log.log.debug("evaluating #{sub_value}")
121
+ Log.log.debug{"evaluating #{sub_value}"}
116
122
  value = m[1] + evaluate(sub_value) + m[4]
117
123
  end
118
124
  return value