cloudron 7.1.2 → 8.0.1

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 CHANGED
@@ -91,7 +91,7 @@ const buildCommand = program.command('build').description('Build using the build
91
91
  .option('--build-service-url <url>', 'Build service URL')
92
92
  .option('--build-service-token <token>', 'Build service token');
93
93
 
94
- buildCommand.command('build', { isDefault: true })
94
+ const buildBuildCommand = buildCommand.command('build', { isDefault: true })
95
95
  .description('Build an app')
96
96
  .option('--build-arg <namevalue>', 'Build arg passed to docker. Can be used multiple times', collectBuildArgs, [])
97
97
  .option('-f, --file <dockerfile>', 'Name of the Dockerfile')
@@ -137,6 +137,11 @@ buildCommand.command('status')
137
137
  .option('--id <buildid>', 'Build ID')
138
138
  .action(buildActions.status);
139
139
 
140
+ buildCommand.addHelpText('after', (ctx) => {
141
+ const header = '\nDefault subcommand `build` (same as `cloudron build build`; used when no subcommand is given):\n\n';
142
+ return header + buildBuildCommand.helpInformation({ error: ctx.error });
143
+ });
144
+
140
145
  backupCommand.command('create')
141
146
  .description('Create new app backup')
142
147
  .option('--site <siteid>', 'App id')
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cloudron",
3
- "version": "7.1.2",
3
+ "version": "8.0.1",
4
4
  "license": "MIT",
5
5
  "description": "Cloudron Commandline Tool",
6
6
  "type": "module",
package/src/actions.js CHANGED
@@ -347,6 +347,7 @@ async function login(adminFqdn, localOptions, cmd) {
347
347
 
348
348
  if (semver.gte(response.body.version, '9.1.0')) {
349
349
  token = await performOidcLogin(adminFqdn, { rejectUnauthorized });
350
+ if (!token) process.exit(1);
350
351
  } else {
351
352
  const username = options.username || await readline.question('Username: ', {});
352
353
  const password = options.password || await readline.question('Password: ', { noEchoBack: true });
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,62 @@ 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 { authorization_endpoint, token_endpoint } = discoveryResponse.body;
84
-
85
- // Generate PKCE code_verifier and code_challenge (S256)
86
- const codeVerifier = crypto.randomBytes(32).toString('base64url');
87
- const codeChallenge = crypto.createHash('sha256').update(codeVerifier).digest('base64url');
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(tokenBody);
174
- if (!rejectUnauthorized) tokenRequest.disableTLSCerts();
175
- const tokenResponse = await tokenRequest;
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 confirm 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
+ console.log('Login failed. Try again.');
123
+ return null;
124
+ }
176
125
 
177
- return tokenResponse.body.access_token;
126
+ console.log('Login timed out. Try again.');
127
+ return null;
178
128
  }
179
129
 
180
130
  export {
@@ -1,61 +0,0 @@
1
- <html>
2
- <head>
3
- <meta charset="utf-8">
4
- <meta name="viewport" content="width=device-width, initial-scale=1">
5
- <title>Authentication successful</title>
6
- <style>
7
- * { margin: 0; padding: 0; box-sizing: border-box; }
8
- body {
9
- font-family: Inter, -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: #f3f4f6;
15
- color: #333;
16
- }
17
- .card {
18
- text-align: center;
19
- background: #fff;
20
- border-radius: 4px;
21
- padding: 48px 40px;
22
- box-shadow: 0 2px 5px rgba(0,0,0,.1);
23
- }
24
- .icon {
25
- width: 64px; height: 64px;
26
- margin: 0 auto 24px;
27
- background: #1a76bf21;
28
- border-radius: 50%;
29
- display: flex;
30
- align-items: center;
31
- justify-content: center;
32
- }
33
- .icon svg { width: 32px; height: 32px; color: #1a76bf; }
34
- h1 { font-size: 22px; font-weight: 600; margin-bottom: 8px; }
35
- p { font-size: 15px; color: #666; line-height: 1.5; }
36
- @media (prefers-color-scheme: dark) {
37
- body {
38
- background-color: black;
39
- color: white;
40
- }
41
- .card {
42
- background: #15181f;
43
- }
44
- p {
45
- color: #aaa;
46
- }
47
- }
48
- </style>
49
- </head>
50
- <body>
51
- <div class="card">
52
- <div class="icon">
53
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
54
- <polyline points="20 6 9 17 4 12"/>
55
- </svg>
56
- </div>
57
- <h1>Authentication successful</h1>
58
- <p>You can close this tab and return to your command line.</p>
59
- </div>
60
- </body>
61
- </html>