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.
- checksums.yaml +4 -4
- checksums.yaml.gz.sig +0 -0
- data/CHANGELOG.md +41 -3
- data/CONTRIBUTING.md +69 -142
- data/README.md +687 -461
- data/bin/ascli +5 -14
- data/bin/asession +3 -5
- data/examples/get_proto_file.rb +4 -3
- data/examples/proxy.pac +20 -20
- data/lib/aspera/agent/base.rb +2 -0
- data/lib/aspera/agent/connect.rb +20 -2
- data/lib/aspera/agent/{alpha.rb → desktop.rb} +12 -18
- data/lib/aspera/agent/direct.rb +30 -31
- data/lib/aspera/agent/node.rb +1 -11
- data/lib/aspera/agent/{trsdk.rb → transferd.rb} +37 -51
- data/lib/aspera/api/alee.rb +1 -1
- data/lib/aspera/api/aoc.rb +13 -8
- data/lib/aspera/api/cos_node.rb +1 -1
- data/lib/aspera/api/node.rb +49 -32
- data/lib/aspera/ascp/installation.rb +98 -77
- data/lib/aspera/ascp/management.rb +27 -6
- data/lib/aspera/cli/extended_value.rb +9 -3
- data/lib/aspera/cli/formatter.rb +155 -154
- data/lib/aspera/cli/info.rb +2 -1
- data/lib/aspera/cli/main.rb +12 -0
- data/lib/aspera/cli/manager.rb +4 -4
- data/lib/aspera/cli/plugin.rb +2 -2
- data/lib/aspera/cli/plugins/aoc.rb +134 -73
- data/lib/aspera/cli/plugins/config.rb +114 -83
- data/lib/aspera/cli/plugins/cos.rb +1 -0
- data/lib/aspera/cli/plugins/faspex.rb +4 -2
- data/lib/aspera/cli/plugins/faspex5.rb +29 -14
- data/lib/aspera/cli/plugins/node.rb +51 -41
- data/lib/aspera/cli/transfer_progress.rb +2 -0
- data/lib/aspera/cli/version.rb +1 -1
- data/lib/aspera/command_line_builder.rb +1 -1
- data/lib/aspera/coverage.rb +5 -3
- data/lib/aspera/environment.rb +59 -16
- data/lib/aspera/faspex_postproc.rb +3 -5
- data/lib/aspera/hash_ext.rb +2 -12
- data/lib/aspera/node_simulator.rb +230 -112
- data/lib/aspera/oauth/base.rb +40 -48
- data/lib/aspera/oauth/factory.rb +41 -2
- data/lib/aspera/oauth/jwt.rb +4 -1
- data/lib/aspera/persistency_action_once.rb +1 -1
- data/lib/aspera/persistency_folder.rb +20 -2
- data/lib/aspera/preview/generator.rb +13 -10
- data/lib/aspera/preview/options.rb +2 -2
- data/lib/aspera/preview/terminal.rb +1 -1
- data/lib/aspera/preview/utils.rb +11 -6
- data/lib/aspera/products/connect.rb +82 -0
- data/lib/aspera/products/desktop.rb +30 -0
- data/lib/aspera/products/other.rb +82 -0
- data/lib/aspera/products/transferd.rb +61 -0
- data/lib/aspera/rest.rb +22 -17
- data/lib/aspera/secret_hider.rb +9 -2
- data/lib/aspera/ssh.rb +31 -24
- data/lib/aspera/temp_file_manager.rb +5 -4
- data/lib/aspera/transfer/parameters.rb +2 -1
- data/lib/aspera/transfer/spec.yaml +22 -20
- data/lib/aspera/transfer/sync.rb +1 -5
- data/lib/aspera/transfer/uri.rb +2 -2
- data/lib/aspera/uri_reader.rb +18 -1
- data/lib/transferd_pb.rb +86 -0
- data/lib/transferd_services_pb.rb +84 -0
- data.tar.gz.sig +0 -0
- metadata +13 -166
- metadata.gz.sig +0 -0
- data/examples/build_exec +0 -74
- data/examples/build_exec_rubyc +0 -40
- data/lib/aspera/ascp/products.rb +0 -168
- data/lib/transfer_pb.rb +0 -84
- 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
|
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(:
|
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(:
|
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(:
|
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
|
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: `
|
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 = []
|
data/lib/aspera/preview/utils.rb
CHANGED
@@ -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
|
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
|
45
|
+
# @return nil
|
46
46
|
def external_command(command_sym, command_args)
|
47
47
|
Aspera.assert_values(command_sym, EXTERNAL_TOOLS){'command'}
|
48
|
-
|
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
|
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(:
|
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
|
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]
|
211
|
-
# @param redirect_max
|
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:
|
215
|
-
not_auth_codes:
|
219
|
+
auth: {type: :none},
|
220
|
+
not_auth_codes: ['401'],
|
216
221
|
redirect_max: 0,
|
217
|
-
headers:
|
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
|
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
|
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
|
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.
|
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.
|
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.
|
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?
|
data/lib/aspera/secret_hider.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
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')
|