aspera-cli 4.27.5 → 4.28.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/CHANGELOG.md +160 -4
- data/CONTRIBUTING.md +4 -2
- data/README.md +1 -1
- data/bin/asession +1 -1
- data/docs/README.md +1086 -331
- data/docs/test-mcp-with-ai.md +5 -3
- data/lib/aspera/agent/direct.rb +15 -4
- data/lib/aspera/agent/factory.rb +2 -1
- data/lib/aspera/agent/node.rb +1 -1
- data/lib/aspera/api/cos_node.rb +1 -1
- data/lib/aspera/api/faspex.rb +1 -1
- data/lib/aspera/api/node.rb +6 -2
- data/lib/aspera/ascp/installation.rb +47 -11
- data/lib/aspera/ascp/management.rb +89 -43
- data/lib/aspera/cli/ascp_actions.rb +5 -19
- data/lib/aspera/cli/bootstrapper.rb +1 -2
- data/lib/aspera/cli/command_registry.rb +43 -3
- data/lib/aspera/cli/command_spec.rb +3 -0
- data/lib/aspera/cli/completion/ascli.bash +39 -5
- data/lib/aspera/cli/completion/ascli.fish +16 -8
- data/lib/aspera/cli/completion/ascli.ps1 +41 -0
- data/lib/aspera/cli/completion/ascli.zsh +20 -4
- data/lib/aspera/cli/completion_actions.rb +360 -0
- data/lib/aspera/cli/error.rb +42 -0
- data/lib/aspera/cli/extended_value.rb +30 -5
- data/lib/aspera/cli/formatter.rb +118 -56
- data/lib/aspera/cli/hints.rb +0 -1
- data/lib/aspera/cli/http.rb +6 -5
- data/lib/aspera/cli/info.rb +10 -4
- data/lib/aspera/cli/mailer.rb +1 -1
- data/lib/aspera/cli/mcp/http_servlet.rb +156 -0
- data/lib/aspera/cli/mcp/secrets.rb +143 -0
- data/lib/aspera/cli/mcp/tool.rb +231 -0
- data/lib/aspera/cli/option_value.rb +9 -1
- data/lib/aspera/cli/options.schema.yaml +390 -39
- data/lib/aspera/cli/parser.rb +14 -5
- data/lib/aspera/cli/plugins/aoc.rb +8 -11
- data/lib/aspera/cli/plugins/base.rb +36 -15
- data/lib/aspera/cli/plugins/config.rb +15 -44
- data/lib/aspera/cli/plugins/console.rb +3 -1
- data/lib/aspera/cli/plugins/factory.rb +1 -1
- data/lib/aspera/cli/plugins/faspex5.rb +9 -19
- data/lib/aspera/cli/plugins/faspio.rb +36 -5
- data/lib/aspera/cli/plugins/httpgw.rb +1 -0
- data/lib/aspera/cli/plugins/mcp.rb +177 -118
- data/lib/aspera/cli/plugins/node.rb +14 -8
- data/lib/aspera/cli/plugins/orchestrator.rb +351 -38
- data/lib/aspera/cli/plugins/server.rb +3 -1
- data/lib/aspera/cli/plugins/shares.rb +205 -35
- data/lib/aspera/cli/preset_actions.rb +15 -7
- data/lib/aspera/cli/preset_manager.rb +24 -2
- data/lib/aspera/cli/prompt.rb +3 -1
- data/lib/aspera/cli/result.rb +17 -4
- data/lib/aspera/cli/runner.rb +133 -43
- data/lib/aspera/cli/security.rb +134 -0
- data/lib/aspera/cli/table_fitter.rb +116 -0
- data/lib/aspera/cli/terminal_formatter.rb +23 -34
- data/lib/aspera/cli/transfer_agent.rb +4 -1
- data/lib/aspera/cli/vault_manager.rb +6 -5
- data/lib/aspera/cli/version.rb +1 -1
- data/lib/aspera/cli/web_secret.html.erb +61 -0
- data/lib/aspera/cli/web_secret.rb +220 -0
- data/lib/aspera/cli/wizard.rb +19 -9
- data/lib/aspera/command_line_builder.rb +26 -0
- data/lib/aspera/data_repository.rb +38 -24
- data/lib/aspera/duration.rb +42 -0
- data/lib/aspera/environment.rb +88 -24
- data/lib/aspera/exec_spec.rb +6 -5
- data/lib/aspera/faspex_gw.rb +98 -32
- data/lib/aspera/faspex_postproc.rb +187 -44
- data/lib/aspera/formatter_interface.rb +0 -1
- data/lib/aspera/id_generator.rb +2 -1
- data/lib/aspera/json_rpc/client.rb +51 -32
- data/lib/aspera/json_rpc/version.rb +1 -0
- data/lib/aspera/keychain/base.rb +5 -0
- data/lib/aspera/keychain/encrypted_hash.rb +6 -2
- data/lib/aspera/keychain/factory.rb +2 -3
- data/lib/aspera/keychain/hashicorp_vault.rb +7 -4
- data/lib/aspera/keychain/macos_security.rb +119 -13
- data/lib/aspera/keychain/one_password_api.rb +8 -3
- data/lib/aspera/keychain/one_password_base.rb +14 -9
- data/lib/aspera/keychain/one_password_cli.rb +4 -2
- data/lib/aspera/log.rb +3 -6
- data/lib/aspera/markdown.rb +152 -115
- data/lib/aspera/mime.rb +6 -0
- data/lib/aspera/nagios.rb +3 -1
- data/lib/aspera/node_emulator.rb +98 -101
- data/lib/aspera/oauth/{base.rb → client/base.rb} +5 -4
- data/lib/aspera/oauth/{boot.rb → client/boot.rb} +3 -2
- data/lib/aspera/oauth/{factory.rb → client/factory.rb} +7 -0
- data/lib/aspera/oauth/{generic.rb → client/generic.rb} +1 -1
- data/lib/aspera/oauth/{json_credentials.rb → client/json_credentials.rb} +1 -1
- data/lib/aspera/oauth/{jwt.rb → client/jwt.rb} +1 -1
- data/lib/aspera/oauth/{url_json.rb → client/url_json.rb} +1 -1
- data/lib/aspera/oauth/{web.rb → client/web.rb} +2 -2
- data/lib/aspera/oauth/server/authorize.html.erb +34 -0
- data/lib/aspera/oauth/server/pkce.rb +426 -0
- data/lib/aspera/orchestrator_workflow_checker.rb +561 -0
- data/lib/aspera/preview/terminal.rb +3 -2
- data/lib/aspera/rainbow.rb +27 -1
- data/lib/aspera/rest/aspera_errors.rb +51 -59
- data/lib/aspera/rest/client.rb +21 -14
- data/lib/aspera/rest/error_analyzer.rb +9 -0
- data/lib/aspera/rest/list.rb +6 -6
- data/lib/aspera/rest/util.rb +7 -1
- data/lib/aspera/rest.rb +0 -1
- data/lib/aspera/schema/IBM Aspera Faspex API-5.0-enhanced.yaml +0 -2
- data/lib/aspera/schema/IBM Aspera Orchestrator API-v1-enhanced.yaml +4001 -0
- data/lib/aspera/schema/IBM Aspera faspio Gateway API-1.0.0-enhanced.yaml +1097 -0
- data/lib/aspera/schema/IBM Aspera on Cloud Automation API-1.0.5-enhanced.yaml +35 -35
- data/lib/aspera/schema/IBM_Aspera_Shares.yaml +819 -7
- data/lib/aspera/schema/documentation.rb +50 -78
- data/lib/aspera/schema/reader.rb +39 -11
- data/lib/aspera/schema/registry.rb +8 -2
- data/lib/aspera/secret_hider.rb +94 -15
- data/lib/aspera/secret_strength.rb +114 -0
- data/lib/aspera/servlet_helpers.rb +111 -0
- data/lib/aspera/sync/args.schema.yaml +3 -1
- data/lib/aspera/sync/conf.schema.yaml +16 -6
- data/lib/aspera/sync/operations.rb +21 -1
- data/lib/aspera/temp_file_manager.rb +79 -20
- data/lib/aspera/transfer/parameters.rb +5 -0
- data/lib/aspera/transfer/spec.rb +52 -12
- data/lib/aspera/transfer/spec.schema.yaml +8 -5
- data/lib/aspera/web_auth/index.html.erb +16 -0
- data/lib/aspera/web_auth/server.rb +18 -10
- data/lib/aspera/web_page.html.erb +262 -0
- data/lib/aspera/web_page.rb +61 -0
- data/lib/aspera/web_server_simple.rb +76 -10
- data.tar.gz.sig +0 -0
- metadata +47 -32
- metadata.gz.sig +0 -0
- data/lib/aspera/cli/mcp_tool.rb +0 -189
- data/lib/aspera/oauth.rb +0 -9
- data/lib/aspera/schema/IBM Aspera Orchestrator API-v1.yaml +0 -1784
- data/lib/aspera/schema/IBM Aspera faspio Gateway API-1.0.0.yaml +0 -249
- data/lib/aspera/web_auth/index.html +0 -147
|
@@ -1,13 +1,20 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'aspera/web_server_simple'
|
|
4
|
+
require 'aspera/servlet_helpers'
|
|
5
|
+
require 'aspera/web_page'
|
|
4
6
|
require 'aspera/assert'
|
|
5
|
-
require 'erb'
|
|
6
7
|
|
|
7
8
|
module Aspera
|
|
8
9
|
module WebAuth
|
|
9
10
|
# servlet called on callback: it records the callback request
|
|
10
11
|
class Servlet < WEBrick::HTTPServlet::AbstractServlet
|
|
12
|
+
include ServletHelpers
|
|
13
|
+
|
|
14
|
+
# ERB template of page displayed in browser after authentication
|
|
15
|
+
HTML_TEMPLATE_PATH = File.join(__dir__, 'index.html.erb')
|
|
16
|
+
private_constant :HTML_TEMPLATE_PATH
|
|
17
|
+
|
|
11
18
|
# @param server [WEBrick::HTTPServer] the HTTP server instance
|
|
12
19
|
# @param web_auth [Server] the Server instance to record the callback
|
|
13
20
|
def initialize(server, web_auth)
|
|
@@ -20,12 +27,14 @@ module Aspera
|
|
|
20
27
|
|
|
21
28
|
def service(request, response)
|
|
22
29
|
Log.log.debug { "received request from browser #{request.request_method} #{request.path}" }
|
|
23
|
-
|
|
24
|
-
|
|
30
|
+
# Refuse requests of other web pages
|
|
31
|
+
check_request(request)
|
|
32
|
+
Aspera.assert_values(request.request_method, ['GET'], type: WEBrick::HTTPStatus::MethodNotAllowed) { "Unsupported method: #{request.request_method}" }
|
|
33
|
+
Aspera.assert_values(request.path, [@web_auth.expected_path], type: WEBrick::HTTPStatus::NotFound) { "Unsupported path: #{request.path}" }
|
|
25
34
|
# response to browser
|
|
26
35
|
response.status = 200
|
|
27
|
-
response
|
|
28
|
-
response.body =
|
|
36
|
+
WebPage.secure_headers(response)
|
|
37
|
+
response.body = WebPage.render(HTML_TEMPLATE_PATH, title: 'Authentication Complete', additional_info: @web_auth.additional_info)
|
|
29
38
|
# continue internal processing
|
|
30
39
|
@web_auth.signal_request(request)
|
|
31
40
|
nil
|
|
@@ -36,7 +45,7 @@ module Aspera
|
|
|
36
45
|
# then start a browser that will callback the local server upon authentication
|
|
37
46
|
# store the final query
|
|
38
47
|
class Server < WebServerSimple
|
|
39
|
-
attr_reader :
|
|
48
|
+
attr_reader :additional_info, :expected_path
|
|
40
49
|
|
|
41
50
|
# @param endpoint_url [String] e.g. 'https://127.0.0.1:12345'
|
|
42
51
|
# @param additional_info [String] Information in web page
|
|
@@ -47,8 +56,7 @@ module Aspera
|
|
|
47
56
|
@cond = ConditionVariable.new
|
|
48
57
|
@expected_path = uri.path.empty? ? '/' : uri.path
|
|
49
58
|
@query = nil
|
|
50
|
-
@
|
|
51
|
-
@info_html = "<p>#{ERB::Util.html_escape(@info_html)}</p>" unless @info_html.empty?
|
|
59
|
+
@additional_info = additional_info
|
|
52
60
|
# last argument (self) is provided to constructor of servlet
|
|
53
61
|
mount(@expected_path, Servlet, self)
|
|
54
62
|
# server runs in thread
|
|
@@ -70,8 +78,8 @@ module Aspera
|
|
|
70
78
|
# wait for request on web server (main thread)
|
|
71
79
|
# @return [Hash] the query
|
|
72
80
|
def received_request
|
|
73
|
-
# wait for signal from thread
|
|
74
|
-
@mutex.synchronize { @cond.wait(@mutex) }
|
|
81
|
+
# wait for signal from thread, unless the request was already received (signal is not memorized)
|
|
82
|
+
@mutex.synchronize { @cond.wait(@mutex) while @query.nil? }
|
|
75
83
|
# tell server thread to stop
|
|
76
84
|
shutdown
|
|
77
85
|
return @query
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
<%# Layout of HTML pages: style and branding, see Aspera::WebPage. Variables: title, content (HTML of the card) %>
|
|
2
|
+
<!DOCTYPE html>
|
|
3
|
+
<html lang="en">
|
|
4
|
+
|
|
5
|
+
<head>
|
|
6
|
+
<meta charset="utf-8">
|
|
7
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
8
|
+
<%# Light and dark themes: the browser adapts form controls and scrollbars too %>
|
|
9
|
+
<meta name="color-scheme" content="light dark">
|
|
10
|
+
<link rel="icon" type="image/svg+xml"
|
|
11
|
+
href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' rx='6' fill='%230f62fe'/%3E%3Ctext x='16' y='22' font-family='monospace' font-size='18' font-weight='bold' fill='white' text-anchor='middle'%3E%3E_%3C/text%3E%3C/svg%3E">
|
|
12
|
+
<title><%= h title %></title>
|
|
13
|
+
<style>
|
|
14
|
+
/* Colors: light theme, overridden by the dark theme of the system or browser */
|
|
15
|
+
:root {
|
|
16
|
+
--page-bg-start: #f0f4f8;
|
|
17
|
+
--page-bg-end: #d9e2ec;
|
|
18
|
+
--card-bg: #ffffff;
|
|
19
|
+
--surface: #f4f4f4;
|
|
20
|
+
--border: #e0e0e0;
|
|
21
|
+
--shadow: 0 4px 24px rgba(0, 0, 0, 0.08), 0 1px 3px rgba(0, 0, 0, 0.04);
|
|
22
|
+
--text: #161616;
|
|
23
|
+
--text-secondary: #525252;
|
|
24
|
+
--text-muted: #8d8d8d;
|
|
25
|
+
--accent: #0f62fe;
|
|
26
|
+
--button-bg: #0f62fe;
|
|
27
|
+
--button-bg-hover: #0043ce;
|
|
28
|
+
--on-button: #ffffff;
|
|
29
|
+
--danger: #da1e28;
|
|
30
|
+
--danger-bg: #da1e28;
|
|
31
|
+
--success: #24a148;
|
|
32
|
+
--icon-bg: #edf5ff;
|
|
33
|
+
--icon-bg-ok: #defbe6;
|
|
34
|
+
--icon-bg-ko: #fff1f1;
|
|
35
|
+
--input-bg: #ffffff;
|
|
36
|
+
--input-border: #8d8d8d;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
@media (prefers-color-scheme: dark) {
|
|
40
|
+
:root {
|
|
41
|
+
--page-bg-start: #121619;
|
|
42
|
+
--page-bg-end: #1d2226;
|
|
43
|
+
--card-bg: #21272a;
|
|
44
|
+
--surface: #2f353a;
|
|
45
|
+
--border: #42484e;
|
|
46
|
+
--shadow: 0 4px 24px rgba(0, 0, 0, 0.45), 0 1px 3px rgba(0, 0, 0, 0.3);
|
|
47
|
+
--text: #f2f4f8;
|
|
48
|
+
--text-secondary: #c1c7cd;
|
|
49
|
+
--text-muted: #878d96;
|
|
50
|
+
--accent: #78a9ff;
|
|
51
|
+
--button-bg: #0f62fe;
|
|
52
|
+
--button-bg-hover: #0050e6;
|
|
53
|
+
--on-button: #ffffff;
|
|
54
|
+
--danger: #ff8389;
|
|
55
|
+
--danger-bg: #da1e28;
|
|
56
|
+
--success: #42be65;
|
|
57
|
+
--icon-bg: rgba(69, 137, 255, 0.18);
|
|
58
|
+
--icon-bg-ok: rgba(66, 190, 101, 0.18);
|
|
59
|
+
--icon-bg-ko: rgba(250, 77, 86, 0.18);
|
|
60
|
+
--input-bg: #121619;
|
|
61
|
+
--input-border: #697077;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
* {
|
|
66
|
+
box-sizing: border-box;
|
|
67
|
+
margin: 0;
|
|
68
|
+
padding: 0;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
body {
|
|
72
|
+
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
|
73
|
+
background: linear-gradient(135deg, var(--page-bg-start) 0%, var(--page-bg-end) 100%);
|
|
74
|
+
min-height: 100vh;
|
|
75
|
+
display: flex;
|
|
76
|
+
align-items: center;
|
|
77
|
+
justify-content: center;
|
|
78
|
+
padding: 1.5rem;
|
|
79
|
+
color: var(--text);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
.card {
|
|
83
|
+
background: var(--card-bg);
|
|
84
|
+
border-radius: 12px;
|
|
85
|
+
box-shadow: var(--shadow);
|
|
86
|
+
border: 1px solid var(--border);
|
|
87
|
+
max-width: 460px;
|
|
88
|
+
width: 100%;
|
|
89
|
+
padding: 2.25rem 2rem;
|
|
90
|
+
text-align: center;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
.brand {
|
|
94
|
+
display: inline-flex;
|
|
95
|
+
align-items: center;
|
|
96
|
+
gap: 0.5rem;
|
|
97
|
+
margin-bottom: 1.5rem;
|
|
98
|
+
padding: 0.25rem 0.75rem;
|
|
99
|
+
background: var(--surface);
|
|
100
|
+
border-radius: 16px;
|
|
101
|
+
font-size: 0.8rem;
|
|
102
|
+
color: var(--text-secondary);
|
|
103
|
+
border: 1px solid var(--border);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
.brand .badge {
|
|
107
|
+
font-weight: 700;
|
|
108
|
+
color: var(--accent);
|
|
109
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/* Icons are drawn with currentColor */
|
|
113
|
+
.icon-circle {
|
|
114
|
+
width: 56px;
|
|
115
|
+
height: 56px;
|
|
116
|
+
border-radius: 50%;
|
|
117
|
+
margin: 0 auto 1.25rem;
|
|
118
|
+
display: flex;
|
|
119
|
+
align-items: center;
|
|
120
|
+
justify-content: center;
|
|
121
|
+
background: var(--icon-bg);
|
|
122
|
+
color: var(--accent);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
.icon-circle.ok {
|
|
126
|
+
background: var(--icon-bg-ok);
|
|
127
|
+
color: var(--success);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
.icon-circle.ko {
|
|
131
|
+
background: var(--icon-bg-ko);
|
|
132
|
+
color: var(--danger);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
h1 {
|
|
136
|
+
font-size: 1.5rem;
|
|
137
|
+
font-weight: 600;
|
|
138
|
+
color: var(--text);
|
|
139
|
+
margin-bottom: 0.5rem;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
p {
|
|
143
|
+
font-size: 0.95rem;
|
|
144
|
+
line-height: 1.5;
|
|
145
|
+
color: var(--text-secondary);
|
|
146
|
+
margin-top: 0.5rem;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
.client {
|
|
150
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
|
151
|
+
font-weight: 600;
|
|
152
|
+
background: var(--surface);
|
|
153
|
+
padding: 0.15rem 0.4rem;
|
|
154
|
+
border-radius: 4px;
|
|
155
|
+
color: var(--text);
|
|
156
|
+
border: 1px solid var(--border);
|
|
157
|
+
word-break: break-all;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
.actions {
|
|
161
|
+
display: flex;
|
|
162
|
+
gap: 0.75rem;
|
|
163
|
+
margin-top: 1.5rem;
|
|
164
|
+
justify-content: center;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
button {
|
|
168
|
+
flex: 1;
|
|
169
|
+
padding: 0.75rem 1.25rem;
|
|
170
|
+
font-size: 0.95rem;
|
|
171
|
+
font-weight: 500;
|
|
172
|
+
border-radius: 6px;
|
|
173
|
+
cursor: pointer;
|
|
174
|
+
transition: background-color 0.15s ease, border-color 0.15s ease, color 0.15s ease;
|
|
175
|
+
border: 1px solid transparent;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
.allow {
|
|
179
|
+
background: var(--button-bg);
|
|
180
|
+
color: var(--on-button);
|
|
181
|
+
border-color: var(--button-bg);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
.allow:hover {
|
|
185
|
+
background: var(--button-bg-hover);
|
|
186
|
+
border-color: var(--button-bg-hover);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
.deny {
|
|
190
|
+
background: var(--card-bg);
|
|
191
|
+
color: var(--danger);
|
|
192
|
+
border: 1px solid var(--danger);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
.deny:hover {
|
|
196
|
+
background: var(--danger-bg);
|
|
197
|
+
border-color: var(--danger-bg);
|
|
198
|
+
color: var(--on-button);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
.secret {
|
|
202
|
+
display: block;
|
|
203
|
+
margin-top: 1.25rem;
|
|
204
|
+
text-align: left;
|
|
205
|
+
font-size: 0.875rem;
|
|
206
|
+
color: var(--text-secondary);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
.secret input {
|
|
210
|
+
display: block;
|
|
211
|
+
width: 100%;
|
|
212
|
+
margin-top: 0.35rem;
|
|
213
|
+
padding: 0.6rem 0.75rem;
|
|
214
|
+
font-size: 0.95rem;
|
|
215
|
+
background: var(--input-bg);
|
|
216
|
+
color: var(--text);
|
|
217
|
+
border: 1px solid var(--input-border);
|
|
218
|
+
border-radius: 6px;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
.error {
|
|
222
|
+
color: var(--danger);
|
|
223
|
+
font-weight: 600;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
.info-box {
|
|
227
|
+
background: var(--surface);
|
|
228
|
+
border-radius: 6px;
|
|
229
|
+
padding: 0.75rem 1rem;
|
|
230
|
+
margin-top: 1.25rem;
|
|
231
|
+
font-size: 0.875rem;
|
|
232
|
+
color: var(--text);
|
|
233
|
+
word-break: break-word;
|
|
234
|
+
border-left: 3px solid var(--accent);
|
|
235
|
+
text-align: left;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
.footer {
|
|
239
|
+
margin-top: 1.5rem;
|
|
240
|
+
padding-top: 1rem;
|
|
241
|
+
border-top: 1px solid var(--surface);
|
|
242
|
+
font-size: 0.75rem;
|
|
243
|
+
color: var(--text-muted);
|
|
244
|
+
}
|
|
245
|
+
</style>
|
|
246
|
+
</head>
|
|
247
|
+
|
|
248
|
+
<body>
|
|
249
|
+
<div class="card">
|
|
250
|
+
<div class="brand">
|
|
251
|
+
<span class="badge">ascli</span>
|
|
252
|
+
<span>IBM Aspera CLI</span>
|
|
253
|
+
</div>
|
|
254
|
+
|
|
255
|
+
<%= content %>
|
|
256
|
+
<div class="footer">
|
|
257
|
+
IBM Aspera Command-Line Interface
|
|
258
|
+
</div>
|
|
259
|
+
</div>
|
|
260
|
+
</body>
|
|
261
|
+
|
|
262
|
+
</html>
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'erb'
|
|
4
|
+
|
|
5
|
+
module Aspera
|
|
6
|
+
# HTML pages of embedded web servers, sharing a layout: style and branding.
|
|
7
|
+
# The ERB template of each page is the content of the card of the layout, with its own variables.
|
|
8
|
+
module WebPage
|
|
9
|
+
# ERB template of the layout, with variables `title` and `content`
|
|
10
|
+
LAYOUT_PATH = File.join(__dir__, 'web_page.html.erb')
|
|
11
|
+
# Context of templates: variables as methods, and `h` to escape HTML
|
|
12
|
+
class Context
|
|
13
|
+
include ERB::Util
|
|
14
|
+
|
|
15
|
+
# @param variables [Hash{Symbol => Object}] Variables of the template
|
|
16
|
+
def initialize(variables)
|
|
17
|
+
variables.each { |name, value| define_singleton_method(name) { value } }
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# @param template_path [String] Path of the ERB template
|
|
21
|
+
# @return [String] Result of the template
|
|
22
|
+
def render(template_path)
|
|
23
|
+
ERB.new(File.read(template_path)).result(template_binding)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
# @return [Binding] Binding without local variables, so that they do not hide template variables
|
|
29
|
+
def template_binding = binding
|
|
30
|
+
end
|
|
31
|
+
# Headers of HTML pages: type, and protection against clickjacking, caching, and leak of the URL to other sites.
|
|
32
|
+
# Not `Referrer-Policy: no-referrer`: the browser would send `Origin: null` with a form, refused by ServletHelpers#check_request
|
|
33
|
+
HEADERS = {
|
|
34
|
+
'Content-Type' => 'text/html; charset=utf-8',
|
|
35
|
+
'X-Frame-Options' => 'DENY',
|
|
36
|
+
'Content-Security-Policy' => "frame-ancestors 'none'",
|
|
37
|
+
'Cache-Control' => 'no-store',
|
|
38
|
+
'Referrer-Policy' => 'same-origin'
|
|
39
|
+
}.freeze
|
|
40
|
+
private_constant :LAYOUT_PATH, :Context, :HEADERS
|
|
41
|
+
|
|
42
|
+
class << self
|
|
43
|
+
# Set the headers of an HTML page: type and security headers
|
|
44
|
+
# @param response [WEBrick::HTTPResponse]
|
|
45
|
+
# @return [nil]
|
|
46
|
+
def secure_headers(response)
|
|
47
|
+
HEADERS.each { |name, value| response[name] = value }
|
|
48
|
+
nil
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# @param template_path [String] Path of the ERB template of the content of the page
|
|
52
|
+
# @param title [String] Title of the page
|
|
53
|
+
# @param variables [Hash] Variables of the template of the content
|
|
54
|
+
# @return [String] HTML page
|
|
55
|
+
def render(template_path, title:, **variables)
|
|
56
|
+
content = Context.new(variables).render(template_path)
|
|
57
|
+
Context.new(title: title, content: content).render(LAYOUT_PATH)
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'delegate'
|
|
4
|
+
require 'ipaddr'
|
|
3
5
|
require 'webrick'
|
|
4
6
|
require 'aspera/log'
|
|
5
7
|
require 'aspera/assert'
|
|
@@ -15,9 +17,67 @@ module Aspera
|
|
|
15
17
|
PKCS12_EXT = %w[p12 pfx].map { |i| ".#{i}" }.freeze
|
|
16
18
|
CLOCK_SKEW_OFFSET_SEC = 5
|
|
17
19
|
|
|
18
|
-
private_constant :GENERIC_ISSUER, :ONE_YEAR_SECONDS, :PKCS12_EXT, :CLOCK_SKEW_OFFSET_SEC
|
|
20
|
+
private_constant :PARAMS, :DEFAULT_URL, :GENERIC_ISSUER, :ONE_YEAR_SECONDS, :PKCS12_EXT, :CLOCK_SKEW_OFFSET_SEC
|
|
21
|
+
|
|
22
|
+
# Logger of WEBrick: its information messages (version, start, shutdown) at debug level, the server logs its URL
|
|
23
|
+
class WebrickLogger < SimpleDelegator
|
|
24
|
+
def info(message = nil, &block) = debug(message, &block)
|
|
25
|
+
def info? = debug?
|
|
26
|
+
end
|
|
27
|
+
private_constant :WebrickLogger
|
|
28
|
+
|
|
29
|
+
# @return [URI] URL of the server, with the port it listens on: any free port if `0` was given
|
|
30
|
+
attr_reader :uri
|
|
19
31
|
|
|
20
32
|
class << self
|
|
33
|
+
# @param host [String] Host name or IP address the server listens on
|
|
34
|
+
# @return [Boolean] `true` if the server is reachable from the local host only
|
|
35
|
+
def loopback?(host)
|
|
36
|
+
return true if host.eql?('localhost')
|
|
37
|
+
address = IPAddr.new(host)
|
|
38
|
+
# Older versions of ipaddr do not see IPv4-mapped IPv6 addresses (::ffff:127.0.0.1) as loopback
|
|
39
|
+
address = address.native if address.ipv4_mapped?
|
|
40
|
+
address.loopback?
|
|
41
|
+
rescue IPAddr::Error
|
|
42
|
+
false
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# @param parameters [Hash] Parameters of a command that starts a server (symbol keys): `url`, `cert`, `key`, `chain`, others are for the servlet
|
|
46
|
+
# @return [URI] URL of the server: `url`, or `DEFAULT_URL`
|
|
47
|
+
def url(parameters) = URI.parse(parameters.fetch(:url, DEFAULT_URL))
|
|
48
|
+
|
|
49
|
+
# @param parameters [Hash] Parameters of a command that starts a server, see `url`
|
|
50
|
+
# @return [Hash] Parameters of the servlet: other than `url`, `cert`, `key`, `chain`
|
|
51
|
+
def servlet_parameters(parameters) = parameters.except(:url, *PARAMS)
|
|
52
|
+
|
|
53
|
+
# Start a server with one servlet on the path of `url`, until signal INT or TERM.
|
|
54
|
+
# A server without protection reachable from the network is refused, see `assert_protected`.
|
|
55
|
+
# @param parameters [Hash] Parameters of the command, see `url`
|
|
56
|
+
# @param servlet [Class] Servlet
|
|
57
|
+
# @param protected [Boolean] `true` if requests are authenticated or restricted
|
|
58
|
+
# @param what [String] Parameters that protect the server, for the error message
|
|
59
|
+
# @param type [Exception] Exception to raise
|
|
60
|
+
# @yieldreturn [Array] Arguments of the servlet, evaluated once parameters are checked
|
|
61
|
+
def serve(parameters, servlet, protected:, what:, type: ParameterError)
|
|
62
|
+
uri = url(parameters)
|
|
63
|
+
assert_protected(uri.hostname, protected: protected, what: what, type: type)
|
|
64
|
+
server = new(uri, **parameters.slice(*PARAMS))
|
|
65
|
+
server.mount(uri.path, servlet, *yield)
|
|
66
|
+
server.start
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Refuse a server without protection reachable from the network: anyone could call it
|
|
70
|
+
# @param host [String] Host name or IP address the server listens on
|
|
71
|
+
# @param protected [Boolean] `true` if requests are authenticated or restricted
|
|
72
|
+
# @param what [String] Parameters that protect the server, for the error message
|
|
73
|
+
# @param type [Exception] Exception to raise
|
|
74
|
+
# @raise [Exception] if not protected and `host` is not a loopback address
|
|
75
|
+
def assert_protected(host, protected:, what:, type: ParameterError)
|
|
76
|
+
Aspera.assert(protected || loopback?(host), type: type) do
|
|
77
|
+
"#{what} is required when the server listens on an address that is not loopback (localhost, 127.0.0.0/8, ::1): #{host}"
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
21
81
|
# Generate or fill and self sign certificate
|
|
22
82
|
def self_signed_cert(private_key, digest: 'SHA256')
|
|
23
83
|
cert = OpenSSL::X509::Certificate.new
|
|
@@ -46,18 +106,19 @@ module Aspera
|
|
|
46
106
|
end
|
|
47
107
|
end
|
|
48
108
|
|
|
49
|
-
# @param
|
|
109
|
+
# @param uri [URI] Local address where server will listen (use scheme, host and port only), port `0` for any free port
|
|
50
110
|
# @param cert [String] Path to certificate file, either with extension .p12 or .pfx, else assumed PEM
|
|
51
111
|
# @param key [String] Path to key file (PEM) or passphrase (pkcs12)
|
|
52
112
|
# @param chain [String] Path to certificate chain file (PEM only)
|
|
53
113
|
def initialize(uri, cert: nil, key: nil, chain: nil)
|
|
54
114
|
Aspera.assert_type(uri, URI)
|
|
55
|
-
@uri = uri
|
|
115
|
+
@uri = uri.dup
|
|
56
116
|
# see https://www.rubydoc.info/stdlib/webrick/WEBrick/Config
|
|
57
117
|
webrick_options = {
|
|
58
|
-
|
|
118
|
+
# without brackets of IPv6 address
|
|
119
|
+
BindAddress: @uri.hostname,
|
|
59
120
|
Port: @uri.port,
|
|
60
|
-
Logger: Log.log,
|
|
121
|
+
Logger: WebrickLogger.new(Log.log),
|
|
61
122
|
AccessLog: [[self, WEBrick::AccessLog::COMMON_LOG_FORMAT]] # replace default access log to call local method "<<" below
|
|
62
123
|
}
|
|
63
124
|
case @uri.scheme
|
|
@@ -92,7 +153,7 @@ module Aspera
|
|
|
92
153
|
else
|
|
93
154
|
OpenSSL::X509::Certificate.new(File.read(cert))
|
|
94
155
|
end
|
|
95
|
-
webrick_options[:SSLExtraChainCert] = read_chain_file(chain) unless chain.nil?
|
|
156
|
+
webrick_options[:SSLExtraChainCert] = self.class.read_chain_file(chain) unless chain.nil?
|
|
96
157
|
Aspera.assert(webrick_options[:SSLCertificate].public_key.to_der == webrick_options[:SSLPrivateKey].public_key.to_der, type: Error) { 'key and cert do not match' }
|
|
97
158
|
end
|
|
98
159
|
end
|
|
@@ -100,14 +161,19 @@ module Aspera
|
|
|
100
161
|
# self signed certificate generates characters on STDERR
|
|
101
162
|
# see create_self_signed_cert in webrick/ssl.rb
|
|
102
163
|
Log.capture_stderr { super(webrick_options) }
|
|
164
|
+
@uri.port = config[:Port]
|
|
103
165
|
end
|
|
104
166
|
|
|
105
|
-
#
|
|
167
|
+
# Serve requests until #shutdown.
|
|
168
|
+
# In the main thread, signals INT and TERM call #shutdown.
|
|
169
|
+
# In another thread, signals are left to the application, which calls #shutdown, e.g. INT interrupts the main thread.
|
|
106
170
|
def start
|
|
107
171
|
Log.log.info { "Listening on #{@uri}" }
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
172
|
+
if Thread.current.equal?(Thread.main)
|
|
173
|
+
# kill (-TERM) for graceful shutdown
|
|
174
|
+
handler = proc { shutdown }
|
|
175
|
+
%i{INT TERM}.each { |sig| trap(sig, &handler) }
|
|
176
|
+
end
|
|
111
177
|
super
|
|
112
178
|
end
|
|
113
179
|
|
data.tar.gz.sig
CHANGED
|
Binary file
|