aspera-cli 4.20.0 → 4.21.2

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 (73) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/CHANGELOG.md +41 -3
  4. data/CONTRIBUTING.md +69 -142
  5. data/README.md +687 -461
  6. data/bin/ascli +5 -14
  7. data/bin/asession +3 -5
  8. data/examples/get_proto_file.rb +4 -3
  9. data/examples/proxy.pac +20 -20
  10. data/lib/aspera/agent/base.rb +2 -0
  11. data/lib/aspera/agent/connect.rb +20 -2
  12. data/lib/aspera/agent/{alpha.rb → desktop.rb} +12 -18
  13. data/lib/aspera/agent/direct.rb +30 -31
  14. data/lib/aspera/agent/node.rb +1 -11
  15. data/lib/aspera/agent/{trsdk.rb → transferd.rb} +37 -51
  16. data/lib/aspera/api/alee.rb +1 -1
  17. data/lib/aspera/api/aoc.rb +13 -8
  18. data/lib/aspera/api/cos_node.rb +1 -1
  19. data/lib/aspera/api/node.rb +49 -32
  20. data/lib/aspera/ascp/installation.rb +98 -77
  21. data/lib/aspera/ascp/management.rb +27 -6
  22. data/lib/aspera/cli/extended_value.rb +9 -3
  23. data/lib/aspera/cli/formatter.rb +155 -154
  24. data/lib/aspera/cli/info.rb +2 -1
  25. data/lib/aspera/cli/main.rb +12 -0
  26. data/lib/aspera/cli/manager.rb +4 -4
  27. data/lib/aspera/cli/plugin.rb +2 -2
  28. data/lib/aspera/cli/plugins/aoc.rb +134 -73
  29. data/lib/aspera/cli/plugins/config.rb +114 -83
  30. data/lib/aspera/cli/plugins/cos.rb +1 -0
  31. data/lib/aspera/cli/plugins/faspex.rb +4 -2
  32. data/lib/aspera/cli/plugins/faspex5.rb +29 -14
  33. data/lib/aspera/cli/plugins/node.rb +51 -41
  34. data/lib/aspera/cli/transfer_progress.rb +2 -0
  35. data/lib/aspera/cli/version.rb +1 -1
  36. data/lib/aspera/command_line_builder.rb +1 -1
  37. data/lib/aspera/coverage.rb +5 -3
  38. data/lib/aspera/environment.rb +59 -16
  39. data/lib/aspera/faspex_postproc.rb +3 -5
  40. data/lib/aspera/hash_ext.rb +2 -12
  41. data/lib/aspera/node_simulator.rb +230 -112
  42. data/lib/aspera/oauth/base.rb +40 -48
  43. data/lib/aspera/oauth/factory.rb +41 -2
  44. data/lib/aspera/oauth/jwt.rb +4 -1
  45. data/lib/aspera/persistency_action_once.rb +1 -1
  46. data/lib/aspera/persistency_folder.rb +20 -2
  47. data/lib/aspera/preview/generator.rb +13 -10
  48. data/lib/aspera/preview/options.rb +2 -2
  49. data/lib/aspera/preview/terminal.rb +1 -1
  50. data/lib/aspera/preview/utils.rb +11 -6
  51. data/lib/aspera/products/connect.rb +82 -0
  52. data/lib/aspera/products/desktop.rb +30 -0
  53. data/lib/aspera/products/other.rb +82 -0
  54. data/lib/aspera/products/transferd.rb +61 -0
  55. data/lib/aspera/rest.rb +22 -17
  56. data/lib/aspera/secret_hider.rb +9 -2
  57. data/lib/aspera/ssh.rb +31 -24
  58. data/lib/aspera/temp_file_manager.rb +5 -4
  59. data/lib/aspera/transfer/parameters.rb +2 -1
  60. data/lib/aspera/transfer/spec.yaml +22 -20
  61. data/lib/aspera/transfer/sync.rb +1 -5
  62. data/lib/aspera/transfer/uri.rb +2 -2
  63. data/lib/aspera/uri_reader.rb +18 -1
  64. data/lib/transferd_pb.rb +86 -0
  65. data/lib/transferd_services_pb.rb +84 -0
  66. data.tar.gz.sig +0 -0
  67. metadata +13 -166
  68. metadata.gz.sig +0 -0
  69. data/examples/build_exec +0 -74
  70. data/examples/build_exec_rubyc +0 -40
  71. data/lib/aspera/ascp/products.rb +0 -168
  72. data/lib/transfer_pb.rb +0 -84
  73. data/lib/transfer_services_pb.rb +0 -82
@@ -4,7 +4,6 @@
4
4
  # spellchecker:ignore pauseframes libx264 trunc bufsize muxer apng libmp3lame maxrate posterize movflags faststart
5
5
  # spellchecker:ignore palettegen paletteuse pointsize bordercolor repage lanczos unoconv optipng reencode conv transframes
6
6
 
7
- require 'open3'
8
7
  require 'aspera/preview/options'
9
8
  require 'aspera/preview/utils'
10
9
  require 'aspera/preview/file_types'
@@ -24,17 +23,18 @@ module Aspera
24
23
  # one of CONVERSION_TYPES
25
24
  attr_reader :conversion_type
26
25
 
27
- # @param src source file path
28
- # @param dst destination file path
29
- # @param api_mime_type optional mime type as provided by node api (or nil)
30
26
  # node API mime types are from: http://svn.apache.org/repos/asf/httpd/httpd/trunk/docs/conf/mime.types
31
- # supported preview type is one of Preview::PREVIEW_FORMATS
32
27
  # the resulting preview file type is taken from destination file extension.
33
28
  # conversion methods are provided by private methods: convert_<conversion_type>_to_<preview_format>
34
29
  # -> conversion_type is one of FileTypes::CONVERSION_TYPES
35
30
  # -> preview_format is one of Generator::PREVIEW_FORMATS
36
31
  # the conversion video->mp4 is implemented in methods: convert_video_to_mp4_using_<video_conversion>
37
32
  # -> conversion method is one of Generator::VIDEO_CONVERSION_METHODS
33
+ # @param src [String] source file path
34
+ # @param dst [String] destination file path
35
+ # @param options [Options] All conversion options
36
+ # @param main_temp_dir [String] Main temp folder, sub folder will be created for generation
37
+ # @param api_mime_type [String,nil] Optional mime type as provided by node api (or nil)
38
38
  def initialize(src, dst, options, main_temp_dir, api_mime_type)
39
39
  @source_file_path = src
40
40
  @destination_file_path = dst
@@ -54,7 +54,7 @@ module Aspera
54
54
  end
55
55
  @processing_method = @processing_method.to_sym
56
56
  Log.log.debug{"method: #{@processing_method}"}
57
- Aspera.assert(respond_to?(@processing_method, true)){"no processing know for #{conversion_type} -> #{@preview_format_sym}"}
57
+ Aspera.assert(respond_to?(@processing_method, true)){"no processing known for #{conversion_type} -> #{@preview_format_sym}"}
58
58
  end
59
59
 
60
60
  # create preview as specified in constructor
@@ -66,7 +66,7 @@ module Aspera
66
66
  result_size = File.size(@destination_file_path)
67
67
  Log.log.warn{"preview size exceeds maximum allowed #{result_size} > #{@options.max_size}"} if result_size > @options.max_size
68
68
  rescue StandardError => e
69
- Log.log.error{"Ignoring: #{e.message}"}
69
+ Log.log.error{"Ignoring: #{e.class} #{e.message}"}
70
70
  Log.log.debug(e.backtrace.join("\n").red)
71
71
  FileUtils.cp(File.expand_path(@preview_format_sym.eql?(:mp4) ? 'video_error.png' : 'image_error.png', File.dirname(__FILE__)), @destination_file_path)
72
72
  ensure
@@ -215,7 +215,8 @@ module Aspera
215
215
 
216
216
  def convert_pdf_to_png(source_file_path=nil)
217
217
  source_file_path ||= @source_file_path
218
- Utils.external_command(:convert, [
218
+ Utils.external_command(:magick, [
219
+ 'convert',
219
220
  '-size', "x#{@options.thumb_img_size}",
220
221
  '-background', 'white',
221
222
  '-flatten',
@@ -224,7 +225,8 @@ module Aspera
224
225
  end
225
226
 
226
227
  def convert_image_to_png
227
- Utils.external_command(:convert, [
228
+ Utils.external_command(:magick, [
229
+ 'convert',
228
230
  '-auto-orient',
229
231
  '-thumbnail', "#{@options.thumb_img_size}x#{@options.thumb_img_size}>",
230
232
  '-quality', 95,
@@ -239,7 +241,8 @@ module Aspera
239
241
  def convert_plaintext_to_png
240
242
  # get 100 first lines of text file
241
243
  first_lines = File.open(@source_file_path){|f|Array.new(100){f.readline rescue ''}.join}
242
- Utils.external_command(:convert, [
244
+ Utils.external_command(:magick, [
245
+ 'convert',
243
246
  '-size', "#{@options.thumb_img_size}x#{@options.thumb_img_size}",
244
247
  'xc:white', # define canvas with background color (xc, or canvas) of preceding size
245
248
  '-font', @options.thumb_text_font,
@@ -19,10 +19,10 @@ module Aspera
19
19
  { name: :thumb_vid_scale, default: "-1:'min(ih,100)'", description: 'png: video: size (ffmpeg scale argument)' },
20
20
  { name: :thumb_vid_fraction, default: 0.1, description: 'png: video: time percent position of snapshot' },
21
21
  { name: :thumb_img_size, default: 800, description: 'png: non-video: height (and width)' },
22
- { name: :thumb_text_font, default: 'Courier', description: 'png: plaintext: font to render text with imagemagick convert (identify -list font)'},
22
+ { name: :thumb_text_font, default: 'Courier', description: 'png: plaintext: font for text rendering: `magick identify -list font`'},
23
23
  { name: :video_conversion, default: :reencode, description: 'mp4: method for preview generation', values: VIDEO_CONVERSION_METHODS },
24
24
  { name: :video_png_conv, default: :fixed, description: 'mp4: method for thumbnail generation', values: VIDEO_THUMBNAIL_METHODS },
25
- { name: :video_scale, default: "'min(iw,360)':-2", description: 'mp4: all: video scale (ffmpeg)' },
25
+ { name: :video_scale, default: "'min(iw,360)':-2", description: 'mp4: all: video scale (ffmpeg scale argument)' },
26
26
  { name: :video_start_sec, default: 10, description: 'mp4: all: start offset (seconds) of video preview' },
27
27
  { name: :reencode_ffmpeg, default: {}, description: 'mp4: reencode: options to ffmpeg' },
28
28
  { name: :blend_keyframes, default: 30, description: 'mp4: blend: # key frames' },
@@ -44,7 +44,7 @@ module Aspera
44
44
  fit_term_ratio = [term_rows.to_f * font_ratio / image.rows.to_f, term_columns.to_f / image.columns.to_f].min
45
45
  height_ratio = double ? 2.0 : 1.0
46
46
  image = image.scale((image.columns * fit_term_ratio).to_i, (image.rows * fit_term_ratio * height_ratio / font_ratio).to_i)
47
- # quantum depth is 8 or 16, see: `convert xc: -format "%q" info:`
47
+ # quantum depth is 8 or 16, see: `magick xc: -format "%q" info:`
48
48
  shift_for_8_bit = Magick::MAGICKCORE_QUANTUM_DEPTH - 8
49
49
  # get all pixel colors, adjusted for Rainbow
50
50
  pixel_colors = []
@@ -14,7 +14,7 @@ module Aspera
14
14
  # from bash manual: meta-character need to be escaped
15
15
  BASH_SPECIAL_CHARACTERS = "|&;()<> \t#\n"
16
16
  # external binaries used
17
- EXTERNAL_TOOLS = %i[ffmpeg ffprobe convert composite optipng unoconv].freeze
17
+ EXTERNAL_TOOLS = %i[ffmpeg ffprobe magick optipng unoconv].freeze
18
18
  TEMP_FORMAT = 'img%04d.jpg'
19
19
  private_constant :BASH_SPECIAL_CHARACTERS, :EXTERNAL_TOOLS, :TEMP_FORMAT
20
20
 
@@ -32,7 +32,7 @@ module Aspera
32
32
  tools_to_check.delete(:unoconv) if skip_types.include?(:office)
33
33
  # Check for binaries
34
34
  tools_to_check.each do |command_sym|
35
- external_command(command_sym, ['-h'])
35
+ external_command(command_sym, ['-h'], out: File::NULL)
36
36
  rescue Errno::ENOENT => e
37
37
  raise "missing #{command_sym} binary: #{e}"
38
38
  rescue
@@ -42,10 +42,15 @@ module Aspera
42
42
 
43
43
  # execute external command
44
44
  # one could use "system", but we would need to redirect stdout/err
45
- # @return true if su
45
+ # @return nil
46
46
  def external_command(command_sym, command_args)
47
47
  Aspera.assert_values(command_sym, EXTERNAL_TOOLS){'command'}
48
- return Environment.secure_capture(command_sym.to_s, *command_args)
48
+ Environment.secure_execute(exec: command_sym.to_s, args: command_args.map(&:to_s), out: File::NULL, err: File::NULL)
49
+ end
50
+
51
+ def external_capture(command_sym, command_args)
52
+ Aspera.assert_values(command_sym, EXTERNAL_TOOLS){'command'}
53
+ return Environment.secure_capture(exec: command_sym.to_s, args: command_args.map(&:to_s))
49
54
  end
50
55
 
51
56
  def ffmpeg(a)
@@ -63,7 +68,7 @@ module Aspera
63
68
 
64
69
  # @return Float in seconds
65
70
  def video_get_duration(input_file)
66
- return external_command(:ffprobe, [
71
+ return external_capture(:ffprobe, [
67
72
  '-loglevel', 'error',
68
73
  '-show_entries', 'format=duration',
69
74
  '-print_format', 'default=noprint_wrappers=1:nokey=1', # cspell:disable-line
@@ -92,7 +97,7 @@ module Aspera
92
97
  1.upto(count) do |i|
93
98
  percent = i * 100 / (count + 1)
94
99
  filename = get_tmp_num_filepath(temp_folder, index1 + i)
95
- external_command(:composite, ['-blend', percent, img2, img1, filename])
100
+ external_command(:magick, ['composite', '-blend', percent, img2, img1, filename])
96
101
  end
97
102
  end
98
103
 
@@ -0,0 +1,82 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'aspera/environment'
4
+ require 'singleton'
5
+
6
+ module Aspera
7
+ module Products
8
+ class Connect
9
+ include Singleton
10
+ APP_NAME = 'IBM Aspera Connect'
11
+
12
+ class << self
13
+ # standard folder locations
14
+ def locations
15
+ case Aspera::Environment.os
16
+ when Aspera::Environment::OS_WINDOWS then [{
17
+ app_root: File.join(ENV.fetch('LOCALAPPDATA', nil), 'Programs', 'Aspera', 'Aspera Connect'),
18
+ log_root: File.join(ENV.fetch('LOCALAPPDATA', nil), 'Aspera', 'Aspera Connect', 'var', 'log'),
19
+ run_root: File.join(ENV.fetch('LOCALAPPDATA', nil), 'Aspera', 'Aspera Connect')
20
+ }]
21
+ when Aspera::Environment::OS_MACOS then [{
22
+ app_root: File.join(Dir.home, 'Applications', 'Aspera Connect.app'),
23
+ log_root: File.join(Dir.home, 'Library', 'Logs', 'Aspera_Connect'),
24
+ run_root: File.join(Dir.home, 'Library', 'Application Support', 'Aspera', 'Aspera Connect'),
25
+ sub_bin: File.join('Contents', 'Resources')
26
+ }, {
27
+ app_root: File.join('', 'Applications', 'Aspera Connect.app'),
28
+ log_root: File.join(Dir.home, 'Library', 'Logs', 'Aspera_Connect'),
29
+ run_root: File.join(Dir.home, 'Library', 'Application Support', 'Aspera', 'Aspera Connect'),
30
+ sub_bin: File.join('Contents', 'Resources')
31
+ }, {
32
+ app_root: File.join(Dir.home, 'Applications', 'IBM Aspera Connect.app'),
33
+ log_root: File.join(Dir.home, 'Library', 'Logs', 'Aspera_Connect'),
34
+ run_root: File.join(Dir.home, 'Library', 'Application Support', 'Aspera', 'Aspera Connect'),
35
+ sub_bin: File.join('Contents', 'Resources')
36
+ }, {
37
+ app_root: File.join('', 'Applications', 'IBM Aspera Connect.app'),
38
+ log_root: File.join(Dir.home, 'Library', 'Logs', 'Aspera_Connect'),
39
+ run_root: File.join(Dir.home, 'Library', 'Application Support', 'Aspera', 'Aspera Connect'),
40
+ sub_bin: File.join('Contents', 'Resources')
41
+ }]
42
+ else [{ # other: Linux and Unix family
43
+ app_root: File.join(Dir.home, '.aspera', 'connect'),
44
+ run_root: File.join(Dir.home, '.aspera', 'connect')
45
+ }]
46
+ end.map { |i| i.merge({ expected: APP_NAME }) }
47
+ end
48
+ end
49
+
50
+ def cdn_api
51
+ Rest.new(base_url: CDN_BASE_URL)
52
+ end
53
+
54
+ # retrieve structure from cloud (CDN) with all versions available
55
+ def versions
56
+ if @connect_versions.nil?
57
+ javascript = cdn_api.call(operation: 'GET', subpath: VERSION_INFO_FILE)
58
+ # get result on one line
59
+ connect_versions_javascript = javascript[:http].body.gsub(/\r?\n\s*/, '')
60
+ Log.log.debug{"javascript=[\n#{connect_versions_javascript}\n]"}
61
+ # get javascript object only
62
+ found = connect_versions_javascript.match(/^.*? = (.*);/)
63
+ raise Cli::Error, 'Problem when getting connect versions from internet' if found.nil?
64
+ all_data = JSON.parse(found[1])
65
+ @connect_versions = all_data['entries']
66
+ end
67
+ return @connect_versions
68
+ end
69
+
70
+ private
71
+
72
+ def initialize
73
+ @connect_versions = nil
74
+ end
75
+
76
+ VERSION_INFO_FILE = 'connectversions.js' # cspell: disable-line
77
+ CDN_BASE_URL = 'https://d3gcli72yxqn2z.cloudfront.net/connect'
78
+
79
+ private_constant :VERSION_INFO_FILE, :CDN_BASE_URL
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'aspera/environment'
4
+
5
+ module Aspera
6
+ module Products
7
+ # Client Aspera for Desktop
8
+ class Desktop
9
+ APP_NAME = 'IBM Aspera for Desktop'
10
+ APP_IDENTIFIER = 'com.ibm.software.aspera.desktop'
11
+ class << self
12
+ # standard folder locations
13
+ def locations
14
+ case Aspera::Environment.os
15
+ when Aspera::Environment::OS_MACOS then [{
16
+ app_root: File.join('', 'Applications', 'IBM Aspera.app'),
17
+ log_root: File.join(Dir.home, 'Library', 'Logs', APP_IDENTIFIER),
18
+ sub_bin: File.join('Contents', 'Resources', 'transferd', 'bin')
19
+ }]
20
+ else []
21
+ end.map { |i| i.merge({ expected: APP_NAME }) }
22
+ end
23
+
24
+ def log_file
25
+ File.join(Dir.home, 'Library', 'Logs', APP_IDENTIFIER, 'ibm-aspera-desktop.log')
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,82 @@
1
+ # frozen_string_literal: true
2
+
3
+ # cspell:ignore LOCALAPPDATA
4
+ require 'aspera/environment'
5
+
6
+ module Aspera
7
+ # Location of Aspera products, for which an Agent is not proposed
8
+ module Products
9
+ # other Aspera products with ascp
10
+ class Other
11
+ CLI_V3 = 'Aspera CLI (deprecated)'
12
+ DRIVE = 'Aspera Drive (deprecated)'
13
+ HSTS = 'IBM Aspera High-Speed Transfer Server'
14
+
15
+ private_constant :CLI_V3, :DRIVE, :HSTS
16
+ # product information manifest: XML (part of aspera product)
17
+ INFO_META_FILE = 'product-info.mf'
18
+
19
+ # :expected M app name is taken from the manifest if present, else defaults to this value
20
+ # :app_root M main folder for the application
21
+ # :log_root O location of log files (Linux uses syslog)
22
+ # :run_root O only for Connect Client, location of http port file
23
+ # :sub_bin O subfolder with executables, default : bin
24
+ LOCATION_ON_THIS_OS = case Aspera::Environment.os
25
+ when Aspera::Environment::OS_WINDOWS then [{
26
+ expected: CLI_V3,
27
+ app_root: File.join('C:', 'Program Files', 'Aspera', 'cli'),
28
+ log_root: File.join('C:', 'Program Files', 'Aspera', 'cli', 'var', 'log')
29
+ }, {
30
+ expected: HSTS,
31
+ app_root: File.join('C:', 'Program Files', 'Aspera', 'Enterprise Server'),
32
+ log_root: File.join('C:', 'Program Files', 'Aspera', 'Enterprise Server', 'var', 'log')
33
+ }]
34
+ when Aspera::Environment::OS_MACOS then [{
35
+ expected: CLI_V3,
36
+ app_root: File.join(Dir.home, 'Applications', 'Aspera CLI'),
37
+ log_root: File.join(Dir.home, 'Library', 'Logs', 'Aspera')
38
+ }, {
39
+ expected: HSTS,
40
+ app_root: File.join('', 'Library', 'Aspera'),
41
+ log_root: File.join(Dir.home, 'Library', 'Logs', 'Aspera')
42
+ }, {
43
+ expected: DRIVE,
44
+ app_root: File.join('', 'Applications', 'Aspera Drive.app'),
45
+ log_root: File.join(Dir.home, 'Library', 'Logs', 'Aspera_Drive'),
46
+ sub_bin: File.join('Contents', 'Resources')
47
+ }]
48
+ else [{ # other: Linux and Unix family
49
+ expected: CLI_V3,
50
+ app_root: File.join(Dir.home, '.aspera', 'cli')
51
+ }, {
52
+ expected: HSTS,
53
+ app_root: File.join('', 'opt', 'aspera')
54
+ }]
55
+ end
56
+ class << self
57
+ def find(scan_locations)
58
+ scan_locations.select do |item|
59
+ # skip if not main folder
60
+ Log.log.trace1{"Checking #{item[:app_root]}"}
61
+ next false unless Dir.exist?(item[:app_root])
62
+ Log.log.debug{"Found #{item[:expected]}"}
63
+ sub_bin = item[:sub_bin] || 'bin'
64
+ item[:ascp_path] = File.join(item[:app_root], sub_bin, Environment.exe_file('ascp'))
65
+ # skip if no ascp
66
+ next false unless File.exist?(item[:ascp_path])
67
+ # read info from product info file if present
68
+ product_info_file = "#{item[:app_root]}/#{INFO_META_FILE}"
69
+ if File.exist?(product_info_file)
70
+ res_s = XmlSimple.xml_in(File.read(product_info_file), {'ForceArray' => false})
71
+ item[:name] = res_s['name']
72
+ item[:version] = res_s['version']
73
+ else
74
+ item[:name] = item[:expected]
75
+ end
76
+ true # select this version
77
+ end
78
+ end
79
+ end
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aspera
4
+ module Products
5
+ class Transferd
6
+ APP_NAME = 'IBM Aspera Transfer Daemon'
7
+ V1_DAEMON_NAME = 'asperatransferd'
8
+ # from 1.1.5
9
+ V2_DAEMON_NAME = 'transferd'
10
+ # folders to extract from SDK archive
11
+ RUNTIME_FOLDERS = %w[bin lib sbin aspera].freeze
12
+ class << self
13
+ # standard folder locations
14
+ def locations
15
+ [{
16
+ app_root: sdk_directory,
17
+ sub_bin: ''
18
+ }].map { |i| i.merge({ expected: APP_NAME }) }
19
+ end
20
+
21
+ # location of SDK files
22
+ def sdk_directory=(v)
23
+ Log.log.debug{"sdk_directory=#{v}"}
24
+ @sdk_dir = v
25
+ sdk_directory
26
+ end
27
+
28
+ # @return the path to folder where SDK is installed
29
+ def sdk_directory
30
+ Aspera.assert(!@sdk_dir.nil?){'SDK path was not initialized'}
31
+ FileUtils.mkdir_p(@sdk_dir)
32
+ @sdk_dir
33
+ end
34
+
35
+ def transferd_path
36
+ v1_path = File.join(sdk_directory, Environment.exe_file(V1_DAEMON_NAME))
37
+ return v1_path if File.exist?(v1_path)
38
+ return File.join(sdk_directory, Environment.exe_file(V2_DAEMON_NAME))
39
+ end
40
+
41
+ # Well, the port number is only in log file
42
+ def daemon_port_from_log(log_file)
43
+ result = nil
44
+ # if port is zero, a dynamic port was created, get it
45
+ File.open(log_file, 'r') do |file|
46
+ file.each_line do |line|
47
+ # Well, it's tricky to depend on log
48
+ if (m = line.match(/Info: API Server: Listening on ([^:]+):(\d+) /))
49
+ result = m[2].to_i
50
+ # no "break" , need to read last matching log line
51
+ end
52
+ end
53
+ end
54
+ raise 'Port not found in daemon logs' if result.nil?
55
+ Log.log.debug{"Got port #{result} from log"}
56
+ return result
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
data/lib/aspera/rest.rb CHANGED
@@ -10,7 +10,6 @@ require 'net/http'
10
10
  require 'net/https'
11
11
  require 'json'
12
12
  require 'base64'
13
- require 'cgi'
14
13
  require 'singleton'
15
14
  require 'securerandom'
16
15
 
@@ -22,11 +21,13 @@ class Net::HTTP::Cancel < Net::HTTPRequest # rubocop:disable Style/ClassAndModul
22
21
  end
23
22
 
24
23
  module Aspera
25
- # Global settings
24
+ # Global settings for Rest object
25
+ # For example to remove certificate verification globally:
26
+ # `RestParameters.instance.session_cb = lambda{|http|http.verify_mode=OpenSSL::SSL::VERIFY_NONE}`
26
27
  # @param user_agent [String] HTTP request header: 'User-Agent'
27
28
  # @param download_partial_suffix [String] suffix for partial download
28
29
  # @param session_cb [lambda] lambda called on new HTTP session. Takes the Net::HTTP as arg. Used to change parameters on creation.
29
- # @param progress_bar [Object] progress bar object
30
+ # @param progress_bar [Object] progress bar object called for file transfer
30
31
  class RestParameters
31
32
  include Singleton
32
33
 
@@ -61,7 +62,7 @@ module Aspera
61
62
 
62
63
  class << self
63
64
  # @return [String] Basic auth token
64
- def basic_token(user, pass); return "Basic #{Base64.strict_encode64("#{user}:#{pass}")}"; end
65
+ def basic_authorization(user, pass); return "Basic #{Base64.strict_encode64("#{user}:#{pass}")}"; end
65
66
 
66
67
  # Build a parameter list prefixed with "[]"
67
68
  # @param values [Array] list of values
@@ -200,6 +201,9 @@ module Aspera
200
201
  }
201
202
  end
202
203
 
204
+ # Create a REST object for API calls
205
+ # HTTP sessions parameters can be modified using global parameters in RestParameters
206
+ # For example, TLS verification can be skipped.
203
207
  # @param base_url [String] base URL of REST API
204
208
  # @param auth [Hash] authentication parameters:
205
209
  # :type (:none, :basic, :url, :oauth2)
@@ -207,14 +211,15 @@ module Aspera
207
211
  # :password [:basic]
208
212
  # :url_query [:url] a hash
209
213
  # :* [:oauth2] see OAuth::Factory class
210
- # @param not_auth_codes [Array] codes that trigger a refresh/regeneration of bearer token
211
- # @param redirect_max [int] max redirection allowed
214
+ # @param not_auth_codes [Array] codes that trigger a refresh/regeneration of bearer token
215
+ # @param redirect_max [Integer] max redirection allowed
216
+ # @param headers [Hash] default headers to include in all calls
212
217
  def initialize(
213
218
  base_url:,
214
- auth: nil,
215
- not_auth_codes: nil,
219
+ auth: {type: :none},
220
+ not_auth_codes: ['401'],
216
221
  redirect_max: 0,
217
- headers: nil
222
+ headers: {}
218
223
  )
219
224
  Aspera.assert_type(base_url, String)
220
225
  # base url with no trailing slashes (note: string may be frozen)
@@ -224,20 +229,20 @@ module Aspera
224
229
  @base_url = @base_url.gsub(/:80$/, '') if @base_url.start_with?('http://')
225
230
  Log.log.debug{"Rest.new(#{@base_url})"}
226
231
  # default is no auth
227
- @auth_params = auth.nil? ? {type: :none} : auth
232
+ @auth_params = auth
228
233
  Aspera.assert_type(@auth_params, Hash)
229
234
  Aspera.assert(@auth_params.key?(:type)){'no auth type defined'}
230
- @not_auth_codes = not_auth_codes.nil? ? ['401'] : not_auth_codes
235
+ @not_auth_codes = not_auth_codes
231
236
  Aspera.assert_type(@not_auth_codes, Array)
232
237
  # persistent session
233
238
  @http_session = nil
234
- # OAuth object (created on demand)
235
- @oauth = nil
236
239
  @redirect_max = redirect_max
237
240
  Aspera.assert_type(@redirect_max, Integer)
238
- @headers = headers.nil? ? {} : headers
241
+ @headers = headers
239
242
  Aspera.assert_type(@headers, Hash)
240
243
  @headers['User-Agent'] ||= RestParameters.instance.user_agent
244
+ # OAuth object (created on demand)
245
+ @oauth = nil
241
246
  end
242
247
 
243
248
  # @return the OAuth object (create, or cached if already created)
@@ -290,7 +295,7 @@ module Aspera
290
295
  Log.log.debug('using Basic auth')
291
296
  # done in build_req
292
297
  when :oauth2
293
- headers['Authorization'] = oauth.token unless headers.key?('Authorization')
298
+ headers['Authorization'] = oauth.authorization unless headers.key?('Authorization')
294
299
  when :url
295
300
  query ||= {}
296
301
  @auth_params[:url_query].each do |key, value|
@@ -404,12 +409,12 @@ module Aspera
404
409
  if @not_auth_codes.include?(result[:http].code.to_s) && @auth_params[:type].eql?(:oauth2)
405
410
  begin
406
411
  # try to use refresh token
407
- req['Authorization'] = oauth.token(refresh: true)
412
+ req['Authorization'] = oauth.authorization(refresh: true)
408
413
  rescue RestCallError => e_tok
409
414
  e = e_tok
410
415
  Log.log.error('refresh failed'.bg_red)
411
416
  # regenerate a brand new token
412
- req['Authorization'] = oauth.token(refresh: true)
417
+ req['Authorization'] = oauth.authorization(cache: false)
413
418
  end
414
419
  Log.log.debug{"using new token=#{headers['Authorization']}"}
415
420
  do_retry = true if (oauth_tries -= 1).positive?
@@ -20,6 +20,8 @@ module Aspera
20
20
  KEY_FALSE_POSITIVES = [/^access_key$/, /^fallback_private_key$/].freeze
21
21
  # regex that define named captures :begin and :end
22
22
  REGEX_LOG_REPLACES = [
23
+ # private key values (place first)
24
+ /(?<begin>--+BEGIN [^-]+ KEY--+)[[:ascii:]]+?(?<end>--+?END [^-]+ KEY--+)\n*/,
23
25
  # CLI manager get/set options
24
26
  /(?<begin>[sg]et (?:#{KEY_SECRETS.join('|')})=).*(?<end>)/,
25
27
  # env var ascp exec
@@ -28,8 +30,6 @@ module Aspera
28
30
  /(?<begin>(?:(?<quote>["'])|:)[^"':=]*(?:#{ALL_SECRETS.join('|')})[^"':=]*\k<quote>?(?:=>|:) *")[^"]+(?<end>")/,
29
31
  # logged data
30
32
  /(?<begin>(?:#{ALL_SECRETS2.join('|')})[ =:]+).*(?<end>$)/,
31
- # private key values
32
- /(?<begin>--+BEGIN [^-]+ KEY--+)[[:ascii:]]+?(?<end>--+?END [^-]+ KEY--+)/,
33
33
  # cred in http dump
34
34
  /(?<begin>(?:#{HTTP_SECRETS.join('|')}): )[^\\]+(?<end>\\)/i
35
35
  ].freeze
@@ -38,6 +38,7 @@ module Aspera
38
38
  class << self
39
39
  attr_accessor :log_secrets
40
40
 
41
+ # @return new log formatter that hides secrets
41
42
  def log_formatter(original_formatter)
42
43
  original_formatter ||= Logger::Formatter.new
43
44
  # NOTE: that @log_secrets may be set AFTER this init is done, so it's done at runtime
@@ -51,6 +52,11 @@ module Aspera
51
52
  end
52
53
  end
53
54
 
55
+ def hide_secrets_in_string(value)
56
+ return value.gsub(REGEX_LOG_REPLACES.first){"#{Regexp.last_match(:begin)}#{HIDDEN_PASSWORD}#{Regexp.last_match(:end)}"}
57
+ end
58
+
59
+ # @return true if the key denotes a secret
54
60
  def secret?(keyword, value)
55
61
  keyword = keyword.to_s if keyword.is_a?(Symbol)
56
62
  # only Strings can be secrets, not booleans, or hash, arrays
@@ -62,6 +68,7 @@ module Aspera
62
68
  ALL_SECRETS.any?{|kw|keyword.include?(kw)}
63
69
  end
64
70
 
71
+ # Hides recursively secrets in Hash or Array of Hash
65
72
  def deep_remove_secret(obj)
66
73
  case obj
67
74
  when Array
data/lib/aspera/ssh.rb CHANGED
@@ -1,33 +1,41 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'net/ssh'
4
-
5
- if ENV.fetch('ASCLI_ENABLE_ED25519', 'false').eql?('false')
6
- # HACK: deactivate ed25519 and ecdsa private keys from SSH identities, as it usually causes problems
7
- old_verbose = $VERBOSE
8
- $VERBOSE = nil
9
- begin
10
- module Net; module SSH; module Authentication; class Session; private; def default_keys; %w[~/.ssh/id_dsa ~/.ssh/id_rsa ~/.ssh2/id_dsa ~/.ssh2/id_rsa]; end; end; end; end; end # rubocop:disable Layout/AccessModifierIndentation, Layout/EmptyLinesAroundAccessModifier, Layout/LineLength, Style/Semicolon
11
- rescue StandardError
12
- # ignore errors
13
- end
14
- $VERBOSE = old_verbose
15
- end
16
-
17
- if defined?(JRUBY_VERSION) && ENV.fetch('ASCLI_ENABLE_ECDSHA2', 'false').eql?('false')
18
- Net::SSH::Transport::Algorithms::ALGORITHMS.each_value { |a| a.reject! { |a| a =~ /^ecd(sa|h)-sha2/ } }
19
- Net::SSH::KnownHosts::SUPPORTED_TYPE.reject! { |t| t =~ /^ecd(sa|h)-sha2/ }
20
- end
4
+ require 'aspera/assert'
5
+ require 'aspera/log'
21
6
 
22
7
  module Aspera
23
8
  # A simple wrapper around Net::SSH
24
9
  # executes one command and get its result from stdout
25
10
  class Ssh
11
+ class << self
12
+ def disable_ed25519_keys
13
+ Log.log.debug('Disabling SSH ed25519 user keys')
14
+ old_verbose = $VERBOSE
15
+ $VERBOSE = nil
16
+ Net::SSH::Authentication::Session.class_eval do
17
+ define_method(:default_keys) do
18
+ %w[~/.ssh/id_dsa ~/.ssh/id_rsa ~/.ssh2/id_dsa ~/.ssh2/id_rsa].freeze
19
+ end
20
+ private(:default_keys)
21
+ end rescue nil
22
+ $VERBOSE = old_verbose
23
+ end
24
+
25
+ def disable_ecd_sha2_algorithms
26
+ Log.log.debug('Disabling SSH ecdsa')
27
+ Net::SSH::Transport::Algorithms::ALGORITHMS.each_value { |a| a.reject! { |a| a =~ /^ecd(sa|h)-sha2/ } }
28
+ Net::SSH::KnownHosts::SUPPORTED_TYPE.reject! { |t| t =~ /^ecd(sa|h)-sha2/ }
29
+ end
30
+ end
26
31
  # ssh_options: same as Net::SSH.start
27
32
  # see: https://net-ssh.github.io/net-ssh/classes/Net/SSH.html#method-c-start
28
33
  def initialize(host, username, ssh_options)
29
34
  Log.log.debug{"ssh:#{username}@#{host}"}
30
35
  Log.log.debug{"ssh_options:#{ssh_options}"}
36
+ Aspera.assert_type(host, String)
37
+ Aspera.assert_type(username, String)
38
+ Aspera.assert_type(ssh_options, Hash)
31
39
  @host = host
32
40
  @username = username
33
41
  @ssh_options = ssh_options
@@ -35,10 +43,7 @@ module Aspera
35
43
  end
36
44
 
37
45
  def execute(cmd, input=nil)
38
- if cmd.is_a?(Array)
39
- # concatenate arguments, enclose in double quotes
40
- cmd = cmd.map{|v|%Q("#{v}")}.join(' ')
41
- end
46
+ Aspera.assert_type(cmd, String)
42
47
  Log.log.debug{"cmd=#{cmd}"}
43
48
  response = []
44
49
  Net::SSH.start(@host, @username, @ssh_options) do |session|
@@ -49,9 +54,7 @@ module Aspera
49
54
  channel.on_extended_data do |_chan, _type, data|
50
55
  error_message = "#{cmd}: [#{data.chomp}]"
51
56
  # Happens when windows user hasn't logged in and created home account.
52
- if data.include?('Could not chdir to home directory')
53
- error_message += "\nHint: home not created in Windows?"
54
- end
57
+ error_message += "\nHint: home not created in Windows?" if data.include?('Could not chdir to home directory')
55
58
  raise error_message
56
59
  end
57
60
  # send command to SSH channel (execute) cspell: disable-next-line
@@ -67,3 +70,7 @@ module Aspera
67
70
  end
68
71
  end
69
72
  end
73
+
74
+ # HACK: deactivate ed25519 and ecdsa private keys from SSH identities, as it usually causes problems
75
+ Aspera::Ssh.disable_ed25519_keys if ENV.fetch('ASCLI_ENABLE_ED25519', 'false').eql?('false')
76
+ Aspera::Ssh.disable_ecd_sha2_algorithms if defined?(JRUBY_VERSION) && ENV.fetch('ASCLI_ENABLE_ECDSHA2', 'false').eql?('false')