aspera-cli 4.9.0 → 4.10.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- checksums.yaml.gz.sig +0 -0
- data/README.md +749 -667
- data/docs/test_env.conf +6 -4
- data/examples/dascli +9 -4
- data/examples/faspex4.rb +1 -1
- data/examples/node.rb +1 -1
- data/examples/server.rb +1 -1
- data/lib/aspera/cli/info.rb +2 -2
- data/lib/aspera/cli/main.rb +1 -1
- data/lib/aspera/cli/manager.rb +6 -4
- data/lib/aspera/cli/plugin.rb +5 -1
- data/lib/aspera/cli/plugins/aoc.rb +1 -1
- data/lib/aspera/cli/plugins/config.rb +100 -84
- data/lib/aspera/cli/plugins/faspex.rb +3 -3
- data/lib/aspera/cli/plugins/faspex5.rb +5 -1
- data/lib/aspera/cli/plugins/node.rb +24 -4
- data/lib/aspera/cli/plugins/preview.rb +1 -1
- data/lib/aspera/cli/transfer_agent.rb +3 -2
- data/lib/aspera/cli/version.rb +1 -1
- data/lib/aspera/environment.rb +19 -1
- data/lib/aspera/fasp/agent_direct.rb +4 -0
- data/lib/aspera/fasp/agent_httpgw.rb +147 -88
- data/lib/aspera/fasp/installation.rb +4 -4
- data/lib/aspera/fasp/parameters.rb +13 -1
- data/lib/aspera/fasp/parameters.yaml +5 -2
- data/lib/aspera/fasp/resume_policy.rb +1 -1
- data/lib/aspera/keychain/encrypted_hash.rb +36 -98
- data/lib/aspera/keychain/macos_security.rb +127 -57
- data/lib/aspera/persistency_folder.rb +3 -2
- data/lib/aspera/proxy_auto_config.rb +12 -5
- data/lib/aspera/rest.rb +19 -10
- data.tar.gz.sig +0 -0
- metadata +5 -33
- metadata.gz.sig +0 -0
data/lib/aspera/environment.rb
CHANGED
@@ -85,10 +85,28 @@ module Aspera
|
|
85
85
|
if force || !File.exist?(path)
|
86
86
|
File.unlink(path) rescue nil # Windows may give error
|
87
87
|
File.write(path,yield)
|
88
|
-
|
88
|
+
restrict_file_access(path)
|
89
89
|
end
|
90
90
|
return path
|
91
91
|
end
|
92
|
+
|
93
|
+
def restrict_file_access(path,mode: nil)
|
94
|
+
begin
|
95
|
+
if mode.nil?
|
96
|
+
# or FileUtils ?
|
97
|
+
if File.file?(path)
|
98
|
+
mode=0600
|
99
|
+
elsif File.directory?(path)
|
100
|
+
mode=0700
|
101
|
+
else
|
102
|
+
Log.log.debug("No restriction can be set for #{path}");
|
103
|
+
end
|
104
|
+
end
|
105
|
+
File.chmod(mode,path) unless mode.nil?
|
106
|
+
rescue => e
|
107
|
+
Log.log.warn(e.message)
|
108
|
+
end
|
109
|
+
end
|
92
110
|
end
|
93
111
|
end
|
94
112
|
end
|
@@ -272,6 +272,10 @@ module Aspera
|
|
272
272
|
env_args[:env]['ASPERA_SCP_TOKEN'] = session[:options][:regenerate_token].call(true)
|
273
273
|
end
|
274
274
|
end
|
275
|
+
# cannot resolve address
|
276
|
+
#if last_status_event['Code'].to_i.eql?(14)
|
277
|
+
# Log.log.warn("host: #{}")
|
278
|
+
#end
|
275
279
|
raise Fasp::Error.new(last_status_event['Description'],last_status_event['Code'].to_i)
|
276
280
|
else # case
|
277
281
|
raise "unexpected last event type: #{last_status_event['Type']}"
|
@@ -4,8 +4,8 @@ require 'aspera/fasp/agent_base'
|
|
4
4
|
require 'aspera/fasp/transfer_spec'
|
5
5
|
require 'aspera/log'
|
6
6
|
require 'aspera/rest'
|
7
|
-
require 'websocket-client-simple'
|
8
7
|
require 'securerandom'
|
8
|
+
require 'websocket'
|
9
9
|
require 'base64'
|
10
10
|
require 'json'
|
11
11
|
|
@@ -13,16 +13,32 @@ require 'json'
|
|
13
13
|
# https://developer.ibm.com/apis/catalog?search=%22aspera%20http%22
|
14
14
|
module Aspera
|
15
15
|
module Fasp
|
16
|
-
# start a transfer using Aspera HTTP Gateway, using web socket session
|
16
|
+
# start a transfer using Aspera HTTP Gateway, using web socket session for uploads
|
17
17
|
class AgentHttpgw < AgentBase
|
18
18
|
# message returned by HTTP GW in case of success
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
19
|
+
MSG_END_UPLOAD = 'end upload'
|
20
|
+
MSG_END_SLICE = 'end_slice_upload'
|
21
|
+
DEFAULT_OPTIONS = {
|
22
|
+
url: nil,
|
23
|
+
upload_chunksize: 64_000,
|
24
|
+
upload_bar_refresh_sec: 0.5
|
25
|
+
}.freeze
|
26
|
+
DEFAULT_BASE_PATH='/aspera/http-gwy'
|
27
|
+
# upload endpoints
|
28
|
+
V1_UPLOAD='/v1/upload'
|
29
|
+
V2_UPLOAD='/v2/upload'
|
30
|
+
private_constant :DEFAULT_OPTIONS,:MSG_END_UPLOAD,:MSG_END_SLICE,:V1_UPLOAD,:V2_UPLOAD
|
31
|
+
|
23
32
|
# send message on http gw web socket
|
24
|
-
def
|
25
|
-
|
33
|
+
def ws_snd_json(data)
|
34
|
+
@slice_uploads += 1 if data.has_key?(:slice_upload)
|
35
|
+
Log.log.debug{JSON.generate(data)}
|
36
|
+
ws_send(JSON.generate(data))
|
37
|
+
end
|
38
|
+
|
39
|
+
def ws_send(data, type: :text)
|
40
|
+
frame = ::WebSocket::Frame::Outgoing::Client.new(data: data, type: type, version: @ws_handshake.version)
|
41
|
+
@ws_io.write(frame.to_s)
|
26
42
|
end
|
27
43
|
|
28
44
|
def upload(transfer_spec)
|
@@ -48,93 +64,129 @@ module Aspera
|
|
48
64
|
# save so that we can actually read the file later
|
49
65
|
source_paths.push(full_src_filepath)
|
50
66
|
end
|
51
|
-
|
67
|
+
# identify this session uniquely
|
52
68
|
session_id = SecureRandom.uuid
|
53
|
-
|
54
|
-
#
|
55
|
-
|
56
|
-
#
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
69
|
+
@slice_uploads=0
|
70
|
+
# web socket endpoint: by default use v2 (newer gateways), without base64 encoding
|
71
|
+
upload_api_version = V2_UPLOAD
|
72
|
+
# is the latest supported? else revert to old api
|
73
|
+
upload_api_version=V1_UPLOAD unless @api_info['endpoints'].any?{|i|i.include?(upload_api_version)}
|
74
|
+
Log.log.debug{"api version: #{upload_api_version}"}
|
75
|
+
url=File.join(@gw_api.params[:base_url],upload_api_version)
|
76
|
+
#uri = URI.parse(url)
|
77
|
+
# open web socket to end point (equivalent to Net::HTTP.start)
|
78
|
+
http_socket = Rest.start_http_session(url)
|
79
|
+
@ws_io = http_socket.instance_variable_get(:@socket)
|
80
|
+
#@ws_io.debug_output = Log.log
|
81
|
+
@ws_handshake = ::WebSocket::Handshake::Client.new(url: url, headers: {})
|
82
|
+
@ws_io.write(@ws_handshake.to_s)
|
83
|
+
sleep(0.1)
|
84
|
+
@ws_handshake << @ws_io.readuntil("\r\n\r\n")
|
85
|
+
raise 'Error in websocket handshake' unless @ws_handshake.finished?
|
86
|
+
Log.log.debug('ws: handshake success')
|
87
|
+
# data shared between main thread and read thread
|
88
|
+
shared_info={
|
89
|
+
read_exception: nil, # error message if any in callback
|
90
|
+
end_uploads: 0 # number of files totally sent
|
91
|
+
#mutex: Mutex.new
|
92
|
+
#cond_var: ConditionVariable.new
|
93
|
+
}
|
94
|
+
# start read thread
|
95
|
+
ws_read_thread = Thread.new do
|
96
|
+
Log.log.debug('ws: thread: started')
|
97
|
+
frame = ::WebSocket::Frame::Incoming::Client.new
|
98
|
+
loop do
|
99
|
+
begin
|
100
|
+
frame << @ws_io.readuntil("\n")
|
101
|
+
while (msg = frame.next)
|
102
|
+
Log.log.debug("ws: thread: message: #{msg.data} #{shared_info[:end_uploads]}")
|
103
|
+
message = msg.data
|
104
|
+
if message.eql?(MSG_END_UPLOAD)
|
105
|
+
shared_info[:end_uploads] += 1
|
106
|
+
elsif message.eql?(MSG_END_SLICE)
|
107
|
+
else
|
108
|
+
message.chomp!
|
109
|
+
error_message =
|
110
|
+
if message.start_with?('"') && message.end_with?('"')
|
111
|
+
JSON.parse(Base64.strict_decode64(message.chomp[1..-2]))['message']
|
112
|
+
elsif message.start_with?('{') && message.end_with?('}')
|
113
|
+
JSON.parse(message)['message']
|
114
|
+
else
|
115
|
+
"unknown message from gateway: [#{message}]"
|
116
|
+
end
|
117
|
+
raise error_message
|
118
|
+
end
|
71
119
|
end
|
120
|
+
rescue => e
|
121
|
+
shared_info[:read_exception] = e unless e.is_a?(EOFError)
|
122
|
+
break
|
123
|
+
end
|
72
124
|
end
|
73
|
-
|
74
|
-
ws.on(:error) do |e|
|
75
|
-
error = e
|
76
|
-
end
|
77
|
-
ws.on(:open) do
|
78
|
-
Log.log.info('ws: open')
|
79
|
-
end
|
80
|
-
ws.on(:close) do
|
81
|
-
Log.log.info('ws: close')
|
82
|
-
end
|
83
|
-
# open web socket to end point
|
84
|
-
ws.connect("#{@gw_api.params[:base_url]}/upload")
|
85
|
-
# async wait ready
|
86
|
-
while !ws.open? && error.nil?
|
87
|
-
Log.log.info('ws: wait')
|
88
|
-
sleep(0.2)
|
125
|
+
Log.log.debug("ws: thread: stopping #{shared_info[:read_exception]} #{shared_info[:read_exception].class}")
|
89
126
|
end
|
90
127
|
# notify progress bar
|
91
128
|
notify_begin(session_id,total_size)
|
92
129
|
# first step send transfer spec
|
93
130
|
Log.dump(:ws_spec,transfer_spec)
|
94
|
-
|
131
|
+
ws_snd_json(transfer_spec: transfer_spec)
|
95
132
|
# current file index
|
96
133
|
file_index = 0
|
97
134
|
# aggregate size sent
|
98
135
|
sent_bytes = 0
|
99
136
|
# last progress event
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
137
|
+
last_progress_time = nil
|
138
|
+
begin
|
139
|
+
transfer_spec['paths'].each do |item|
|
140
|
+
# TODO: get mime type?
|
141
|
+
file_mime_type = ''
|
142
|
+
file_size = item['file_size']
|
143
|
+
file_name = File.basename(item[item['destination'].nil? ? 'source' : 'destination'])
|
144
|
+
# compute total number of slices
|
145
|
+
numslices = 1 + ((file_size - 1) / @options[:upload_chunksize])
|
146
|
+
File.open(source_paths[file_index]) do |file|
|
147
|
+
# current slice index
|
148
|
+
slicenum = 0
|
149
|
+
while !file.eof?
|
150
|
+
# interrupt main thread if read thread failed
|
151
|
+
raise shared_info[:read_exception] unless shared_info[:read_exception].nil?
|
152
|
+
data = file.read(@options[:upload_chunksize])
|
153
|
+
slice_data = {
|
154
|
+
name: file_name,
|
155
|
+
type: file_mime_type,
|
156
|
+
size: file_size,
|
157
|
+
slice: slicenum,
|
158
|
+
total_slices: numslices,
|
159
|
+
fileIndex: file_index
|
160
|
+
}
|
161
|
+
#Log.dump(:slice_data,slice_data) #if slicenum.eql?(0)
|
162
|
+
if upload_api_version.eql?(V1_UPLOAD)
|
163
|
+
slice_data[:data] = Base64.strict_encode64(data)
|
164
|
+
ws_snd_json(slice_upload: slice_data)
|
165
|
+
else
|
166
|
+
ws_snd_json(slice_upload: slice_data) if slicenum.eql?(0)
|
167
|
+
ws_send(data,type: :binary)
|
168
|
+
Log.log.debug{"ws: sent buffer: #{file_index} / #{slicenum}"}
|
169
|
+
ws_snd_json(slice_upload: slice_data) if slicenum.eql?(numslices-1)
|
170
|
+
end
|
171
|
+
sent_bytes += data.length
|
172
|
+
currenttime = Time.now
|
173
|
+
if last_progress_time.nil? || ((currenttime - last_progress_time) > @options[:upload_bar_refresh_sec])
|
174
|
+
notify_progress(session_id,sent_bytes)
|
175
|
+
last_progress_time = currenttime
|
176
|
+
end
|
177
|
+
slicenum += 1
|
130
178
|
end
|
131
|
-
slicenum += 1
|
132
|
-
raise error unless error.nil?
|
133
179
|
end
|
180
|
+
file_index += 1
|
134
181
|
end
|
135
|
-
file_index += 1
|
136
182
|
end
|
137
|
-
|
183
|
+
Log.log.debug('Finished upload')
|
184
|
+
ws_read_thread.join
|
185
|
+
Log.log.debug{"result: #{shared_info[:end_uploads]} / #{@slice_uploads}"}
|
186
|
+
ws_send(nil, type: :close) unless @ws_io.nil?
|
187
|
+
@ws_io = nil
|
188
|
+
http_socket&.finish
|
189
|
+
notify_progress(session_id,sent_bytes)
|
138
190
|
notify_end(session_id)
|
139
191
|
end
|
140
192
|
|
@@ -153,7 +205,7 @@ module Aspera
|
|
153
205
|
end
|
154
206
|
transfer_spec['download_name'] = dname
|
155
207
|
end
|
156
|
-
creation = @gw_api.create('download',{'transfer_spec' => transfer_spec})[:data]
|
208
|
+
creation = @gw_api.create('v1/download',{'transfer_spec' => transfer_spec})[:data]
|
157
209
|
transfer_uuid = creation['url'].split('/').last
|
158
210
|
file_dest =
|
159
211
|
if transfer_spec['zip_required'] || transfer_spec['paths'].length > 1
|
@@ -164,7 +216,7 @@ module Aspera
|
|
164
216
|
File.basename(transfer_spec['paths'].first['source'])
|
165
217
|
end
|
166
218
|
file_dest = File.join(transfer_spec['destination_root'],file_dest)
|
167
|
-
@gw_api.call({operation: 'GET',subpath: "download/#{transfer_uuid}",save_to_file: file_dest})
|
219
|
+
@gw_api.call({operation: 'GET',subpath: "v1/download/#{transfer_uuid}",save_to_file: file_dest})
|
168
220
|
end
|
169
221
|
|
170
222
|
# start FASP transfer based on transfer spec (hash table)
|
@@ -200,15 +252,22 @@ module Aspera
|
|
200
252
|
|
201
253
|
private
|
202
254
|
|
203
|
-
def initialize(
|
204
|
-
|
205
|
-
|
206
|
-
|
255
|
+
def initialize(opts)
|
256
|
+
Log.log.debug("local options= #{opts}")
|
257
|
+
# set default options and override if specified
|
258
|
+
@options = DEFAULT_OPTIONS.dup
|
259
|
+
raise "httpgw agent parameters (transfer_info): expecting Hash, but have #{opts.class}" unless opts.is_a?(Hash)
|
260
|
+
opts.symbolize_keys.each do |k,v|
|
261
|
+
raise "httpgw agent parameter: Unknown: #{k}, expect one of #{DEFAULT_OPTIONS.keys.map(&:to_s).join(',')}" unless DEFAULT_OPTIONS.has_key?(k)
|
262
|
+
@options[k] = v
|
263
|
+
end
|
264
|
+
raise 'missing param: url' if @options[:url].nil?
|
265
|
+
# remove /v1 from end
|
266
|
+
@options[:url].gsub(%r{/v1/*$},'')
|
207
267
|
super()
|
208
|
-
@gw_api = Rest.new({base_url:
|
209
|
-
api_info = @gw_api.read('info')[:data]
|
210
|
-
Log.log.info(api_info.to_s)
|
211
|
-
@upload_chunksize = 128_000 # TODO: configurable ?
|
268
|
+
@gw_api = Rest.new({base_url: @options[:url]})
|
269
|
+
@api_info = @gw_api.read('v1/info')[:data]
|
270
|
+
Log.log.info(@api_info.to_s)
|
212
271
|
end
|
213
272
|
end # AgentHttpgw
|
214
273
|
end
|
@@ -219,7 +219,7 @@ module Aspera
|
|
219
219
|
return nil unless File.exist?(exe_path)
|
220
220
|
exe_version = nil
|
221
221
|
cmd_out = %x("#{exe_path}" #{vers_arg})
|
222
|
-
raise "An error
|
222
|
+
raise "An error occurred when testing #{ascp_filename}: #{cmd_out}" unless $CHILD_STATUS == 0
|
223
223
|
# get version from ascp, only after full extract, as windows requires DLLs (SSL/TLS/etc...)
|
224
224
|
m = cmd_out.match(/ version ([0-9.]+)/)
|
225
225
|
exe_version = m[1] unless m.nil?
|
@@ -269,12 +269,12 @@ module Aspera
|
|
269
269
|
path(:aspera_conf)
|
270
270
|
ascp_path = File.join(sdk_folder,ascp_filename)
|
271
271
|
raise "No #{ascp_filename} found in SDK archive" unless File.exist?(ascp_path)
|
272
|
-
|
273
|
-
|
272
|
+
Environment.restrict_file_access(ascp_path, mode: 0755)
|
273
|
+
Environment.restrict_file_access(ascp_path.gsub('ascp','ascp4'), mode: 0755)
|
274
274
|
ascp_version = get_ascp_version(File.join(sdk_folder,ascp_filename))
|
275
275
|
trd_path = transferd_filepath
|
276
276
|
Log.log.warn("No #{trd_path} in SDK archive") unless File.exist?(trd_path)
|
277
|
-
|
277
|
+
Environment.restrict_file_access(trd_path, mode: 0755) if File.exist?(trd_path)
|
278
278
|
transferd_version = get_exe_version(trd_path,'version')
|
279
279
|
sdk_version = transferd_version || ascp_version
|
280
280
|
File.write(File.join(sdk_folder,PRODUCT_INFO),"<product><name>IBM Aspera SDK</name><version>#{sdk_version}</version></product>")
|
@@ -51,7 +51,19 @@ module Aspera
|
|
51
51
|
param[:cli] =
|
52
52
|
case i[:cltype]
|
53
53
|
when :envvar then 'env:' + i[:clvarname]
|
54
|
-
when :opt_without_arg
|
54
|
+
when :opt_without_arg then i[:clswitch]
|
55
|
+
when :opt_with_arg
|
56
|
+
values=if i.has_key?(:enum)
|
57
|
+
['enum']
|
58
|
+
elsif i[:accepted_types].is_a?(Array)
|
59
|
+
i[:accepted_types]
|
60
|
+
elsif !i[:accepted_types].nil?
|
61
|
+
[i[:accepted_types]]
|
62
|
+
else
|
63
|
+
raise "error: #{param}"
|
64
|
+
end.map{|n|"{#{n}}"}.join('|')
|
65
|
+
conv=i.has_key?(:clconvert) ? '(conversion)' : ''
|
66
|
+
"#{i[:clswitch]} #{conv}#{values}"
|
55
67
|
else ''
|
56
68
|
end
|
57
69
|
if i.has_key?(:enum)
|
@@ -4,7 +4,7 @@
|
|
4
4
|
# enum : set with list of values for enum types accepted in transfer spec
|
5
5
|
# tragents : supported agents (for doc only)
|
6
6
|
# required : optional, default: false
|
7
|
-
# cltype : ascp: type of parameter
|
7
|
+
# cltype : ascp: type of parameter: :opt_with_arg,:opt_without_arg,:envvar,:defer,:ignore,
|
8
8
|
# clswitch : ascp: switch for ascp command line
|
9
9
|
# clconvert : ascp: transform value: either a Hash with conversion values, or name of class
|
10
10
|
# clvarname : ascp: name of env var
|
@@ -144,8 +144,11 @@ https_fallback_port:
|
|
144
144
|
:cltype: :opt_with_arg
|
145
145
|
:clswitch: "-t"
|
146
146
|
move_after_transfer:
|
147
|
-
:desc: The relative path to which the files will be moved after the transfer at the source side.
|
147
|
+
:desc: The relative path to which the files will be moved after the transfer at the source side. Available as of 3.8.0.
|
148
148
|
:cltype: :opt_with_arg
|
149
|
+
:tragents:
|
150
|
+
- :direct
|
151
|
+
- :node
|
149
152
|
multi_session:
|
150
153
|
:desc: |
|
151
154
|
Use multi-session transfer. max 128.
|
@@ -1,134 +1,72 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require 'aspera/hash_ext'
|
4
|
-
require '
|
4
|
+
require 'symmetric_encryption/core'
|
5
|
+
require 'yaml'
|
5
6
|
|
6
7
|
module Aspera
|
7
8
|
module Keychain
|
8
|
-
class SimpleCipher
|
9
|
-
def initialize(key)
|
10
|
-
@key = Digest::SHA1.hexdigest(key+('*'*23))[0..23]
|
11
|
-
@cipher = OpenSSL::Cipher.new('DES-EDE3-CBC')
|
12
|
-
end
|
13
|
-
|
14
|
-
def encrypt(value)
|
15
|
-
@cipher.encrypt
|
16
|
-
@cipher.key = @key
|
17
|
-
s = @cipher.update(value) + @cipher.final
|
18
|
-
s.unpack1('H*')
|
19
|
-
end
|
20
|
-
|
21
|
-
def decrypt(value)
|
22
|
-
@cipher.decrypt
|
23
|
-
@cipher.key = @key
|
24
|
-
s = [value].pack('H*').unpack('C*').pack('c*')
|
25
|
-
@cipher.update(s) + @cipher.final
|
26
|
-
end
|
27
|
-
end
|
28
|
-
|
29
9
|
# Manage secrets in a simple Hash
|
30
10
|
class EncryptedHash
|
31
|
-
|
32
|
-
ACCEPTED_KEYS = %i[username url
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
raise '
|
37
|
-
@all_secrets
|
11
|
+
CIPHER_NAME='aes-256-cbc'
|
12
|
+
ACCEPTED_KEYS = %i[label username password url description].freeze
|
13
|
+
def initialize(path,current_password)
|
14
|
+
@path=path
|
15
|
+
self.password=current_password
|
16
|
+
raise 'path to vault file shall be String' unless @path.is_a?(String)
|
17
|
+
@all_secrets=File.exist?(@path) ? YAML.load_stream(@cipher.decrypt(File.read(@path))).first : {}
|
38
18
|
end
|
39
19
|
|
40
|
-
def
|
41
|
-
|
42
|
-
|
20
|
+
def password=(new_password)
|
21
|
+
key_bytes=CIPHER_NAME.split('-')[1].to_i/8
|
22
|
+
# derive key from passphrase
|
23
|
+
key="#{new_password}#{"\x0"*key_bytes}"[0..(key_bytes-1)]
|
24
|
+
Log.log.debug("key=[#{key}],#{key.length}")
|
25
|
+
SymmetricEncryption.cipher=@cipher = SymmetricEncryption::Cipher.new(cipher_name: CIPHER_NAME,key: key,encoding: :none)
|
26
|
+
end
|
27
|
+
|
28
|
+
def save
|
29
|
+
File.write(@path, @cipher.encrypt(YAML.dump(@all_secrets)),encoding: 'BINARY')
|
43
30
|
end
|
44
31
|
|
45
32
|
def set(options)
|
46
33
|
raise 'options shall be Hash' unless options.is_a?(Hash)
|
47
34
|
unsupported = options.keys - ACCEPTED_KEYS
|
48
35
|
raise "unsupported options: #{unsupported}" unless unsupported.empty?
|
49
|
-
|
50
|
-
raise
|
51
|
-
|
52
|
-
|
53
|
-
secret = options[:secret]
|
54
|
-
raise 'options shall have secret' if secret.nil?
|
55
|
-
key = identifier(options)
|
56
|
-
raise "secret #{key} already exist, delete first" if @all_secrets.has_key?(key)
|
57
|
-
obj = {username: username, url: url, secret: SimpleCipher.new(key).encrypt(secret)}
|
58
|
-
obj[:description] = options[:description] if options.has_key?(:description)
|
59
|
-
@all_secrets[key] = obj.stringify_keys
|
60
|
-
nil
|
36
|
+
label = options.delete(:label)
|
37
|
+
raise "secret #{label} already exist, delete first" if @all_secrets.has_key?(label)
|
38
|
+
@all_secrets[label] = options.symbolize_keys
|
39
|
+
save
|
61
40
|
end
|
62
41
|
|
63
42
|
def list
|
64
43
|
result = []
|
65
|
-
|
66
|
-
|
67
|
-
normal =
|
68
|
-
|
69
|
-
when String
|
70
|
-
legacy_detected=true
|
71
|
-
{username: name, url: '', secret: value}
|
72
|
-
when Hash then value.symbolize_keys
|
73
|
-
else raise 'error secret must be String (legacy) or Hash (new)'
|
74
|
-
end
|
75
|
-
normal[:description] = '' unless normal.has_key?(:description)
|
76
|
-
extraneous_keys=normal.keys - ACCEPTED_KEYS
|
77
|
-
Log.log.error("wrongs keys in secret hash: #{extraneous_keys.map(&:to_s).join(',')}") unless extraneous_keys.empty?
|
44
|
+
@all_secrets.each do |label,values|
|
45
|
+
normal = values.symbolize_keys
|
46
|
+
normal[:label] = label
|
47
|
+
ACCEPTED_KEYS.each{|k|normal[k] = '' unless normal.has_key?(k)}
|
78
48
|
result.push(normal)
|
79
49
|
end
|
80
|
-
Log.log.warn('Legacy vault format detected in config file, please refer to documentation to convert to new format.') if legacy_detected
|
81
50
|
return result
|
82
51
|
end
|
83
52
|
|
84
53
|
def delete(options)
|
85
54
|
raise 'options shall be Hash' unless options.is_a?(Hash)
|
86
|
-
unsupported = options.keys - %i[
|
55
|
+
unsupported = options.keys - %i[label]
|
87
56
|
raise "unsupported options: #{unsupported}" unless unsupported.empty?
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
key = nil
|
92
|
-
if !url.nil?
|
93
|
-
extk = identifier(options)
|
94
|
-
key = extk if @all_secrets.has_key?(extk)
|
95
|
-
end
|
96
|
-
# backward compatibility: TODO: remove in future ? (make url mandatory ?)
|
97
|
-
key = username if key.nil? && @all_secrets.has_key?(username)
|
98
|
-
raise 'no such secret' if key.nil?
|
99
|
-
@all_secrets.delete(key)
|
57
|
+
label=options[:label]
|
58
|
+
@all_secrets.delete(label)
|
59
|
+
save
|
100
60
|
end
|
101
61
|
|
102
62
|
def get(options)
|
103
63
|
raise 'options shall be Hash' unless options.is_a?(Hash)
|
104
|
-
unsupported = options.keys - %i[
|
64
|
+
unsupported = options.keys - %i[label]
|
105
65
|
raise "unsupported options: #{unsupported}" unless unsupported.empty?
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
if !url.nil?
|
111
|
-
info = @all_secrets[identifier(options)]
|
112
|
-
end
|
113
|
-
# backward compatibility: TODO: remove in future ? (make url mandatory ?)
|
114
|
-
if info.nil?
|
115
|
-
info = @all_secrets[username]
|
116
|
-
end
|
117
|
-
result = options.clone
|
118
|
-
case info
|
119
|
-
when NilClass
|
120
|
-
raise "no such secret: [#{url}|#{username}] in #{@all_secrets.keys.join(',')}"
|
121
|
-
when String
|
122
|
-
result[:secret] = info
|
123
|
-
result[:description] = ''
|
124
|
-
when Hash
|
125
|
-
info=info.symbolize_keys
|
126
|
-
key = identifier(options)
|
127
|
-
plain = SimpleCipher.new(key).decrypt(info[:secret]) rescue info[:secret]
|
128
|
-
result[:secret] = plain
|
129
|
-
result[:description] = info[:description]
|
130
|
-
else raise "#{info.class} is not an expected type"
|
131
|
-
end
|
66
|
+
label=options[:label]
|
67
|
+
result = @all_secrets[label].clone
|
68
|
+
raise "no such entry #{label}" if result.nil?
|
69
|
+
result[:label]=label
|
132
70
|
return result
|
133
71
|
end
|
134
72
|
end
|