aspera-cli 4.14.0 → 4.16.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/BUGS.md +29 -3
- data/CHANGELOG.md +300 -185
- data/CONTRIBUTING.md +74 -23
- data/README.md +2346 -1619
- data/bin/ascli +16 -25
- data/bin/asession +15 -15
- data/examples/dascli +2 -2
- data/examples/proxy.pac +1 -1
- data/lib/aspera/aoc.rb +216 -150
- data/lib/aspera/ascmd.rb +25 -18
- data/lib/aspera/assert.rb +45 -0
- data/lib/aspera/cli/basic_auth_plugin.rb +9 -6
- data/lib/aspera/cli/error.rb +17 -0
- data/lib/aspera/cli/extended_value.rb +51 -16
- data/lib/aspera/cli/formatter.rb +276 -174
- data/lib/aspera/cli/hints.rb +81 -0
- data/lib/aspera/cli/main.rb +114 -147
- data/lib/aspera/cli/manager.rb +181 -136
- data/lib/aspera/cli/plugin.rb +82 -64
- data/lib/aspera/cli/plugins/alee.rb +0 -1
- data/lib/aspera/cli/plugins/aoc.rb +327 -331
- data/lib/aspera/cli/plugins/ats.rb +12 -8
- data/lib/aspera/cli/plugins/bss.rb +2 -2
- data/lib/aspera/cli/plugins/config.rb +575 -439
- data/lib/aspera/cli/plugins/console.rb +40 -0
- data/lib/aspera/cli/plugins/cos.rb +4 -5
- data/lib/aspera/cli/plugins/faspex.rb +111 -92
- data/lib/aspera/cli/plugins/faspex5.rb +245 -182
- data/lib/aspera/cli/plugins/node.rb +239 -160
- data/lib/aspera/cli/plugins/orchestrator.rb +56 -19
- data/lib/aspera/cli/plugins/preview.rb +54 -38
- data/lib/aspera/cli/plugins/server.rb +63 -20
- data/lib/aspera/cli/plugins/shares.rb +64 -38
- data/lib/aspera/cli/sync_actions.rb +68 -0
- data/lib/aspera/cli/transfer_agent.rb +64 -67
- data/lib/aspera/cli/transfer_progress.rb +73 -0
- data/lib/aspera/cli/version.rb +1 -1
- data/lib/aspera/colors.rb +3 -1
- data/lib/aspera/command_line_builder.rb +27 -22
- data/lib/aspera/cos_node.rb +6 -4
- data/lib/aspera/coverage.rb +22 -0
- data/lib/aspera/data_repository.rb +33 -2
- data/lib/aspera/environment.rb +21 -8
- data/lib/aspera/fasp/agent_alpha.rb +116 -0
- data/lib/aspera/fasp/agent_base.rb +40 -76
- data/lib/aspera/fasp/agent_connect.rb +21 -22
- data/lib/aspera/fasp/agent_direct.rb +169 -179
- data/lib/aspera/fasp/agent_httpgw.rb +200 -195
- data/lib/aspera/fasp/agent_node.rb +43 -35
- data/lib/aspera/fasp/agent_trsdk.rb +124 -41
- data/lib/aspera/fasp/error_info.rb +2 -2
- data/lib/aspera/fasp/faux_file.rb +52 -0
- data/lib/aspera/fasp/installation.rb +89 -191
- data/lib/aspera/fasp/management.rb +249 -0
- data/lib/aspera/fasp/parameters.rb +86 -47
- data/lib/aspera/fasp/parameters.yaml +75 -8
- data/lib/aspera/fasp/products.rb +162 -0
- data/lib/aspera/fasp/resume_policy.rb +7 -5
- data/lib/aspera/fasp/sync.rb +273 -0
- data/lib/aspera/fasp/transfer_spec.rb +10 -8
- data/lib/aspera/fasp/uri.rb +6 -6
- data/lib/aspera/faspex_gw.rb +11 -8
- data/lib/aspera/faspex_postproc.rb +8 -7
- data/lib/aspera/hash_ext.rb +2 -2
- data/lib/aspera/id_generator.rb +3 -1
- data/lib/aspera/json_rpc.rb +51 -0
- data/lib/aspera/keychain/encrypted_hash.rb +46 -11
- data/lib/aspera/keychain/macos_security.rb +15 -13
- data/lib/aspera/line_logger.rb +23 -0
- data/lib/aspera/log.rb +61 -19
- data/lib/aspera/nagios.rb +7 -2
- data/lib/aspera/node.rb +105 -21
- data/lib/aspera/node_simulator.rb +214 -0
- data/lib/aspera/oauth.rb +57 -36
- data/lib/aspera/open_application.rb +4 -4
- data/lib/aspera/persistency_action_once.rb +13 -14
- data/lib/aspera/persistency_folder.rb +5 -4
- data/lib/aspera/preview/file_types.rb +56 -268
- data/lib/aspera/preview/generator.rb +28 -39
- data/lib/aspera/preview/options.rb +2 -0
- data/lib/aspera/preview/terminal.rb +36 -16
- data/lib/aspera/preview/utils.rb +23 -29
- data/lib/aspera/proxy_auto_config.rb +6 -3
- data/lib/aspera/rest.rb +127 -80
- data/lib/aspera/rest_call_error.rb +1 -1
- data/lib/aspera/rest_error_analyzer.rb +16 -14
- data/lib/aspera/rest_errors_aspera.rb +39 -34
- data/lib/aspera/secret_hider.rb +18 -17
- data/lib/aspera/ssh.rb +10 -5
- data/lib/aspera/temp_file_manager.rb +11 -4
- data/lib/aspera/web_auth.rb +10 -7
- data/lib/aspera/web_server_simple.rb +11 -5
- data.tar.gz.sig +0 -0
- metadata +108 -39
- metadata.gz.sig +0 -0
- data/lib/aspera/cli/listener/line_dump.rb +0 -19
- data/lib/aspera/cli/listener/logger.rb +0 -22
- data/lib/aspera/cli/listener/progress.rb +0 -50
- data/lib/aspera/cli/listener/progress_multi.rb +0 -84
- data/lib/aspera/cli/plugins/sync.rb +0 -44
- data/lib/aspera/fasp/listener.rb +0 -13
- data/lib/aspera/sync.rb +0 -213
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'aspera/log'
|
|
4
|
+
require 'aspera/fasp/installation'
|
|
5
|
+
require 'aspera/fasp/agent_direct'
|
|
6
|
+
require 'webrick'
|
|
7
|
+
require 'json'
|
|
8
|
+
|
|
9
|
+
module Aspera
|
|
10
|
+
# this class answers the Faspex /send API and creates a package on Aspera on Cloud
|
|
11
|
+
class NodeSimulatorServlet < WEBrick::HTTPServlet::AbstractServlet
|
|
12
|
+
PATH_TRANSFERS = '/ops/transfers'
|
|
13
|
+
PATH_ONE_TRANSFER = %r{/ops/transfers/(.+)$}
|
|
14
|
+
# @param app_api [Aspera::AoC]
|
|
15
|
+
# @param app_context [String]
|
|
16
|
+
def initialize(server, credentials, transfer)
|
|
17
|
+
super(server)
|
|
18
|
+
@credentials = credentials
|
|
19
|
+
@xfer_manager = Aspera::Fasp::AgentDirect.new
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def do_POST(request, response)
|
|
23
|
+
case request.path
|
|
24
|
+
when PATH_TRANSFERS
|
|
25
|
+
job_id = @xfer_manager.start_transfer(JSON.parse(request.body))
|
|
26
|
+
session = @xfer_manager.sessions_by_job(job_id).first
|
|
27
|
+
result = session[:ts].clone
|
|
28
|
+
result['id'] = job_id
|
|
29
|
+
set_json_response(response, result)
|
|
30
|
+
Log.log.debug{">>> transfer started: #{job_id}"}
|
|
31
|
+
else
|
|
32
|
+
set_json_response(response, [{error: 'Bad request'}], code: 400)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def do_GET(request, response)
|
|
37
|
+
case request.path
|
|
38
|
+
when '/info'
|
|
39
|
+
info = Aspera::Fasp::Installation.instance.ascp_info
|
|
40
|
+
set_json_response(response, {
|
|
41
|
+
application: 'node',
|
|
42
|
+
current_time: Time.now.utc.iso8601(0),
|
|
43
|
+
version: info['ascp_version'].gsub(/ .*$/, ''),
|
|
44
|
+
license_expiration_date: info['expiration_date'],
|
|
45
|
+
license_max_rate: info['maximum_bandwidth'],
|
|
46
|
+
os: %x(uname -srv).chomp,
|
|
47
|
+
aej_status: 'disconnected',
|
|
48
|
+
async_reporting: 'yes',
|
|
49
|
+
transfer_activity_reporting: 'yes',
|
|
50
|
+
transfer_user: 'xfer',
|
|
51
|
+
docroot: 'file:////data/aoc/eudemo-sedemo',
|
|
52
|
+
node_id: '2bbdcc39-f789-4d47-8163-6767fc14f421',
|
|
53
|
+
cluster_id: '6dae2844-d1a9-47a5-916d-9b3eac3ea466',
|
|
54
|
+
acls: [],
|
|
55
|
+
access_key_configuration_capabilities: {
|
|
56
|
+
transfer: %w[
|
|
57
|
+
cipher
|
|
58
|
+
policy
|
|
59
|
+
target_rate_cap_kbps
|
|
60
|
+
target_rate_kbps
|
|
61
|
+
preserve_timestamps
|
|
62
|
+
content_protection_secret
|
|
63
|
+
aggressiveness
|
|
64
|
+
token_encryption_key
|
|
65
|
+
byok_enabled
|
|
66
|
+
bandwidth_flow_network_rc_module
|
|
67
|
+
file_checksum_type],
|
|
68
|
+
server: %w[
|
|
69
|
+
activity_event_logging
|
|
70
|
+
activity_file_event_logging
|
|
71
|
+
recursive_counts
|
|
72
|
+
aej_logging
|
|
73
|
+
wss_enabled
|
|
74
|
+
activity_transfer_ignore_skipped_files
|
|
75
|
+
activity_files_max
|
|
76
|
+
access_key_credentials_encryption_type
|
|
77
|
+
discovery
|
|
78
|
+
auto_delete
|
|
79
|
+
allow
|
|
80
|
+
deny]
|
|
81
|
+
},
|
|
82
|
+
capabilities: [
|
|
83
|
+
{name: 'sync', value: true},
|
|
84
|
+
{name: 'watchfolder', value: true},
|
|
85
|
+
{name: 'symbolic_links', value: true},
|
|
86
|
+
{name: 'move_file', value: true},
|
|
87
|
+
{name: 'move_directory', value: true},
|
|
88
|
+
{name: 'filelock', value: false},
|
|
89
|
+
{name: 'ssh_fingerprint', value: false},
|
|
90
|
+
{name: 'aej_version', value: '1.0'},
|
|
91
|
+
{name: 'page', value: true},
|
|
92
|
+
{name: 'file_id_version', value: '2.0'},
|
|
93
|
+
{name: 'auto_delete', value: false}],
|
|
94
|
+
settings: [
|
|
95
|
+
{name: 'content_protection_required', value: false},
|
|
96
|
+
{name: 'content_protection_strong_pass_required', value: false},
|
|
97
|
+
{name: 'filelock_restriction', value: 'none'},
|
|
98
|
+
{name: 'ssh_fingerprint', value: nil},
|
|
99
|
+
{name: 'wss_enabled', value: false},
|
|
100
|
+
{name: 'wss_port', value: 443}
|
|
101
|
+
]})
|
|
102
|
+
when PATH_TRANSFERS
|
|
103
|
+
result = @xfer_manager.sessions.map { |session| job_to_transfer(session) }
|
|
104
|
+
set_json_response(response, result)
|
|
105
|
+
when PATH_ONE_TRANSFER
|
|
106
|
+
job_id = request.path.match(PATH_ONE_TRANSFER)[1]
|
|
107
|
+
set_json_response(response, job_to_transfer(@xfer_manager.sessions_by_job(job_id).first))
|
|
108
|
+
else
|
|
109
|
+
set_json_response(response, [{error: 'Unknown request'}], code: 400)
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def set_json_response(response, json, code: 200)
|
|
114
|
+
response.status = code
|
|
115
|
+
response['Content-Type'] = 'application/json'
|
|
116
|
+
response.body = json.to_json
|
|
117
|
+
Log.log.trace1{Log.dump('response', json)}
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def job_to_transfer(job)
|
|
121
|
+
session = {
|
|
122
|
+
id: 'bafc72b8-366c-4501-8095-47208183d6b8',
|
|
123
|
+
client_node_id: '',
|
|
124
|
+
server_node_id: '2bbdcc39-f789-4d47-8163-6767fc14f421',
|
|
125
|
+
client_ip_address: '192.168.0.100',
|
|
126
|
+
server_ip_address: '5.10.114.4',
|
|
127
|
+
status: 'running',
|
|
128
|
+
retry_timeout: 3600,
|
|
129
|
+
retry_count: 0,
|
|
130
|
+
start_time_usec: 1701094040000000,
|
|
131
|
+
end_time_usec: nil,
|
|
132
|
+
elapsed_usec: 405312,
|
|
133
|
+
bytes_transferred: 26,
|
|
134
|
+
bytes_written: 26,
|
|
135
|
+
bytes_lost: 0,
|
|
136
|
+
files_completed: 1,
|
|
137
|
+
directories_completed: 0,
|
|
138
|
+
target_rate_kbps: 500000,
|
|
139
|
+
min_rate_kbps: 0,
|
|
140
|
+
calc_rate_kbps: 9900,
|
|
141
|
+
network_delay_usec: 40000,
|
|
142
|
+
avg_rate_kbps: 0.51,
|
|
143
|
+
error_code: 0,
|
|
144
|
+
error_desc: '',
|
|
145
|
+
source_statistics: {
|
|
146
|
+
args_scan_attempted: 1,
|
|
147
|
+
args_scan_completed: 1,
|
|
148
|
+
paths_scan_attempted: 1,
|
|
149
|
+
paths_scan_failed: 0,
|
|
150
|
+
paths_scan_skipped: 0,
|
|
151
|
+
paths_scan_excluded: 0,
|
|
152
|
+
dirs_scan_completed: 0,
|
|
153
|
+
files_scan_completed: 1,
|
|
154
|
+
dirs_xfer_attempted: 0,
|
|
155
|
+
dirs_xfer_fail: 0,
|
|
156
|
+
files_xfer_attempted: 1,
|
|
157
|
+
files_xfer_fail: 0,
|
|
158
|
+
files_xfer_noxfer: 0
|
|
159
|
+
},
|
|
160
|
+
precalc: {
|
|
161
|
+
enabled: true,
|
|
162
|
+
status: 'ready',
|
|
163
|
+
bytes_expected: 0,
|
|
164
|
+
directories_expected: 0,
|
|
165
|
+
files_expected: 0,
|
|
166
|
+
files_excluded: 0,
|
|
167
|
+
files_special: 0,
|
|
168
|
+
files_failed: 1
|
|
169
|
+
}}
|
|
170
|
+
return {
|
|
171
|
+
id: '609a667d-642e-4290-9312-b4d20d3c0159',
|
|
172
|
+
status: 'running',
|
|
173
|
+
start_spec: job[:ts],
|
|
174
|
+
sessions: [session],
|
|
175
|
+
bytes_transferred: 26,
|
|
176
|
+
bytes_written: 26,
|
|
177
|
+
bytes_lost: 0,
|
|
178
|
+
avg_rate_kbps: 0.51,
|
|
179
|
+
files_completed: 1,
|
|
180
|
+
files_skipped: 0,
|
|
181
|
+
directories_completed: 0,
|
|
182
|
+
start_time_usec: 1701094040000000,
|
|
183
|
+
end_time_usec: 1701094040405312,
|
|
184
|
+
elapsed_usec: 405312,
|
|
185
|
+
error_code: 0,
|
|
186
|
+
error_desc: '',
|
|
187
|
+
precalc: {
|
|
188
|
+
status: 'ready',
|
|
189
|
+
bytes_expected: 0,
|
|
190
|
+
files_expected: 0,
|
|
191
|
+
directories_expected: 0,
|
|
192
|
+
files_special: 0,
|
|
193
|
+
files_failed: 1
|
|
194
|
+
},
|
|
195
|
+
files: [{
|
|
196
|
+
id: 'd1b5c112-82b75425-860745fc-93851671-64541bdd',
|
|
197
|
+
path: '/workspaces/45071/packages/bYA_ilq73g.asp-package/contents/data_file.bin',
|
|
198
|
+
start_time_usec: 1701094040000000,
|
|
199
|
+
elapsed_usec: 105616,
|
|
200
|
+
end_time_usec: 1701094040001355,
|
|
201
|
+
status: 'completed',
|
|
202
|
+
error_code: 0,
|
|
203
|
+
error_desc: '',
|
|
204
|
+
size: 26,
|
|
205
|
+
type: 'file',
|
|
206
|
+
checksum_type: 'none',
|
|
207
|
+
checksum: nil,
|
|
208
|
+
start_byte: 0,
|
|
209
|
+
bytes_written: 26,
|
|
210
|
+
session_id: 'bafc72b8-366c-4501-8095-47208183d6b8'}]
|
|
211
|
+
}
|
|
212
|
+
end
|
|
213
|
+
end # NodeSimulatorServlet
|
|
214
|
+
end # Aspera
|
data/lib/aspera/oauth.rb
CHANGED
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
require 'aspera/open_application'
|
|
4
4
|
require 'aspera/web_auth'
|
|
5
5
|
require 'aspera/id_generator'
|
|
6
|
+
require 'aspera/log'
|
|
7
|
+
require 'aspera/assert'
|
|
6
8
|
require 'base64'
|
|
7
9
|
require 'date'
|
|
8
10
|
require 'socket'
|
|
@@ -24,18 +26,23 @@ module Aspera
|
|
|
24
26
|
# OAuth methods supported by default
|
|
25
27
|
STD_AUTH_TYPES = %i[web jwt].freeze
|
|
26
28
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
29
|
+
@@globals = { # rubocop:disable Style/ClassVars
|
|
30
|
+
# remove 5 minutes to account for time offset between client and server (TODO: configurable?)
|
|
31
|
+
jwt_accepted_offset_sec: 300,
|
|
32
|
+
# one hour validity (TODO: configurable?)
|
|
33
|
+
jwt_expiry_offset_sec: 3600,
|
|
34
|
+
# tokens older than 30 minutes will be discarded from cache
|
|
35
|
+
token_cache_expiry_sec: 1800,
|
|
36
|
+
# tokens valid for less than this duration will be regenerated
|
|
37
|
+
token_expiration_guard_sec: 120
|
|
38
|
+
}
|
|
39
|
+
|
|
35
40
|
# a prefix for persistency of tokens (simplify garbage collect)
|
|
36
41
|
PERSIST_CATEGORY_TOKEN = 'token'
|
|
42
|
+
# prefix for bearer token when in header
|
|
43
|
+
BEARER_PREFIX = 'Bearer '
|
|
37
44
|
|
|
38
|
-
private_constant :
|
|
45
|
+
private_constant :PERSIST_CATEGORY_TOKEN, :BEARER_PREFIX
|
|
39
46
|
|
|
40
47
|
# persistency manager
|
|
41
48
|
@persist = nil
|
|
@@ -45,10 +52,23 @@ module Aspera
|
|
|
45
52
|
@id_handlers = {}
|
|
46
53
|
|
|
47
54
|
class << self
|
|
55
|
+
def bearer_build(token)
|
|
56
|
+
return BEARER_PREFIX + token
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def bearer_extract(token)
|
|
60
|
+
assert(bearer?(token)){'not a bearer token, wrong prefix'}
|
|
61
|
+
return token[BEARER_PREFIX.length..-1]
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def bearer?(token)
|
|
65
|
+
return token.start_with?(BEARER_PREFIX)
|
|
66
|
+
end
|
|
67
|
+
|
|
48
68
|
def persist_mgr=(manager)
|
|
49
69
|
@persist = manager
|
|
50
70
|
# cleanup expired tokens
|
|
51
|
-
@persist.garbage_collect(PERSIST_CATEGORY_TOKEN,
|
|
71
|
+
@persist.garbage_collect(PERSIST_CATEGORY_TOKEN, @@globals[:token_cache_expiry_sec])
|
|
52
72
|
end
|
|
53
73
|
|
|
54
74
|
def persist_mgr
|
|
@@ -88,26 +108,28 @@ module Aspera
|
|
|
88
108
|
# @param id_create called to generate unique id for token, for cache
|
|
89
109
|
def register_token_creator(id, lambda_create, id_create)
|
|
90
110
|
Log.log.debug{"registering token creator #{id}"}
|
|
91
|
-
|
|
111
|
+
assert_type(id, Symbol)
|
|
112
|
+
assert_type(lambda_create, Proc)
|
|
113
|
+
assert_type(id_create, Proc)
|
|
92
114
|
@create_handlers[id] = lambda_create
|
|
93
115
|
@id_handlers[id] = id_create
|
|
94
116
|
end
|
|
95
117
|
|
|
96
118
|
# @return one of the registered creators for the given create type
|
|
97
119
|
def token_creator(id)
|
|
98
|
-
|
|
120
|
+
assert(@create_handlers.key?(id)){"token grant method unknown: '#{id}' (#{id.class})"}
|
|
99
121
|
@create_handlers[id]
|
|
100
122
|
end
|
|
101
123
|
|
|
102
124
|
# list of identifiers found in creation parameters that can be used to uniquely identify the token
|
|
103
125
|
def id_creator(id)
|
|
104
|
-
|
|
126
|
+
assert(@id_handlers.key?(id)){"id creator type unknown: #{id}/#{id.class}"}
|
|
105
127
|
@id_handlers[id]
|
|
106
128
|
end
|
|
107
129
|
end # self
|
|
108
130
|
|
|
109
131
|
# JSON Web Signature (JWS) compact serialization: https://datatracker.ietf.org/doc/html/rfc7515
|
|
110
|
-
register_decoder lambda { |token| parts = token.split('.');
|
|
132
|
+
register_decoder lambda { |token| parts = token.split('.'); assert(parts.length.eql?(3)){'not aoc token'}; JSON.parse(Base64.decode64(parts[1]))} # rubocop:disable Style/Semicolon, Layout/LineLength
|
|
111
133
|
|
|
112
134
|
# generic token creation, parameters are provided in :generic
|
|
113
135
|
register_token_creator :generic, lambda { |oauth|
|
|
@@ -134,7 +156,7 @@ module Aspera
|
|
|
134
156
|
OpenApplication.instance.uri(login_page_url)
|
|
135
157
|
# wait for code in request
|
|
136
158
|
received_params = web_server.received_request
|
|
137
|
-
|
|
159
|
+
assert(random_state.eql?(received_params['state'])){'wrong received state'}
|
|
138
160
|
# exchange code for token
|
|
139
161
|
return oauth.create_token(oauth.optional_scope_client_id(add_secret: true).merge(
|
|
140
162
|
grant_type: 'authorization_code',
|
|
@@ -151,11 +173,11 @@ module Aspera
|
|
|
151
173
|
require 'jwt'
|
|
152
174
|
seconds_since_epoch = Time.new.to_i
|
|
153
175
|
Log.log.info{"seconds=#{seconds_since_epoch}"}
|
|
154
|
-
|
|
176
|
+
assert(oauth.specific_parameters[:payload].is_a?(Hash)){'missing JWT payload'}
|
|
155
177
|
jwt_payload = {
|
|
156
|
-
exp: seconds_since_epoch +
|
|
157
|
-
nbf: seconds_since_epoch -
|
|
158
|
-
iat: seconds_since_epoch -
|
|
178
|
+
exp: seconds_since_epoch + @@globals[:jwt_expiry_offset_sec], # expiration time
|
|
179
|
+
nbf: seconds_since_epoch - @@globals[:jwt_accepted_offset_sec], # not before
|
|
180
|
+
iat: seconds_since_epoch - @@globals[:jwt_accepted_offset_sec] + 1, # issued at (we tell a little in the past so that server always accepts)
|
|
159
181
|
jti: SecureRandom.uuid # JWT id
|
|
160
182
|
}.merge(oauth.specific_parameters[:payload])
|
|
161
183
|
Log.log.debug{"JWT jwt_payload=[#{jwt_payload}]"}
|
|
@@ -173,14 +195,14 @@ module Aspera
|
|
|
173
195
|
private
|
|
174
196
|
|
|
175
197
|
# [M]=mandatory [D]=has default value [0]=accept nil
|
|
176
|
-
# :base_url [M]
|
|
198
|
+
# :base_url [M] URL of authentication API
|
|
177
199
|
# :auth
|
|
178
|
-
# :grant_method [M]
|
|
200
|
+
# :grant_method [M] :generic, :web, :jwt, [custom types]
|
|
179
201
|
# :client_id [0]
|
|
180
202
|
# :client_secret [0]
|
|
181
203
|
# :scope [0]
|
|
182
|
-
# :path_token [D]
|
|
183
|
-
# :token_field [D]
|
|
204
|
+
# :path_token [D] API end point to create a token
|
|
205
|
+
# :token_field [D] field in result that contains the token
|
|
184
206
|
# :jwt:private_key_obj [M] for type :jwt
|
|
185
207
|
# :jwt:payload [M] for type :jwt
|
|
186
208
|
# :jwt:headers [0] for type :jwt
|
|
@@ -189,18 +211,16 @@ module Aspera
|
|
|
189
211
|
# :generic [M] for type :generic
|
|
190
212
|
def initialize(a_params)
|
|
191
213
|
Log.log.debug{"auth=#{a_params}"}
|
|
192
|
-
#
|
|
214
|
+
# set default values if not set in parameters common to all types
|
|
193
215
|
@generic_parameters = DEFAULT_CREATE_PARAMS.deep_merge(a_params)
|
|
194
|
-
# legacy
|
|
195
|
-
@generic_parameters[:grant_method] ||= @generic_parameters.delete(:crtype) if @generic_parameters.key?(:crtype) # cspell: disable-line
|
|
196
216
|
# check that type is known
|
|
197
217
|
self.class.token_creator(@generic_parameters[:grant_method])
|
|
198
218
|
# specific parameters for the creation type
|
|
199
219
|
@specific_parameters = @generic_parameters[@generic_parameters[:grant_method]]
|
|
200
220
|
if @generic_parameters[:grant_method].eql?(:web) && @specific_parameters.key?(:redirect_uri)
|
|
201
221
|
uri = URI.parse(@specific_parameters[:redirect_uri])
|
|
202
|
-
|
|
203
|
-
|
|
222
|
+
assert(%w[http https].include?(uri.scheme)){'redirect_uri scheme must be http or https'}
|
|
223
|
+
assert(!uri.port.nil?){'redirect_uri must have a port'}
|
|
204
224
|
# TODO: we could check that host is localhost or local address
|
|
205
225
|
end
|
|
206
226
|
rest_params = {
|
|
@@ -208,13 +228,14 @@ module Aspera
|
|
|
208
228
|
redirect_max: 2
|
|
209
229
|
}
|
|
210
230
|
rest_params[:auth] = a_params[:auth] if a_params.key?(:auth)
|
|
231
|
+
# this is the OAuth API
|
|
211
232
|
@api = Rest.new(rest_params)
|
|
212
|
-
# if needed use from api
|
|
233
|
+
# if those are needed use from @api
|
|
213
234
|
@generic_parameters.delete(:base_url)
|
|
214
235
|
@generic_parameters.delete(:auth)
|
|
215
236
|
@generic_parameters.delete(@generic_parameters[:grant_method])
|
|
216
|
-
Log.dump(:generic_parameters, @generic_parameters)
|
|
217
|
-
Log.dump(:specific_parameters, @specific_parameters)
|
|
237
|
+
Log.log.debug{Log.dump(:generic_parameters, @generic_parameters)}
|
|
238
|
+
Log.log.debug{Log.dump(:specific_parameters, @specific_parameters)}
|
|
218
239
|
end
|
|
219
240
|
|
|
220
241
|
public
|
|
@@ -248,7 +269,7 @@ module Aspera
|
|
|
248
269
|
@generic_parameters[:grant_method],
|
|
249
270
|
self.class.id_creator(@generic_parameters[:grant_method]).call(self), # array, so we flatten later
|
|
250
271
|
@generic_parameters[:scope],
|
|
251
|
-
@api.params.dig(
|
|
272
|
+
@api.params.dig(*%i[auth username])
|
|
252
273
|
].flatten)
|
|
253
274
|
|
|
254
275
|
# get token_data from cache (or nil), token_data is what is returned by /token
|
|
@@ -259,14 +280,14 @@ module Aspera
|
|
|
259
280
|
# `direct` agent is equipped with refresh code
|
|
260
281
|
if !use_refresh_token && !token_data.nil?
|
|
261
282
|
decoded_token = self.class.decode_token(token_data[@generic_parameters[:token_field]])
|
|
262
|
-
Log.dump('decoded_token', decoded_token) unless decoded_token.nil?
|
|
283
|
+
Log.log.debug{Log.dump('decoded_token', decoded_token)} unless decoded_token.nil?
|
|
263
284
|
if decoded_token.is_a?(Hash)
|
|
264
285
|
expires_at_sec =
|
|
265
286
|
if decoded_token['expires_at'].is_a?(String) then DateTime.parse(decoded_token['expires_at']).to_time
|
|
266
287
|
elsif decoded_token['exp'].is_a?(Integer) then Time.at(decoded_token['exp'])
|
|
267
288
|
end
|
|
268
289
|
# force refresh if we see a token too close from expiration
|
|
269
|
-
use_refresh_token = true if expires_at_sec.is_a?(Time) && (expires_at_sec - Time.now) <
|
|
290
|
+
use_refresh_token = true if expires_at_sec.is_a?(Time) && (expires_at_sec - Time.now) < @@globals[:token_expiration_guard_sec]
|
|
270
291
|
Log.log.debug{"Expiration: #{expires_at_sec} / #{use_refresh_token}"}
|
|
271
292
|
end
|
|
272
293
|
end
|
|
@@ -304,9 +325,9 @@ module Aspera
|
|
|
304
325
|
token_data = JSON.parse(json_data)
|
|
305
326
|
self.class.persist_mgr.put(token_id, json_data)
|
|
306
327
|
end # if ! in_cache
|
|
307
|
-
|
|
328
|
+
assert(token_data.key?(@generic_parameters[:token_field])){"API error: No such field in answer: #{@generic_parameters[:token_field]}"}
|
|
308
329
|
# ok we shall have a token here
|
|
309
|
-
return
|
|
330
|
+
return self.class.bearer_build(token_data[@generic_parameters[:token_field]])
|
|
310
331
|
end
|
|
311
332
|
end # OAuth
|
|
312
333
|
end # Aspera
|
|
@@ -27,7 +27,7 @@ module Aspera
|
|
|
27
27
|
def uri_graphical(uri)
|
|
28
28
|
case Aspera::Environment.os
|
|
29
29
|
when Aspera::Environment::OS_X then return system('open', uri.to_s)
|
|
30
|
-
when Aspera::Environment::OS_WINDOWS then return system('start', 'explorer',
|
|
30
|
+
when Aspera::Environment::OS_WINDOWS then return system('start', 'explorer', %Q{"#{uri}"})
|
|
31
31
|
when Aspera::Environment::OS_LINUX then return system('xdg-open', uri.to_s)
|
|
32
32
|
else
|
|
33
33
|
raise "no graphical open method for #{Aspera::Environment.os}"
|
|
@@ -38,7 +38,7 @@ module Aspera
|
|
|
38
38
|
if ENV.key?('EDITOR')
|
|
39
39
|
system(ENV['EDITOR'], file_path.to_s)
|
|
40
40
|
elsif Aspera::Environment.os.eql?(Aspera::Environment::OS_WINDOWS)
|
|
41
|
-
system('notepad.exe',
|
|
41
|
+
system('notepad.exe', %Q{"#{file_path}"})
|
|
42
42
|
else
|
|
43
43
|
uri_graphical(file_path.to_s)
|
|
44
44
|
end
|
|
@@ -59,9 +59,9 @@ module Aspera
|
|
|
59
59
|
when :text
|
|
60
60
|
case the_url.to_s
|
|
61
61
|
when /^http/
|
|
62
|
-
puts "USER ACTION: please enter this url in a browser:\n
|
|
62
|
+
puts "USER ACTION: please enter this url in a browser:\n#{the_url.to_s.red}\n"
|
|
63
63
|
else
|
|
64
|
-
puts "USER ACTION: open this:\n
|
|
64
|
+
puts "USER ACTION: open this:\n#{the_url.to_s.red}\n"
|
|
65
65
|
end
|
|
66
66
|
else
|
|
67
67
|
raise StandardError, "unsupported url open method: #{@url_method}"
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require 'json'
|
|
4
4
|
require 'aspera/log'
|
|
5
|
+
require 'aspera/assert'
|
|
5
6
|
|
|
6
7
|
module Aspera
|
|
7
8
|
# Persist data on file system
|
|
@@ -13,21 +14,19 @@ module Aspera
|
|
|
13
14
|
# @param :parse Optional parse method (default to JSON)
|
|
14
15
|
# @param :format Optional dump method (default to JSON)
|
|
15
16
|
# @param :merge Optional merge data from file to current data
|
|
16
|
-
def initialize(
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
@
|
|
24
|
-
@persisted_object = options[:data]
|
|
25
|
-
@object_id = options[:id]
|
|
17
|
+
def initialize(manager:, data:, id:, delete: nil, parse: nil, format: nil, merge: nil)
|
|
18
|
+
assert(!manager.nil?)
|
|
19
|
+
assert(!data.nil?)
|
|
20
|
+
assert_type(id, String)
|
|
21
|
+
assert(!id.empty?)
|
|
22
|
+
@manager = manager
|
|
23
|
+
@persisted_object = data
|
|
24
|
+
@object_id = id
|
|
26
25
|
# by default , at save time, file is deleted if data is nil
|
|
27
|
-
@delete_condition =
|
|
28
|
-
@persist_format =
|
|
29
|
-
persist_parse =
|
|
30
|
-
persist_merge =
|
|
26
|
+
@delete_condition = delete || lambda{|d|d.empty?}
|
|
27
|
+
@persist_format = format || lambda {|h| JSON.generate(h)}
|
|
28
|
+
persist_parse = parse || lambda {|t| JSON.parse(t)}
|
|
29
|
+
persist_merge = merge || lambda {|current, file| current.concat(file).uniq rescue current}
|
|
31
30
|
value = @manager.get(@object_id)
|
|
32
31
|
persist_merge.call(@persisted_object, persist_parse.call(value)) unless value.nil?
|
|
33
32
|
end
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require 'fileutils'
|
|
4
4
|
require 'aspera/log'
|
|
5
|
+
require 'aspera/assert'
|
|
5
6
|
require 'aspera/environment'
|
|
6
7
|
|
|
7
8
|
# search: persistency_folder PersistencyFolder
|
|
@@ -34,10 +35,10 @@ module Aspera
|
|
|
34
35
|
end
|
|
35
36
|
|
|
36
37
|
def put(object_id, value)
|
|
37
|
-
|
|
38
|
+
assert_type(value, String)
|
|
38
39
|
persist_filepath = id_to_filepath(object_id)
|
|
39
40
|
Log.log.debug{"persistency saving: #{persist_filepath}"}
|
|
40
|
-
|
|
41
|
+
FileUtils.rm_f(persist_filepath)
|
|
41
42
|
File.write(persist_filepath, value)
|
|
42
43
|
Environment.restrict_file_access(persist_filepath)
|
|
43
44
|
@cache[object_id] = value
|
|
@@ -46,7 +47,7 @@ module Aspera
|
|
|
46
47
|
def delete(object_id)
|
|
47
48
|
persist_filepath = id_to_filepath(object_id)
|
|
48
49
|
Log.log.debug{"persistency deleting: #{persist_filepath}"}
|
|
49
|
-
|
|
50
|
+
FileUtils.rm_f(persist_filepath)
|
|
50
51
|
@cache.delete(object_id)
|
|
51
52
|
end
|
|
52
53
|
|
|
@@ -67,7 +68,7 @@ module Aspera
|
|
|
67
68
|
|
|
68
69
|
# @param object_id String or Array
|
|
69
70
|
def id_to_filepath(object_id)
|
|
70
|
-
|
|
71
|
+
assert_type(object_id, String)
|
|
71
72
|
FileUtils.mkdir_p(@folder)
|
|
72
73
|
Environment.restrict_file_access(@folder)
|
|
73
74
|
return File.join(@folder, "#{object_id}#{FILE_SUFFIX}")
|