aspera-cli 4.9.0 → 4.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|