mirror of
https://github.com/balena-io/balena-cli.git
synced 2025-04-16 15:28:54 +00:00
Merge pull request #1829 from balena-io/1404-1710-web-login
Fix 'balena login' web authorization hanging with Google Chrome
This commit is contained in:
commit
e50d92727e
@ -58,12 +58,10 @@ async function checkBuildTimestamps() {
|
||||
|
||||
This probably means that \`npm run build\` or \`npm test\` have not been executed,
|
||||
and this error can be fixed by doing so. Running \`npm run build\` or \`npm test\`
|
||||
before commiting is currently a requirement (documented in the CONTRIBUTING.md
|
||||
file) for three reasons:
|
||||
1. To update the CLI markdown documentation (in case any command-line options
|
||||
were updated, added or removed).
|
||||
2. To catch Typescript type check errors sooner and reduce overall waiting time,
|
||||
given that balena-cli CI builds/tests are currently rather lengthy.
|
||||
before commiting is required in order to update the CLI markdown documentation
|
||||
(in case any command-line options were updated, added or removed) and also to
|
||||
catch Typescript type check errors sooner and reduce overall waiting time, given
|
||||
that the CI build/tests are currently rather lengthy.
|
||||
|
||||
If you need/wish to bypass this check without running \`npm run build\`, run:
|
||||
npx touch -am "${docFile}"
|
||||
|
@ -1712,10 +1712,11 @@ how frequently (in minutes) to poll for application updates
|
||||
## preload <image>
|
||||
|
||||
Preload a balena application release (app images/containers), and optionally
|
||||
a balenaOS splash screen, in a previously downloaded balenaOS image file (or
|
||||
Edison zip archive) in the local disk. The balenaOS image file can then be
|
||||
flashed to a device's SD card. When the device boots, it will not need to
|
||||
download the application, as it was preloaded.
|
||||
a balenaOS splash screen, in a previously downloaded '.img' balenaOS image file
|
||||
in the local disk (a zip file is only accepted for the Intel Edison device type).
|
||||
After preloading, the balenaOS image file can be flashed to a device's SD card.
|
||||
When the device boots, it will not need to download the application, as it was
|
||||
preloaded.
|
||||
|
||||
Warning: "balena preload" requires Docker to be correctly installed in
|
||||
your shell environment. For more information (including Windows support)
|
||||
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
Copyright 2016-2017 Balena
|
||||
Copyright 2016-2020 Balena
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@ -110,7 +110,6 @@ Examples:
|
||||
} else if (loginOptions.credentials) {
|
||||
return patterns.authenticate(loginOptions);
|
||||
} else if (loginOptions.web) {
|
||||
console.info('Connecting to the web dashboard');
|
||||
const auth = await import('../auth');
|
||||
await auth.login();
|
||||
return;
|
||||
@ -143,6 +142,11 @@ Find out about the available commands by running:
|
||||
$ balena help
|
||||
|
||||
${messages.reachingOut}`);
|
||||
|
||||
if (options.web) {
|
||||
const { shutdownServer } = await import('../auth');
|
||||
shutdownServer();
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
|
@ -239,10 +239,11 @@ export const preload = {
|
||||
description: 'preload an app on a disk image (or Edison zip archive)',
|
||||
help: `\
|
||||
Preload a balena application release (app images/containers), and optionally
|
||||
a balenaOS splash screen, in a previously downloaded balenaOS image file (or
|
||||
Edison zip archive) in the local disk. The balenaOS image file can then be
|
||||
flashed to a device's SD card. When the device boots, it will not need to
|
||||
download the application, as it was preloaded.
|
||||
a balenaOS splash screen, in a previously downloaded '.img' balenaOS image file
|
||||
in the local disk (a zip file is only accepted for the Intel Edison device type).
|
||||
After preloading, the balenaOS image file can be flashed to a device's SD card.
|
||||
When the device boots, it will not need to download the application, as it was
|
||||
preloaded.
|
||||
|
||||
Warning: "balena preload" requires Docker to be correctly installed in
|
||||
your shell environment. For more information (including Windows support)
|
||||
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
Copyright 2016 Balena
|
||||
Copyright 2016-2020 Balena
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@ -15,6 +15,9 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import { getBalenaSdk } from '../utils/lazy';
|
||||
import { awaitForToken, shutdownServer } from './server';
|
||||
|
||||
export { shutdownServer };
|
||||
|
||||
/**
|
||||
* @module auth
|
||||
@ -52,6 +55,7 @@ export const login = async () => {
|
||||
// from mixed content warnings (as the target of a form in the result page)
|
||||
const callbackUrl = `http://127.0.0.1:${options.port}${options.path}`;
|
||||
const loginUrl = await utils.getDashboardLoginURL(callbackUrl);
|
||||
console.info(`Opening web browser for URL:\n${loginUrl}`);
|
||||
// Leave a bit of time for the
|
||||
// server to get up and runing
|
||||
setTimeout(async () => {
|
||||
@ -59,7 +63,6 @@ export const login = async () => {
|
||||
open(loginUrl, { wait: false });
|
||||
}, 1000);
|
||||
|
||||
const server = await import('./server');
|
||||
const balena = getBalenaSdk();
|
||||
return server.awaitForToken(options).tap(balena.auth.loginWithToken);
|
||||
return awaitForToken(options).tap(balena.auth.loginWithToken);
|
||||
};
|
||||
|
@ -12,7 +12,8 @@
|
||||
<div class="center">
|
||||
<img class="icon" src="./static/images/sad.png" inline>
|
||||
<h1>Something went wrong</h1>
|
||||
<p>You couldn't login to the balena CLI for some reason</p>
|
||||
<br>
|
||||
<p>The balena CLI login was not successful.</p>
|
||||
<br>
|
||||
<br>
|
||||
<a href="https://forums.balena.io/" class="button danger">Get help in our forums</a>
|
||||
|
@ -12,10 +12,9 @@
|
||||
<div class="center">
|
||||
<img class="icon" src="./static/images/happy.png" inline>
|
||||
<h1>Success!</h1>
|
||||
<p>You successfully logged in the balena CLI</p>
|
||||
<br>
|
||||
<br>
|
||||
<a href="<%= dashboardUrl %>" class="button normal">Go to the dashboard</a>
|
||||
<p>The balena CLI login was successful.</p>
|
||||
<p>You may now close this page and return to the command prompt.</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
Copyright 2016 Balena
|
||||
Copyright 2016-2020 Balena
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@ -17,10 +17,13 @@ limitations under the License.
|
||||
import * as Promise from 'bluebird';
|
||||
import * as bodyParser from 'body-parser';
|
||||
import * as express from 'express';
|
||||
import { Socket } from 'net';
|
||||
import * as path from 'path';
|
||||
import { getBalenaSdk } from '../utils/lazy';
|
||||
|
||||
import * as utils from './utils';
|
||||
|
||||
const serverSockets: Socket[] = [];
|
||||
|
||||
const createServer = ({ port }: { port: number }) => {
|
||||
const app = express();
|
||||
app.use(
|
||||
@ -33,10 +36,29 @@ const createServer = ({ port }: { port: number }) => {
|
||||
app.set('views', path.join(__dirname, 'pages'));
|
||||
|
||||
const server = app.listen(port);
|
||||
server.on('connection', socket => serverSockets.push(socket));
|
||||
|
||||
return { app, server };
|
||||
};
|
||||
|
||||
/**
|
||||
* By design (more like a bug, but they won't admit it), a Node.js `http.server`
|
||||
* instance prevents the process from exiting for up to 2 minutes (by default) if a
|
||||
* client keeps a HTTP connection open, and regardless of whether `server.close()`
|
||||
* was called: the `server.close(callback)` callback takes just as long to be called.
|
||||
* Setting `server.timeout` to some value like 3 seconds works, but then the CLI
|
||||
* process hangs for "only" 3 seconds (not good enough). Reducing the timeout to 1
|
||||
* second may cause authentication failure if the laptop or CI server are slow for
|
||||
* any reason. The only reliable way around it seems to be to explicitly unref the
|
||||
* sockets, so the event loop stops waiting for it. See:
|
||||
* https://github.com/nodejs/node/issues/2642
|
||||
* https://github.com/nodejs/node-v0.x-archive/issues/9066
|
||||
*/
|
||||
export function shutdownServer() {
|
||||
serverSockets.forEach(s => s.unref());
|
||||
serverSockets.splice(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Await for token
|
||||
* @function
|
||||
@ -60,45 +82,10 @@ export const awaitForToken = (options: {
|
||||
const { app, server } = createServer({ port: options.port });
|
||||
|
||||
return new Promise<string>((resolve, reject) => {
|
||||
const closeServer = (
|
||||
errorMessage: string | undefined,
|
||||
successPayload?: string,
|
||||
) => {
|
||||
server.close(() => {
|
||||
if (errorMessage) {
|
||||
reject(new Error(errorMessage));
|
||||
return;
|
||||
}
|
||||
|
||||
resolve(successPayload);
|
||||
});
|
||||
};
|
||||
|
||||
const renderAndDone = async ({
|
||||
request,
|
||||
response,
|
||||
viewName,
|
||||
errorMessage,
|
||||
statusCode = 200,
|
||||
token,
|
||||
}: {
|
||||
request: express.Request;
|
||||
response: express.Response;
|
||||
viewName: 'success' | 'error';
|
||||
errorMessage?: string;
|
||||
statusCode?: number;
|
||||
token?: string;
|
||||
}) => {
|
||||
const context = await getContext(viewName);
|
||||
response.status(statusCode).render(viewName, context);
|
||||
request.connection.destroy();
|
||||
closeServer(errorMessage, token);
|
||||
};
|
||||
|
||||
app.post(options.path, async (request, response) => {
|
||||
server.close(); // stop listening for new connections
|
||||
try {
|
||||
const token = request.body.token?.trim();
|
||||
|
||||
if (!token) {
|
||||
throw new Error('No token');
|
||||
}
|
||||
@ -106,31 +93,18 @@ export const awaitForToken = (options: {
|
||||
if (!loggedIn) {
|
||||
throw new Error('Invalid token');
|
||||
}
|
||||
await renderAndDone({ request, response, viewName: 'success', token });
|
||||
response.status(200).render('success');
|
||||
resolve(token);
|
||||
} catch (error) {
|
||||
await renderAndDone({
|
||||
request,
|
||||
response,
|
||||
viewName: 'error',
|
||||
statusCode: 401,
|
||||
errorMessage: error.message,
|
||||
});
|
||||
response.status(401).render('error');
|
||||
reject(new Error(error.message));
|
||||
}
|
||||
});
|
||||
|
||||
app.use((_request, response) => {
|
||||
server.close(); // stop listening for new connections
|
||||
response.status(404).send('Not found');
|
||||
closeServer('Unknown path or verb');
|
||||
reject(new Error('Unknown path or verb'));
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
export const getContext = (viewName: 'success' | 'error') => {
|
||||
if (viewName === 'success') {
|
||||
return Promise.props({
|
||||
dashboardUrl: getBalenaSdk().settings.get('dashboardUrl'),
|
||||
});
|
||||
}
|
||||
|
||||
return Promise.resolve({});
|
||||
};
|
||||
|
@ -1,4 +1,20 @@
|
||||
import * as Promise from 'bluebird';
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2019-2020 Balena Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import * as Bluebird from 'bluebird';
|
||||
import * as chai from 'chai';
|
||||
import chaiAsPromised = require('chai-as-promised');
|
||||
import * as ejs from 'ejs';
|
||||
@ -7,12 +23,7 @@ import * as path from 'path';
|
||||
import * as request from 'request';
|
||||
import * as sinon from 'sinon';
|
||||
|
||||
// TODO: Convert server code to Typescript so it can have a declaration file
|
||||
// @ts-ignore
|
||||
import * as server from '../../build/auth/server';
|
||||
|
||||
// TODO: Convert utils code to Typescript so it can have a declaration file
|
||||
// @ts-ignore
|
||||
import * as utils from '../../build/auth/utils';
|
||||
import tokens from './tokens';
|
||||
|
||||
@ -25,9 +36,7 @@ const options = {
|
||||
path: '/auth',
|
||||
};
|
||||
|
||||
const getPage = function(
|
||||
name: Parameters<typeof server.getContext>[0],
|
||||
): Promise<string> {
|
||||
async function getPage(name: string): Promise<string> {
|
||||
const pagePath = path.join(
|
||||
__dirname,
|
||||
'..',
|
||||
@ -39,8 +48,8 @@ const getPage = function(
|
||||
);
|
||||
const tpl = fs.readFileSync(pagePath, { encoding: 'utf8' });
|
||||
const compiledTpl = ejs.compile(tpl);
|
||||
return server.getContext(name).then((context: any) => compiledTpl(context));
|
||||
};
|
||||
return compiledTpl();
|
||||
}
|
||||
|
||||
describe('Server:', function() {
|
||||
it('should get 404 if posting to an unknown path', function(done) {
|
||||
@ -86,7 +95,7 @@ describe('Server:', function() {
|
||||
describe('given the token authenticates with the server', function() {
|
||||
beforeEach(function() {
|
||||
this.loginIfTokenValidStub = sinon.stub(utils, 'loginIfTokenValid');
|
||||
return this.loginIfTokenValidStub.returns(Promise.resolve(true));
|
||||
return this.loginIfTokenValidStub.returns(Bluebird.resolve(true));
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
@ -119,7 +128,7 @@ describe('Server:', function() {
|
||||
return describe('given the token does not authenticate with the server', function() {
|
||||
beforeEach(function() {
|
||||
this.loginIfTokenValidStub = sinon.stub(utils, 'loginIfTokenValid');
|
||||
return this.loginIfTokenValidStub.returns(Promise.resolve(false));
|
||||
return this.loginIfTokenValidStub.returns(Bluebird.resolve(false));
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
|
Loading…
x
Reference in New Issue
Block a user