cloudron 7.1.1 → 8.0.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.
- package/bin/cloudron +2 -1
- package/package.json +1 -1
- package/src/helper.js +46 -98
- package/src/versions-actions.js +16 -7
- package/src/login-success.html +0 -50
package/bin/cloudron
CHANGED
|
@@ -418,7 +418,8 @@ versionsCommand.command('list')
|
|
|
418
418
|
.action(versionsActions.list);
|
|
419
419
|
|
|
420
420
|
versionsCommand.command('revoke')
|
|
421
|
-
.description('Revoke
|
|
421
|
+
.description('Revoke a version')
|
|
422
|
+
.option('--version <version>', 'Version to revoke (defaults to latest)')
|
|
422
423
|
.action(versionsActions.revoke);
|
|
423
424
|
|
|
424
425
|
versionsCommand.command('update')
|
package/package.json
CHANGED
package/src/helper.js
CHANGED
|
@@ -1,9 +1,6 @@
|
|
|
1
|
-
import crypto from 'crypto';
|
|
2
1
|
import fs from 'fs';
|
|
3
|
-
import http from 'http';
|
|
4
2
|
import open from 'open';
|
|
5
3
|
import path from 'path';
|
|
6
|
-
import readline from 'readline';
|
|
7
4
|
import safe from '@cloudron/safetydance';
|
|
8
5
|
import superagent from '@cloudron/superagent';
|
|
9
6
|
import util from 'util';
|
|
@@ -72,109 +69,60 @@ function parseChangelog(file, version) {
|
|
|
72
69
|
|
|
73
70
|
async function performOidcLogin(adminFqdn, { rejectUnauthorized = true } = {}) {
|
|
74
71
|
const OIDC_CLIENT_ID = 'cid-cli';
|
|
75
|
-
const OIDC_CLIENT_SECRET = 'notused';
|
|
76
72
|
const OIDC_PROVIDER = `https://${adminFqdn}`;
|
|
77
|
-
const OIDC_CALLBACK_URL = 'http://localhost:1312/callback';
|
|
78
73
|
|
|
79
|
-
// Discover OIDC endpoints
|
|
80
74
|
const discoveryRequest = superagent.get(`${OIDC_PROVIDER}/.well-known/openid-configuration`).timeout(60000);
|
|
81
75
|
if (!rejectUnauthorized) discoveryRequest.disableTLSCerts();
|
|
82
76
|
const discoveryResponse = await discoveryRequest;
|
|
83
|
-
const {
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
const
|
|
88
|
-
|
|
89
|
-
// Generate state for CSRF protection
|
|
90
|
-
const state = crypto.randomBytes(16).toString('hex');
|
|
91
|
-
|
|
92
|
-
// Build authorization URL
|
|
93
|
-
const authUrl = new URL(authorization_endpoint);
|
|
94
|
-
authUrl.searchParams.set('response_type', 'code');
|
|
95
|
-
authUrl.searchParams.set('client_id', OIDC_CLIENT_ID);
|
|
96
|
-
authUrl.searchParams.set('redirect_uri', OIDC_CALLBACK_URL);
|
|
97
|
-
authUrl.searchParams.set('state', state);
|
|
98
|
-
authUrl.searchParams.set('scope', 'openid');
|
|
99
|
-
authUrl.searchParams.set('code_challenge', codeChallenge);
|
|
100
|
-
authUrl.searchParams.set('code_challenge_method', 'S256');
|
|
101
|
-
|
|
102
|
-
// Start local HTTP server and wait for the OIDC callback
|
|
103
|
-
const code = await new Promise((resolve, reject) => {
|
|
104
|
-
const server = http.createServer((req, res) => {
|
|
105
|
-
const url = new URL(req.url, 'http://localhost:1312');
|
|
106
|
-
if (url.pathname !== '/callback') {
|
|
107
|
-
res.writeHead(404);
|
|
108
|
-
res.end('Not found');
|
|
109
|
-
return;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
const error = url.searchParams.get('error');
|
|
113
|
-
if (error) {
|
|
114
|
-
const description = url.searchParams.get('error_description') || error;
|
|
115
|
-
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
116
|
-
res.end('<html><body><h1>Login failed</h1><p>' + description + '</p><p>You can close this window.</p></body></html>');
|
|
117
|
-
server.close();
|
|
118
|
-
reject(new Error(`OIDC login failed: ${description}`));
|
|
119
|
-
return;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
const receivedState = url.searchParams.get('state');
|
|
123
|
-
if (receivedState !== state) {
|
|
124
|
-
res.writeHead(400, { 'Content-Type': 'text/html' });
|
|
125
|
-
res.end('<html><body><h1>Login failed</h1><p>State mismatch.</p></body></html>');
|
|
126
|
-
server.close();
|
|
127
|
-
reject(new Error('OIDC state mismatch'));
|
|
128
|
-
return;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
const receivedCode = url.searchParams.get('code');
|
|
132
|
-
const successHtml = fs.readFileSync(path.join(import.meta.dirname, 'login-success.html'), 'utf8');
|
|
133
|
-
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
134
|
-
res.end(successHtml);
|
|
135
|
-
server.close();
|
|
136
|
-
resolve(receivedCode);
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
// without the host ip, it will listen on :: . on mac, which has dual stack disabled, it will listen on ipv6 only
|
|
140
|
-
server.listen(1312, '127.0.0.1', () => {
|
|
141
|
-
// console.log('Login at:');
|
|
142
|
-
// console.log(authUrl.toString());
|
|
143
|
-
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
144
|
-
rl.question('Press ENTER to authenticate using the browser...', () => {
|
|
145
|
-
rl.close();
|
|
146
|
-
open(authUrl.toString());
|
|
147
|
-
});
|
|
148
|
-
});
|
|
149
|
-
|
|
150
|
-
server.on('error', (err) => {
|
|
151
|
-
reject(new Error(`Failed to start local server: ${err.message}`));
|
|
152
|
-
});
|
|
153
|
-
|
|
154
|
-
// Timeout after 2 minutes so the CLI doesn't hang indefinitely
|
|
155
|
-
setTimeout(() => {
|
|
156
|
-
server.close();
|
|
157
|
-
reject(new Error('Login timed out after 2 minutes'));
|
|
158
|
-
}, 120000);
|
|
159
|
-
});
|
|
160
|
-
|
|
161
|
-
// Exchange authorization code for access token
|
|
162
|
-
const tokenBody = new URLSearchParams({
|
|
163
|
-
grant_type: 'authorization_code',
|
|
164
|
-
code,
|
|
165
|
-
redirect_uri: OIDC_CALLBACK_URL,
|
|
166
|
-
client_id: OIDC_CLIENT_ID,
|
|
167
|
-
client_secret: OIDC_CLIENT_SECRET,
|
|
168
|
-
code_verifier: codeVerifier,
|
|
169
|
-
}).toString();
|
|
170
|
-
const tokenRequest = superagent.post(token_endpoint)
|
|
77
|
+
const { device_authorization_endpoint, token_endpoint } = discoveryResponse.body;
|
|
78
|
+
|
|
79
|
+
if (!device_authorization_endpoint) throw new Error('Server does not support device flow. Please update your Cloudron.');
|
|
80
|
+
|
|
81
|
+
const deviceRequest = superagent.post(device_authorization_endpoint)
|
|
171
82
|
.timeout(60000)
|
|
172
83
|
.set('Content-Type', 'application/x-www-form-urlencoded')
|
|
173
|
-
.send(
|
|
174
|
-
if (!rejectUnauthorized)
|
|
175
|
-
const
|
|
84
|
+
.send(new URLSearchParams({ client_id: OIDC_CLIENT_ID, scope: 'openid' }).toString());
|
|
85
|
+
if (!rejectUnauthorized) deviceRequest.disableTLSCerts();
|
|
86
|
+
const deviceResponse = await deviceRequest;
|
|
87
|
+
|
|
88
|
+
const { device_code, user_code, verification_uri_complete, verification_uri, interval: pollInterval = 5 } = deviceResponse.body;
|
|
89
|
+
|
|
90
|
+
console.log(`\nOpen ${verification_uri_complete || verification_uri} in a browser and enter code: ${user_code}\n`);
|
|
91
|
+
|
|
92
|
+
// try to open browser automatically
|
|
93
|
+
safe(open(verification_uri_complete || verification_uri));
|
|
94
|
+
|
|
95
|
+
const startTime = Date.now();
|
|
96
|
+
const timeout = 600000; // 10 minutes
|
|
97
|
+
|
|
98
|
+
while (Date.now() - startTime < timeout) {
|
|
99
|
+
await new Promise((resolve) => setTimeout(resolve, pollInterval * 1000));
|
|
100
|
+
|
|
101
|
+
const tokenRequest = superagent.post(token_endpoint)
|
|
102
|
+
.timeout(60000)
|
|
103
|
+
.ok(() => true)
|
|
104
|
+
.set('Content-Type', 'application/x-www-form-urlencoded')
|
|
105
|
+
.send(new URLSearchParams({
|
|
106
|
+
grant_type: 'urn:ietf:params:oauth:grant-type:device_code',
|
|
107
|
+
device_code,
|
|
108
|
+
client_id: OIDC_CLIENT_ID,
|
|
109
|
+
}).toString());
|
|
110
|
+
if (!rejectUnauthorized) tokenRequest.disableTLSCerts();
|
|
111
|
+
const tokenResponse = await tokenRequest;
|
|
112
|
+
|
|
113
|
+
if (tokenResponse.status === 200) return tokenResponse.body.access_token;
|
|
114
|
+
|
|
115
|
+
const error = tokenResponse.body.error;
|
|
116
|
+
if (error === 'authorization_pending') continue;
|
|
117
|
+
if (error === 'slow_down') {
|
|
118
|
+
await new Promise((resolve) => setTimeout(resolve, 5000));
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
throw new Error(tokenResponse.body.error_description || error);
|
|
123
|
+
}
|
|
176
124
|
|
|
177
|
-
|
|
125
|
+
throw new Error('Login timed out');
|
|
178
126
|
}
|
|
179
127
|
|
|
180
128
|
export {
|
package/src/versions-actions.js
CHANGED
|
@@ -252,21 +252,30 @@ async function list(/*localOptions, cmd*/) {
|
|
|
252
252
|
console.log(t.toString());
|
|
253
253
|
}
|
|
254
254
|
|
|
255
|
-
async function revoke() {
|
|
255
|
+
async function revoke(localOptions, cmd) {
|
|
256
256
|
const versionsFilePath = await locateVersions();
|
|
257
257
|
if (!versionsFilePath) return exit(NO_VERSIONS_FOUND_ERROR_STRING);
|
|
258
258
|
|
|
259
|
+
const options = cmd.optsWithGlobals();
|
|
259
260
|
const versionsRoot = await readVersions(versionsFilePath);
|
|
260
261
|
const versions = versionsRoot.versions;
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
if (
|
|
264
|
-
|
|
262
|
+
|
|
263
|
+
let targetVersion;
|
|
264
|
+
if (options.version) {
|
|
265
|
+
if (!(options.version in versions)) exit(`${options.version} does not exist in ${path.relative(process.cwd(), versionsFilePath)}.`);
|
|
266
|
+
targetVersion = options.version;
|
|
267
|
+
} else {
|
|
268
|
+
const sortedVersions = Object.keys(versions).sort(manifestFormat.packageVersionCompare);
|
|
269
|
+
targetVersion = sortedVersions.at(-1);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
if (versions[targetVersion].publishState !== PUBLISH_STATE_PUBLISHED) {
|
|
273
|
+
return exit(`Only versions in "${PUBLISH_STATE_PUBLISHED}" can be revoked. ${targetVersion} is currently marked as "${versions[targetVersion].publishState}"`);
|
|
265
274
|
}
|
|
266
275
|
|
|
267
|
-
versions[
|
|
276
|
+
versions[targetVersion].publishState = PUBLISH_STATE_REVOKED;
|
|
268
277
|
await writeVersions(versionsFilePath, versionsRoot);
|
|
269
|
-
console.log(`Marked ${
|
|
278
|
+
console.log(`Marked ${targetVersion} as revoked in ${path.relative(process.cwd(), versionsFilePath)}`);
|
|
270
279
|
}
|
|
271
280
|
|
|
272
281
|
export default {
|
package/src/login-success.html
DELETED
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
<html>
|
|
2
|
-
<head>
|
|
3
|
-
<meta charset="utf-8">
|
|
4
|
-
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
5
|
-
<title>Login Successful</title>
|
|
6
|
-
<style>
|
|
7
|
-
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
8
|
-
body {
|
|
9
|
-
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
|
10
|
-
min-height: 100vh;
|
|
11
|
-
display: flex;
|
|
12
|
-
align-items: center;
|
|
13
|
-
justify-content: center;
|
|
14
|
-
background: #f5f7fa;
|
|
15
|
-
color: #333;
|
|
16
|
-
}
|
|
17
|
-
.card {
|
|
18
|
-
text-align: center;
|
|
19
|
-
background: #fff;
|
|
20
|
-
border-radius: 12px;
|
|
21
|
-
padding: 48px 40px;
|
|
22
|
-
box-shadow: 0 2px 12px rgba(0,0,0,0.08);
|
|
23
|
-
max-width: 420px;
|
|
24
|
-
}
|
|
25
|
-
.icon {
|
|
26
|
-
width: 64px; height: 64px;
|
|
27
|
-
margin: 0 auto 24px;
|
|
28
|
-
background: #e8f5e9;
|
|
29
|
-
border-radius: 50%;
|
|
30
|
-
display: flex;
|
|
31
|
-
align-items: center;
|
|
32
|
-
justify-content: center;
|
|
33
|
-
}
|
|
34
|
-
.icon svg { width: 32px; height: 32px; color: #43a047; }
|
|
35
|
-
h1 { font-size: 22px; font-weight: 600; margin-bottom: 8px; }
|
|
36
|
-
p { font-size: 15px; color: #666; line-height: 1.5; }
|
|
37
|
-
</style>
|
|
38
|
-
</head>
|
|
39
|
-
<body>
|
|
40
|
-
<div class="card">
|
|
41
|
-
<div class="icon">
|
|
42
|
-
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
|
43
|
-
<polyline points="20 6 9 17 4 12"/>
|
|
44
|
-
</svg>
|
|
45
|
-
</div>
|
|
46
|
-
<h1>Authentication Successful</h1>
|
|
47
|
-
<p>You can close this tab and return to your command line.</p>
|
|
48
|
-
</div>
|
|
49
|
-
</body>
|
|
50
|
-
</html>
|