aspera-cli 4.18.1 → 4.20.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.
Files changed (85) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/CHANGELOG.md +33 -0
  4. data/CONTRIBUTING.md +17 -12
  5. data/README.md +396 -185
  6. data/bin/asession +26 -19
  7. data/examples/build_exec +74 -0
  8. data/examples/{rubyc → build_exec_rubyc} +18 -2
  9. data/examples/get_proto_file.rb +7 -0
  10. data/lib/aspera/agent/alpha.rb +8 -8
  11. data/lib/aspera/agent/base.rb +4 -18
  12. data/lib/aspera/agent/connect.rb +14 -13
  13. data/lib/aspera/agent/direct.rb +123 -120
  14. data/lib/aspera/agent/httpgw.rb +2 -3
  15. data/lib/aspera/agent/node.rb +10 -10
  16. data/lib/aspera/agent/trsdk.rb +17 -20
  17. data/lib/aspera/api/alee.rb +15 -0
  18. data/lib/aspera/api/aoc.rb +128 -99
  19. data/lib/aspera/api/ats.rb +1 -1
  20. data/lib/aspera/api/cos_node.rb +1 -1
  21. data/lib/aspera/api/httpgw.rb +104 -64
  22. data/lib/aspera/api/node.rb +33 -12
  23. data/lib/aspera/ascmd.rb +56 -48
  24. data/lib/aspera/ascp/installation.rb +142 -70
  25. data/lib/aspera/ascp/management.rb +7 -3
  26. data/lib/aspera/ascp/products.rb +13 -7
  27. data/lib/aspera/assert.rb +10 -5
  28. data/lib/aspera/cli/formatter.rb +42 -26
  29. data/lib/aspera/cli/hints.rb +2 -1
  30. data/lib/aspera/cli/info.rb +12 -10
  31. data/lib/aspera/cli/main.rb +16 -13
  32. data/lib/aspera/cli/manager.rb +15 -10
  33. data/lib/aspera/cli/plugin.rb +17 -31
  34. data/lib/aspera/cli/plugin_factory.rb +10 -1
  35. data/lib/aspera/cli/plugins/alee.rb +3 -3
  36. data/lib/aspera/cli/plugins/aoc.rb +222 -194
  37. data/lib/aspera/cli/plugins/ats.rb +16 -14
  38. data/lib/aspera/cli/plugins/config.rb +66 -53
  39. data/lib/aspera/cli/plugins/console.rb +3 -3
  40. data/lib/aspera/cli/plugins/faspex.rb +11 -21
  41. data/lib/aspera/cli/plugins/faspex5.rb +44 -42
  42. data/lib/aspera/cli/plugins/faspio.rb +2 -2
  43. data/lib/aspera/cli/plugins/httpgw.rb +1 -1
  44. data/lib/aspera/cli/plugins/node.rb +155 -96
  45. data/lib/aspera/cli/plugins/orchestrator.rb +14 -13
  46. data/lib/aspera/cli/plugins/preview.rb +8 -9
  47. data/lib/aspera/cli/plugins/server.rb +6 -10
  48. data/lib/aspera/cli/plugins/shares.rb +13 -9
  49. data/lib/aspera/cli/sync_actions.rb +72 -31
  50. data/lib/aspera/cli/transfer_agent.rb +13 -14
  51. data/lib/aspera/cli/transfer_progress.rb +36 -18
  52. data/lib/aspera/cli/version.rb +1 -1
  53. data/lib/aspera/command_line_builder.rb +3 -4
  54. data/lib/aspera/coverage.rb +13 -1
  55. data/lib/aspera/environment.rb +59 -10
  56. data/lib/aspera/faspex_gw.rb +3 -3
  57. data/lib/aspera/json_rpc.rb +1 -1
  58. data/lib/aspera/keychain/encrypted_hash.rb +2 -0
  59. data/lib/aspera/keychain/macos_security.rb +7 -12
  60. data/lib/aspera/log.rb +4 -4
  61. data/lib/aspera/node_simulator.rb +1 -1
  62. data/lib/aspera/oauth/base.rb +39 -45
  63. data/lib/aspera/oauth/factory.rb +11 -4
  64. data/lib/aspera/oauth/generic.rb +4 -8
  65. data/lib/aspera/oauth/jwt.rb +4 -4
  66. data/lib/aspera/oauth/url_json.rb +3 -2
  67. data/lib/aspera/oauth/web.rb +10 -6
  68. data/lib/aspera/persistency_action_once.rb +16 -8
  69. data/lib/aspera/preview/utils.rb +5 -16
  70. data/lib/aspera/rest.rb +100 -76
  71. data/lib/aspera/secret_hider.rb +3 -2
  72. data/lib/aspera/ssh.rb +1 -1
  73. data/lib/aspera/transfer/faux_file.rb +7 -5
  74. data/lib/aspera/transfer/parameters.rb +41 -35
  75. data/lib/aspera/transfer/spec.rb +16 -18
  76. data/lib/aspera/transfer/sync.rb +51 -50
  77. data/lib/aspera/transfer/uri.rb +1 -1
  78. data/lib/aspera/uri_reader.rb +1 -1
  79. data/lib/aspera/web_auth.rb +166 -18
  80. data/lib/aspera/web_server_simple.rb +27 -15
  81. data/lib/transfer_pb.rb +84 -0
  82. data/lib/transfer_services_pb.rb +82 -0
  83. data.tar.gz.sig +0 -0
  84. metadata +25 -6
  85. metadata.gz.sig +0 -0
@@ -4,6 +4,7 @@
4
4
 
5
5
  require 'aspera/command_line_builder'
6
6
  require 'aspera/ascp/installation'
7
+ require 'aspera/agent/direct'
7
8
  require 'aspera/log'
8
9
  require 'aspera/assert'
9
10
  require 'json'
@@ -13,12 +14,12 @@ require 'English'
13
14
 
14
15
  module Aspera
15
16
  module Transfer
16
- # builds command line arg for async
17
+ # builds command line arg for async and execute it
17
18
  module Sync
18
19
  # sync direction, default is push
19
20
  DIRECTIONS = %i[push pull bidi].freeze
20
- # custom JSON for async instance command line options
21
- PARAMS_VX_INSTANCE =
21
+ # JSON for async instance command line options
22
+ CMDLINE_PARAMS_INSTANCE =
22
23
  {
23
24
  'alt_logdir' => { cli: { type: :opt_with_arg}, accepted_types: :string},
24
25
  'watchd' => { cli: { type: :opt_with_arg}, accepted_types: :string},
@@ -28,7 +29,7 @@ module Aspera
28
29
  }.freeze
29
30
 
30
31
  # map sync session parameters to transfer spec: sync -> ts, true if same
31
- PARAMS_VX_SESSION =
32
+ CMDLINE_PARAMS_SESSION =
32
33
  {
33
34
  'name' => { cli: { type: :opt_with_arg}, accepted_types: :string},
34
35
  'local_dir' => { cli: { type: :opt_with_arg}, accepted_types: :string},
@@ -65,13 +66,13 @@ module Aspera
65
66
  'license' => { cli: { type: :envvar, variable: 'ASPERA_SCP_LICENSE'}}
66
67
  }.freeze
67
68
 
68
- CommandLineBuilder.normalize_description(PARAMS_VX_INSTANCE)
69
- CommandLineBuilder.normalize_description(PARAMS_VX_SESSION)
69
+ CommandLineBuilder.normalize_description(CMDLINE_PARAMS_INSTANCE)
70
+ CommandLineBuilder.normalize_description(CMDLINE_PARAMS_SESSION)
70
71
 
71
- PARAMS_VX_KEYS = %w[instance sessions].freeze
72
+ CMDLINE_PARAMS_KEYS = %w[instance sessions].freeze
72
73
 
73
74
  # Translation of transfer spec parameters to async v2 API (asyncs)
74
- TS_TO_PARAMS_V2 = {
75
+ TSPEC_TO_ASYNC_CONF = {
75
76
  'remote_host' => 'remote.host',
76
77
  'remote_user' => 'remote.user',
77
78
  'remote_password' => 'remote.pass',
@@ -83,10 +84,9 @@ module Aspera
83
84
  'tags' => 'tags'
84
85
  }.freeze
85
86
 
86
- ASYNC_EXECUTABLE = 'async'
87
87
  ASYNC_ADMIN_EXECUTABLE = 'asyncadmin'
88
88
 
89
- private_constant :PARAMS_VX_INSTANCE, :PARAMS_VX_SESSION, :PARAMS_VX_KEYS, :TS_TO_PARAMS_V2, :ASYNC_EXECUTABLE, :ASYNC_ADMIN_EXECUTABLE
89
+ private_constant :CMDLINE_PARAMS_INSTANCE, :CMDLINE_PARAMS_SESSION, :CMDLINE_PARAMS_KEYS, :TSPEC_TO_ASYNC_CONF, :ASYNC_ADMIN_EXECUTABLE
90
90
 
91
91
  class << self
92
92
  # Set remote_dir in sync parameters based on transfer spec
@@ -126,7 +126,7 @@ module Aspera
126
126
  remote.delete('ws_port')
127
127
  # add SSH bypass keys when authentication is token and no auth is provided
128
128
  if remote.key?('token') && !remote.key?('pass')
129
- certificates_to_use.concat(Ascp::Installation.instance.aspera_token_ssh_key_paths)
129
+ certificates_to_use.concat(Ascp::Installation.instance.aspera_token_ssh_key_paths(:rsa))
130
130
  end
131
131
  end
132
132
  return certificates_to_use
@@ -134,23 +134,27 @@ module Aspera
134
134
 
135
135
  # @param sync_params [Hash] sync parameters, old or new format
136
136
  # @param block [nil, Proc] block to generate transfer spec, takes: direction (one of DIRECTIONS), local_dir, remote_dir
137
- def start(sync_params, &block)
137
+ def start(
138
+ sync_params,
139
+ &block
140
+ )
141
+ Log.log.debug{Log.dump(:sync_params_initial, sync_params)}
138
142
  Aspera.assert_type(sync_params, Hash)
139
143
  env_args = {
140
144
  args: [],
141
145
  env: {}
142
146
  }
143
147
  if sync_params.key?('local')
148
+ # async native JSON format (conf option)
149
+ Aspera.assert_type(sync_params['local'], Hash){'local'}
144
150
  remote = sync_params['remote']
145
- # async native JSON format (v2)
146
- Aspera.assert_type(remote, Hash)
151
+ Aspera.assert_type(remote, Hash){'remote'}
152
+ Aspera.assert_type(remote['path'], String){'remote path'}
147
153
  # get transfer spec if possible, and feed back to new structure
148
154
  if block
149
155
  transfer_spec = yield((sync_params['direction'] || 'push').to_sym, sync_params['local']['path'], remote['path'])
150
- # async native JSON format
151
- Aspera.assert_type(sync_params['local'], Hash)
152
156
  # translate transfer spec to async parameters
153
- TS_TO_PARAMS_V2.each do |ts_param, sy_path|
157
+ TSPEC_TO_ASYNC_CONF.each do |ts_param, sy_path|
154
158
  next unless transfer_spec.key?(ts_param)
155
159
  sy_dig = sy_path.split('.')
156
160
  param = sy_dig.pop
@@ -160,36 +164,41 @@ module Aspera
160
164
  end
161
165
  update_remote_dir(remote, 'path', transfer_spec)
162
166
  end
163
- remote['connect_mode'] ||= remote.key?('ws_port') ? 'ws' : 'ssh'
167
+ remote['connect_mode'] ||= transfer_spec['wss_enabled'] ? 'ws' : 'ssh'
164
168
  add_certificates = remote_certificates(remote)
165
169
  if !add_certificates.empty?
166
170
  remote['private_key_paths'] ||= []
167
171
  remote['private_key_paths'].concat(add_certificates)
168
172
  end
169
- Aspera.assert_type(sync_params, Hash)
173
+ # '--exclusive-mgmt-port=12345', '--arg-err-path=-',
170
174
  env_args[:args] = ["--conf64=#{Base64.strict_encode64(JSON.generate(sync_params))}"]
175
+ Log.log.debug{Log.dump(:sync_params_enriched, sync_params)}
176
+ agent = Agent::Direct.new
177
+ agent.start_and_monitor_process(session: {}, name: :async, **env_args)
171
178
  elsif sync_params.key?('sessions')
172
- # ascli JSON format (v1)
179
+ # ascli JSON format (cmdline)
180
+ raise StandardError, "Only 'sessions', and optionally 'instance' keys are allowed" unless
181
+ sync_params.keys.push('instance').uniq.sort.eql?(CMDLINE_PARAMS_KEYS)
182
+ Aspera.assert_type(sync_params['sessions'], Array)
183
+ Aspera.assert_type(sync_params['sessions'].first, Hash)
173
184
  if block
174
185
  sync_params['sessions'].each do |session|
186
+ Aspera.assert_type(session['local_dir'], String){'local_dir'}
187
+ Aspera.assert_type(session['remote_dir'], String){'remote_dir'}
175
188
  transfer_spec = yield((session['direction'] || 'push').to_sym, session['local_dir'], session['remote_dir'])
176
- PARAMS_VX_SESSION.each do |async_param, behavior|
189
+ CMDLINE_PARAMS_SESSION.each do |async_param, behavior|
177
190
  if behavior.key?(:ts)
178
191
  tspec_param = behavior[:ts].is_a?(TrueClass) ? async_param : behavior[:ts].to_s
179
192
  session[async_param] ||= transfer_spec[tspec_param] if transfer_spec.key?(tspec_param)
180
193
  end
181
194
  end
182
- session['private_key_paths'] = Ascp::Installation.instance.aspera_token_ssh_key_paths if transfer_spec.key?('token')
195
+ session['private_key_paths'] = Ascp::Installation.instance.aspera_token_ssh_key_paths(:rsa) if transfer_spec.key?('token')
183
196
  update_remote_dir(session, 'remote_dir', transfer_spec)
184
197
  end
185
198
  end
186
- raise StandardError, "Only 'sessions', and optionally 'instance' keys are allowed" unless
187
- sync_params.keys.push('instance').uniq.sort.eql?(PARAMS_VX_KEYS)
188
- Aspera.assert_type(sync_params['sessions'], Array)
189
- Aspera.assert_type(sync_params['sessions'].first, Hash)
190
199
  if sync_params.key?('instance')
191
200
  Aspera.assert_type(sync_params['instance'], Hash)
192
- instance_builder = CommandLineBuilder.new(sync_params['instance'], PARAMS_VX_INSTANCE)
201
+ instance_builder = CommandLineBuilder.new(sync_params['instance'], CMDLINE_PARAMS_INSTANCE)
193
202
  instance_builder.process_params
194
203
  instance_builder.add_env_args(env_args)
195
204
  end
@@ -197,23 +206,19 @@ module Aspera
197
206
  sync_params['sessions'].each do |session_params|
198
207
  Aspera.assert_type(session_params, Hash)
199
208
  Aspera.assert(session_params.key?('name')){'session must contain at least name'}
200
- session_builder = CommandLineBuilder.new(session_params, PARAMS_VX_SESSION)
209
+ session_builder = CommandLineBuilder.new(session_params, CMDLINE_PARAMS_SESSION)
201
210
  session_builder.process_params
202
211
  session_builder.add_env_args(env_args)
203
212
  end
213
+ async_exec = Ascp::Installation.instance.path(:async)
214
+ Process.wait(Environment.secure_spawn(env: env_args[:env], exec: async_exec, args: env_args[:args]))
215
+ if $CHILD_STATUS.exitstatus != 0
216
+ raise "Sync failed with exit: #{$CHILD_STATUS.exitstatus}"
217
+ end
204
218
  else
205
219
  raise 'At least one of `local` or `sessions` must be present in async parameters'
206
220
  end
207
- Log.log.debug{Log.dump(:sync_params, sync_params)}
208
- Log.log.debug{"execute: #{env_args[:env].map{|k, v| "#{k}=\"#{v}\""}.join(' ')} \"#{ASYNC_EXECUTABLE}\" \"#{env_args[:args].join('" "')}\""}
209
- res = system(env_args[:env], [ASYNC_EXECUTABLE, ASYNC_EXECUTABLE], *env_args[:args])
210
- Log.log.debug{"result=#{res}"}
211
- case res
212
- when true then return nil
213
- when false then raise "failed: #{$CHILD_STATUS}"
214
- when nil then raise "not started: #{$CHILD_STATUS}"
215
- else Aspera.error_unexpected_value(res)
216
- end
221
+ return nil
217
222
  end
218
223
 
219
224
  def parse_status(stdout)
@@ -234,15 +239,15 @@ module Aspera
234
239
  end
235
240
 
236
241
  def admin_status(sync_params, session_name)
237
- command_line = [ASYNC_ADMIN_EXECUTABLE, '--quiet']
242
+ arguments = ['--quiet']
238
243
  if sync_params.key?('local')
239
244
  Aspera.assert(!sync_params['name'].nil?){'Missing session name'}
240
245
  Aspera.assert(session_name.nil? || session_name.eql?(sync_params['name'])){'Session not found'}
241
- command_line.push("--name=#{sync_params['name']}")
246
+ arguments.push("--name=#{sync_params['name']}")
242
247
  if sync_params.key?('local_db_dir')
243
- command_line.push("--local-db-dir=#{sync_params['local_db_dir']}")
248
+ arguments.push("--local-db-dir=#{sync_params['local_db_dir']}")
244
249
  elsif sync_params.dig('local', 'path')
245
- command_line.push("--local-dir=#{sync_params.dig('local', 'path')}")
250
+ arguments.push("--local-dir=#{sync_params.dig('local', 'path')}")
246
251
  else
247
252
  raise 'Missing either local_db_dir or local.path'
248
253
  end
@@ -250,22 +255,18 @@ module Aspera
250
255
  session = session_name.nil? ? sync_params['sessions'].first : sync_params['sessions'].find{|s|s['name'].eql?(session_name)}
251
256
  raise "Session #{session_name} not found in #{sync_params['sessions'].map{|s|s['name']}.join(',')}" if session.nil?
252
257
  raise 'Missing session name' if session['name'].nil?
253
- command_line.push("--name=#{session['name']}")
258
+ arguments.push("--name=#{session['name']}")
254
259
  if session.key?('local_db_dir')
255
- command_line.push("--local-db-dir=#{session['local_db_dir']}")
260
+ arguments.push("--local-db-dir=#{session['local_db_dir']}")
256
261
  elsif session.key?('local_dir')
257
- command_line.push("--local-dir=#{session['local_dir']}")
262
+ arguments.push("--local-dir=#{session['local_dir']}")
258
263
  else
259
264
  raise 'Missing either local_db_dir or local_dir'
260
265
  end
261
266
  else
262
267
  raise 'At least one of `local` or `sessions` must be present in async parameters'
263
268
  end
264
- Log.log.debug{"execute: #{command_line.join(' ')}"}
265
- stdout, stderr, status = Open3.capture3(*command_line)
266
- Log.log.debug{"status=#{status}, stderr=#{stderr}"}
267
- Log.log.trace1{"stdout=#{stdout}"}
268
- raise "Sync failed: #{status.exitstatus} : #{stderr}" unless status.success?
269
+ stdout = Environment.secure_capture(exec: ASYNC_ADMIN_EXECUTABLE, args: arguments)
269
270
  return parse_status(stdout)
270
271
  end
271
272
  end
@@ -26,7 +26,7 @@ module Aspera
26
26
  # faspex 4 does not encode trailing base64 padding, fix that to be able to decode properly
27
27
  fixed_query = @fasp_uri.query.gsub(/(=+)$/){|trail_equals|'%3D' * trail_equals.length}
28
28
 
29
- Rest.decode_query(fixed_query).each do |name, value|
29
+ Rest.query_to_h(fixed_query).each do |name, value|
30
30
  case name
31
31
  when 'cookie' then result_ts['cookie'] = value
32
32
  when 'token' then result_ts['token'] = value
@@ -12,7 +12,7 @@ module Aspera
12
12
  uri = URI.parse(uri_to_read)
13
13
  case uri.scheme
14
14
  when 'http', 'https'
15
- return Rest.new(base_url: uri_to_read, redirect_max: 5).call(operation: 'GET', subpath: '', headers: {'Accept' => 'text/plain'})[:data]
15
+ return Rest.new(base_url: uri_to_read, redirect_max: 5).call(operation: 'GET', headers: {'Accept' => '*/*'})[:data]
16
16
  when 'file', NilClass
17
17
  local_file_path = uri.path
18
18
  raise 'URL shall have a path, check syntax' if local_file_path.nil?
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'aspera/web_server_simple'
4
- require 'stringio'
4
+ require 'aspera/assert'
5
5
 
6
6
  module Aspera
7
7
  # servlet called on callback: it records the callback request
@@ -16,41 +16,189 @@ module Aspera
16
16
 
17
17
  def service(request, response)
18
18
  Log.log.debug{"received request from browser #{request.request_method} #{request.path}"}
19
- raise WEBrick::HTTPStatus::MethodNotAllowed, "unexpected method: #{request.request_method}" unless request.request_method.eql?('GET')
20
- raise WEBrick::HTTPStatus::NotFound, "unexpected path: #{request.path}" unless request.path.eql?(@web_auth.expected_path)
21
- # acquire lock and signal change
22
- @web_auth.mutex.synchronize do
23
- @web_auth.query = request.query
24
- @web_auth.cond.signal
25
- end
19
+ Aspera.assert_values(request.request_method, ['GET'], exception_class: WEBrick::HTTPStatus::MethodNotAllowed){'HTTP verb'}
20
+ additionnal_info = @web_auth.signal_request(request)
26
21
  response.status = 200
27
22
  response.content_type = 'text/html'
28
- response.body = '<html><head><title>Ok</title></head><body><h1>Thank you !</h1><p>You can close this window.</p></body></html>'
23
+ response.body = <<~HTML
24
+ <!DOCTYPE html>
25
+ <html lang="en">
26
+ <head>
27
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
28
+ <link rel="icon" href="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIzMiIgaGVpZ2h0PSIzMiIgdmlld0JveD0iMCAwIDMyIDMyIiBmaWxsPSJub25lIiBzdHJva2U9IiMyMjIiIHN0cm9rZS13aWR0aD0iMyI+CiAgPGxpbmUgeDE9IjMiIHkxPSIzIiB4Mj0iMjkiIHkyPSIyOSIgIHN0cm9rZT0icmVkIi8+CiAgPGxpbmUgeDE9IjI5IiB5MT0iMyIgeDI9IjMiIHkyPSIyOSIgc3Ryb2tlPSJyZWQiIC8+Cjwvc3ZnPg==" type="image/svg+xml">
29
+ <title>Close Now</title>
30
+ <style>
31
+ body {
32
+ font-family: Arial, sans-serif;
33
+ text-align: center;
34
+ padding: 2rem;
35
+ margin: 0;
36
+ background: linear-gradient(135deg, #f0f4f8, #d9e2ec);
37
+ color: #333;
38
+ overflow: hidden; /* Ensure no scrollbars for the background animation */
39
+ position: relative;
40
+ }
41
+ h1 {
42
+ font-size: 2.5rem;
43
+ color: #0078d4;
44
+ }
45
+ p {
46
+ font-size: 1.2rem;
47
+ margin-top: 1rem;
48
+ }
49
+
50
+ /* Styling for animated IBM logos */
51
+ .logo {
52
+ position: absolute;
53
+ bottom: -100px;
54
+ width: 40px;
55
+ height: 40px;
56
+ background: none;
57
+ display: flex;
58
+ justify-content: center;
59
+ align-items: center;
60
+ animation: rise 10s infinite ease-in-out;
61
+ }
62
+
63
+ .logo svg {
64
+ width: 100%;
65
+ height: 100%;
66
+ }
67
+
68
+ .logo:nth-child(odd) {
69
+ animation-duration: 8s;
70
+ }
71
+
72
+ .logo:nth-child(even) {
73
+ animation-duration: 12s;
74
+ }
75
+
76
+ @keyframes rise {
77
+ 0% {
78
+ transform: translateY(0) scale(1);
79
+ opacity: 1;
80
+ }
81
+ 50% {
82
+ opacity: 0.7;
83
+ }
84
+ 100% {
85
+ transform: translateY(-120vh) scale(0.7);
86
+ opacity: 0;
87
+ }
88
+ }
89
+ </style>
90
+ </head>
91
+ <body>
92
+ <h1>Thank You!</h1>
93
+ <p>You can close this window.</p>
94
+ <p>#{additionnal_info}</p>
95
+
96
+ <!-- JavaScript to generate IBM logos -->
97
+ <script>
98
+ // Function to create logos dynamically
99
+ function createLogos() {
100
+ const body = document.body;
101
+ const svgContent = `
102
+ <svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="1000px" height="401.149px" viewBox="0 0 1000 401.149" xml:space="preserve">
103
+ <g>
104
+ <rect fill="#1F70C1" x="0" y="373.217" width="194.433" height="27.833"/>
105
+ <rect fill="#1F70C1" x="0" y="319.83" width="194.433" height="27.931"/>
106
+ <rect fill="#1F70C1" x="55.468" y="266.541" width="83.399" height="27.932"/>
107
+ <rect fill="#1F70C1" x="55.468" y="213.253" width="83.399" height="27.932"/>
108
+ <rect fill="#1F70C1" x="55.468" y="159.964" width="83.399" height="27.932"/>
109
+ <rect fill="#1F70C1" x="55.468" y="106.577" width="83.399" height="27.932"/>
110
+ <rect fill="#1F70C1" x="0" y="53.288" width="194.433" height="27.932"/>
111
+ <rect fill="#1F70C1" x="0" y="0" width="194.433" height="27.932"/>
112
+ <path fill="#1F70C1" d="M222.167,347.761h299.029c5.051-8.617,8.815-18.027,11.094-27.932H222.167V347.761z"/>
113
+ <path fill="#1F70C1" d="M497.92,213.253H277.734v27.932h243.463C514.857,230.487,507.032,221.078,497.92,213.253z"/>
114
+ <path fill="#1F70C1" d="M277.734,159.964v27.932H497.92c9.311-7.825,17.135-17.235,23.277-27.932H277.734z"/>
115
+ <path fill="#1F70C1" d="M521.197,53.288H222.167V81.22H532.29C529.715,71.315,525.951,61.906,521.197,53.288z"/>
116
+ <path fill="#1F70C1" d="M429.279,0H222.167v27.932h278.526C482.072,10.697,456.815,0,429.279,0z"/>
117
+ <rect fill="#1F70C1" x="277.734" y="106.577" width="83.3" height="27.932"/>
118
+ <path fill="#1F70C1" d="M444.433,134.509h87.163c2.476-8.914,3.764-18.324,3.764-27.932h-90.927z"/>
119
+ <rect fill="#1F70C1" x="277.734" y="266.541" width="83.3" height="27.932"/>
120
+ <path fill="#1F70C1" d="M444.433,266.541v27.932h90.927c0-9.608-1.288-19.017-3.764-27.932H444.433z"/>
121
+ <path fill="#1F70C1" d="M222.167,400.852h207.112c27.734,0,52.793-10.697,71.513-27.932H222.167V400.852z"/>
122
+ <rect fill="#1F70C1" x="555.567" y="373.217" width="138.866" height="27.833"/>
123
+ <rect fill="#1F70C1" x="555.567" y="319.83" width="138.866" height="27.931"/>
124
+ <rect fill="#1F70C1" x="611.034" y="266.541" width="83.399" height="27.932"/>
125
+ <rect fill="#1F70C1" x="611.034" y="213.253" width="83.399" height="27.932"/>
126
+ <polygon fill="#1F70C1" points="733.063,53.288 555.567,53.288 555.567,81.22 742.67,81.22"/>
127
+ <polygon fill="#1F70C1" points="714.639,0 555.567,0 555.567,27.932 724.247,27.932"/>
128
+ <rect fill="#1F70C1" x="861.034" y="373.217" width="138.866" height="27.833"/>
129
+ <rect fill="#1F70C1" x="861.034" y="319.83" width="138.866" height="27.931"/>
130
+ <rect fill="#1F70C1" x="861.034" y="266.541" width="83.399" height="27.932"/>
131
+ <rect fill="#1F70C1" x="861.034" y="213.253" width="83.399" height="27.932"/>
132
+ <polygon fill="#1F70C1" points="861.034,187.896 944.433,187.896 944.433,159.964 861.034,159.964 694.433,159.964 611.034,159.964 611.034,187.896 694.433,187.896 852.219,187.896"/>
133
+ <polygon fill="#1F70C1" points="944.433,106.577 803.982,106.577 794.374,134.509 944.433,134.509"/>
134
+ <polygon fill="#1F70C1" points="840.927,0 831.319,27.932 1000,27.932 1000,0"/>
135
+ <polygon fill="#1F70C1" points="777.734,400.852 787.341,373.217 768.126,373.217"/>
136
+ <polygon fill="#1F70C1" points="759.311,347.761 796.157,347.761 806.062,319.83 749.505,319.83"/>
137
+ <polygon fill="#1F70C1" points="740.59,294.473 814.877,294.473 824.683,266.541 730.784,266.541"/>
138
+ <polygon fill="#1F70C1" points="721.969,241.185 833.597,241.185 843.106,213.253 712.361,213.253"/>
139
+ <polygon fill="#1F70C1" points="611.034,134.509 761.093,134.509 751.486,106.577 611.034,106.577"/>
140
+ <polygon fill="#1F70C1" points="812.896,81.22 1000,81.22 1000,53.288 822.405,53.288"/>
141
+ </g>
142
+ </svg>
143
+ `;
144
+ for (let i = 0; i < 20; i++) {
145
+ const logo = document.createElement('div');
146
+ logo.className = 'logo';
147
+ const size = Math.random() * 30 + 20; // Random size between 20px and 50px
148
+ logo.style.width = `${size}px`;
149
+ logo.style.height = `${size}px`;
150
+ logo.style.left = `${Math.random() * 100}vw`;
151
+ logo.style.animationDelay = `${Math.random() * 5}s`;
152
+ logo.innerHTML = svgContent;
153
+ body.appendChild(logo);
154
+ }
155
+ }
156
+
157
+ // Call the function to create logos on load
158
+ createLogos();
159
+ </script>
160
+ </body>
161
+ </html>
162
+ HTML
163
+
29
164
  return nil
30
165
  end
31
166
  end
32
167
 
33
- # start a local web server, then start a browser that will callback the local server upon authentication
168
+ # start a local web server
169
+ # then start a browser that will callback the local server upon authentication
170
+ # store the final query
34
171
  class WebAuth < WebServerSimple
35
- attr_reader :expected_path, :mutex, :cond
36
- attr_writer :query
37
-
38
- # @param endpoint_url [String] e.g. 'https://127.0.0.1:12345'
39
- def initialize(endpoint_url)
172
+ # @param endpoint_url [String] e.g. 'https://127.0.0.1:12345'
173
+ # @param additionnal_info [String] Information in web page
174
+ def initialize(endpoint_url, additionnal_info = nil)
40
175
  uri = URI.parse(endpoint_url)
41
176
  super(uri)
42
- # parameters for servlet
43
177
  @mutex = Mutex.new
44
178
  @cond = ConditionVariable.new
45
179
  @expected_path = uri.path.empty? ? '/' : uri.path
46
180
  @query = nil
181
+ @additionnal_info = additionnal_info
47
182
  # last argument (self) is provided to constructor of servlet
48
183
  mount(@expected_path, WebAuthServlet, self)
184
+ # server runs in thread
49
185
  Thread.new { start }
50
186
  end
51
187
 
52
- # wait for request on web server
53
- # @return Hash the query
188
+ # Called by web server thread on received request
189
+ # @return [String] additional information for web page
190
+ def signal_request(request)
191
+ raise WEBrick::HTTPStatus::NotFound, "unexpected path: #{request.path}" unless request.path.eql?(@expected_path)
192
+ # acquire lock and signal change
193
+ @mutex.synchronize do
194
+ @query = request.query
195
+ @cond.signal
196
+ end
197
+ return @additionnal_info
198
+ end
199
+
200
+ # wait for request on web server (main thread)
201
+ # @return [Hash] the query
54
202
  def received_request
55
203
  # wait for signal from thread
56
204
  @mutex.synchronize{@cond.wait(@mutex)}
@@ -4,19 +4,20 @@ require 'webrick'
4
4
  require 'webrick/https'
5
5
  require 'aspera/log'
6
6
  require 'aspera/assert'
7
+ require 'aspera/hash_ext'
7
8
  require 'openssl'
8
9
 
9
10
  module Aspera
10
11
  # Simple WEBrick server with HTTPS support
11
12
  class WebServerSimple < WEBrick::HTTPServer
12
- CERT_PARAMETERS = %i[key cert chain].freeze
13
+ CERT_PARAMETERS = %i[key cert chain pkcs12].freeze
13
14
  GENERIC_ISSUER = '/C=FR/O=Test/OU=Test/CN=Test'
14
15
  ONE_YEAR_SECONDS = 365 * 24 * 60 * 60
15
16
 
16
17
  private_constant :CERT_PARAMETERS, :GENERIC_ISSUER, :ONE_YEAR_SECONDS
17
18
 
18
19
  class << self
19
- # generates and adds self signed cert to provided webrick options
20
+ # Fill and self sign provided certificate
20
21
  def fill_self_signed_cert(cert, key, digest = 'SHA256')
21
22
  cert.subject = cert.issuer = OpenSSL::X509::Name.parse(GENERIC_ISSUER)
22
23
  cert.not_before = cert.not_after = Time.now
@@ -58,24 +59,35 @@ module Aspera
58
59
  Aspera.assert_type(certificate, Hash)
59
60
  certificate = certificate.symbolize_keys
60
61
  raise "unexpected key in certificate config: only: #{CERT_PARAMETERS.join(', ')}" if certificate.keys.any?{|key|!CERT_PARAMETERS.include?(key)}
61
- webrick_options[:SSLPrivateKey] = if certificate.key?(:key)
62
- OpenSSL::PKey::RSA.new(File.read(certificate[:key]))
62
+ if certificate.key?(:pkcs12)
63
+ Log.log.debug('Using PKCS12 certificate')
64
+ raise 'pkcs12 requires a key (password)' unless certificate.key?(:key)
65
+ pkcs12 = OpenSSL::PKCS12.new(File.read(certificate[:pkcs12]), certificate[:key])
66
+ webrick_options[:SSLCertificate] = pkcs12.certificate
67
+ webrick_options[:SSLPrivateKey] = pkcs12.key
68
+ webrick_options[:SSLExtraChainCert] = pkcs12.ca_certs
63
69
  else
64
- OpenSSL::PKey::RSA.new(4096)
65
- end
66
- if certificate.key?(:cert)
67
- webrick_options[:SSLCertificate] = OpenSSL::X509::Certificate.new(File.read(certificate[:cert]))
68
- else
69
- webrick_options[:SSLCertificate] = OpenSSL::X509::Certificate.new
70
- self.class.fill_self_signed_cert(webrick_options[:SSLCertificate], webrick_options[:SSLPrivateKey])
71
- end
72
- if certificate.key?(:chain)
73
- webrick_options[:SSLExtraChainCert] = [OpenSSL::X509::Certificate.new(File.read(certificate[:chain]))]
70
+ Log.log.debug('Using PEM certificate')
71
+ webrick_options[:SSLPrivateKey] = if certificate.key?(:key)
72
+ OpenSSL::PKey::RSA.new(File.read(certificate[:key]))
73
+ else
74
+ OpenSSL::PKey::RSA.new(4096)
75
+ end
76
+ if certificate.key?(:cert)
77
+ webrick_options[:SSLCertificate] = OpenSSL::X509::Certificate.new(File.read(certificate[:cert]))
78
+ else
79
+ webrick_options[:SSLCertificate] = OpenSSL::X509::Certificate.new
80
+ self.class.fill_self_signed_cert(webrick_options[:SSLCertificate], webrick_options[:SSLPrivateKey])
81
+ end
82
+ if certificate.key?(:chain)
83
+ webrick_options[:SSLExtraChainCert] = [OpenSSL::X509::Certificate.new(File.read(certificate[:chain]))]
84
+ end
74
85
  end
75
86
  end
76
87
  end
77
88
  # call constructor of parent class, but capture STDERR
78
- # self signed certificate generates characters on STDERR, see create_self_signed_cert in webrick/ssl.rb
89
+ # self signed certificate generates characters on STDERR
90
+ # see create_self_signed_cert in webrick/ssl.rb
79
91
  Log.capture_stderr { super(webrick_options) }
80
92
  end
81
93