feat: 🎸 Add initial setup for client daemon integration (#1987)
✅ Closes: https://hashicorp.atlassian.net/browse/ICU-11702
This commit is contained in:
1 parent
25c6ed6100
commit
b8b970ce4d
12 files changed
+227
-35
No files matched your search
@@ -179,6 +179,10 @@ export default function initializeMockIPC(server, config) {
|
||||
closeWindow() {}
|
||||
toggleFullscreenWindow() {}
|
||||
stop() {}
|
||||
checkCommand() {
|
||||
return faker.system.filePath();
|
||||
}
|
||||
addTokenToClientDaemon() {}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -36,6 +36,15 @@ export default class ApplicationRoute extends Route {
|
||||
const theme = this.session.get('data.theme');
|
||||
this.toggleTheme(theme);
|
||||
await this.clusterUrl.updateClusterUrl();
|
||||
|
||||
// Add token to client daemon after a successful authentication restoration
|
||||
if (this.session.isAuthenticated) {
|
||||
const sessionData = this.session.data?.authenticated;
|
||||
this.ipc.invoke('addTokenToClientDaemon', {
|
||||
tokenId: sessionData?.id,
|
||||
token: sessionData?.token,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import { inject as service } from '@ember/service';
|
||||
import BaseSessionService from 'ember-simple-auth/services/session';
|
||||
|
||||
export default class SessionService extends BaseSessionService {
|
||||
@service ipc;
|
||||
|
||||
/**
|
||||
* Extend ember simple auth's handleAuthentication method
|
||||
* so we can hook in and add the user's token to the client daemon
|
||||
*/
|
||||
handleAuthentication() {
|
||||
super.handleAuthentication(...arguments);
|
||||
|
||||
if (this.session.isAuthenticated) {
|
||||
const sessionData = this.data?.authenticated;
|
||||
this.ipc.invoke('addTokenToClientDaemon', {
|
||||
tokenId: sessionData?.id,
|
||||
token: sessionData?.token,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: MPL-2.0
|
||||
*/
|
||||
|
||||
const { net } = require('electron');
|
||||
|
||||
const requestTimeoutSeconds = 10;
|
||||
// Simple promise wrapper around Electron's net.request feature.
|
||||
// https://www.electronjs.org/docs/api/client-request
|
||||
const netRequestPromise = (url) =>
|
||||
new Promise((resolve, reject) => {
|
||||
try {
|
||||
const request = net.request(url);
|
||||
// We don't worry about canceling the request timeout timer once a
|
||||
// request ends because: request.abort() has no effect after a request
|
||||
// ends and the promise will already have been completed, so a rejection
|
||||
// has no effect either.
|
||||
const requestTimeout = setTimeout(() => {
|
||||
request.abort();
|
||||
reject(new Error('Request timeout'));
|
||||
}, requestTimeoutSeconds * 1000);
|
||||
request.on('error', reject);
|
||||
request.on('response', resolve);
|
||||
request.end();
|
||||
} catch (e) {
|
||||
reject(e);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = netRequestPromise;
|
||||
@@ -0,0 +1,69 @@
|
||||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: MPL-2.0
|
||||
*/
|
||||
|
||||
const { net } = require('electron');
|
||||
const http = require('http');
|
||||
|
||||
const requestTimeoutSeconds = 10;
|
||||
// Simple promise wrapper around Electron's net.request feature.
|
||||
// https://www.electronjs.org/docs/api/client-request
|
||||
const netRequest = (url) =>
|
||||
new Promise((resolve, reject) => {
|
||||
try {
|
||||
const request = net.request(url);
|
||||
// We don't worry about canceling the request timeout timer once a
|
||||
// request ends because: request.abort() has no effect after a request
|
||||
// ends and the promise will already have been completed, so a rejection
|
||||
// has no effect either.
|
||||
const requestTimeout = setTimeout(() => {
|
||||
request.abort();
|
||||
reject(new Error('Request timeout'));
|
||||
}, requestTimeoutSeconds * 1000);
|
||||
request.on('error', reject);
|
||||
request.on('response', resolve);
|
||||
request.end();
|
||||
} catch (e) {
|
||||
reject(e);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Promise wrapper around node's http request function. The primary
|
||||
* use case is to pass in a socket path to be able to make http
|
||||
* requests to the client daemon.
|
||||
* @param options
|
||||
* @param reqBody
|
||||
* @returns {Promise}
|
||||
*/
|
||||
const unixSocketRequest = (options, reqBody) =>
|
||||
new Promise((resolve, reject) => {
|
||||
const req = http.request(options);
|
||||
req.once('error', reject);
|
||||
req.once('response', async (res) => {
|
||||
const bufs = [];
|
||||
for await (const buf of res) {
|
||||
bufs.push(buf);
|
||||
}
|
||||
|
||||
try {
|
||||
let parsedResponse;
|
||||
const response = Buffer.concat(bufs);
|
||||
if (response.length > 0) {
|
||||
parsedResponse = JSON.parse(response);
|
||||
}
|
||||
|
||||
resolve(parsedResponse);
|
||||
} catch (e) {
|
||||
reject(e);
|
||||
}
|
||||
});
|
||||
|
||||
if (reqBody) {
|
||||
req.write(JSON.stringify(reqBody));
|
||||
}
|
||||
req.end();
|
||||
});
|
||||
|
||||
module.exports = { netRequest, unixSocketRequest };
|
||||
@@ -78,7 +78,25 @@ module.exports = {
|
||||
*/
|
||||
spawnSync(command) {
|
||||
const childProcess = spawnSync(path(), command);
|
||||
const rawOutput = childProcess.output.toString();
|
||||
const rawOutput = childProcess.stdout.toString();
|
||||
return rawOutput;
|
||||
},
|
||||
|
||||
/**
|
||||
* Spawn a child process and return a promise.
|
||||
* Resolves on any output from stdout or stderr.
|
||||
* @param command
|
||||
*/
|
||||
spawn(command) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const childProcess = spawn(path(), command);
|
||||
childProcess.stdout.on('data', (data) => {
|
||||
resolve({ childProcess, stdout: data.toString() });
|
||||
});
|
||||
childProcess.stderr.on('data', (data) => {
|
||||
resolve({ childProcess, stderr: data.toString() });
|
||||
});
|
||||
childProcess.on('error', (err) => reject(e));
|
||||
});
|
||||
},
|
||||
};
|
||||
@@ -22,9 +22,10 @@ const {
|
||||
} = require('electron');
|
||||
require('./ipc/handlers.js');
|
||||
|
||||
const runtimeSettings = require('./services/runtime-settings.js');
|
||||
const { generateCSPHeader } = require('./config/content-security-policy.js');
|
||||
const runtimeSettings = require('./services/runtime-settings.js');
|
||||
const sessionManager = require('./services/session-manager.js');
|
||||
const clientDaemonManager = require('./services/client-daemon-manager');
|
||||
|
||||
const menu = require('./config/menu.js');
|
||||
const appUpdater = require('./helpers/app-updater.js');
|
||||
@@ -233,6 +234,8 @@ app.on('ready', async () => {
|
||||
mainWindow = createWindow(partition, closeWindowCB);
|
||||
}
|
||||
});
|
||||
|
||||
await clientDaemonManager.start();
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -251,6 +254,12 @@ app.on('before-quit', (event) => {
|
||||
}
|
||||
});
|
||||
|
||||
app.on('quit', () => {
|
||||
// TODO: Should we check if the desktop client started the daemon?
|
||||
// If not, we probably shouldn't stop the daemon
|
||||
clientDaemonManager.stop();
|
||||
});
|
||||
|
||||
// Handle an unhandled error in the main thread
|
||||
//
|
||||
// Note that 'uncaughtException' is a crude mechanism for exception handling intended to
|
||||
|
||||
@@ -14,6 +14,8 @@ const { isLinux, isMac, isWindows } = require('../helpers/platform.js');
|
||||
const os = require('node:os');
|
||||
const pty = require('node-pty');
|
||||
const which = require('which');
|
||||
const { unixSocketRequest } = require('../helpers/request-promise');
|
||||
const clientDaemonManager = require('../services/client-daemon-manager');
|
||||
|
||||
/**
|
||||
* Returns the current runtime clusterUrl, which is used by the main thread to
|
||||
@@ -117,6 +119,10 @@ handle('closeWindow', () => app.quit());
|
||||
*/
|
||||
handle('checkCommand', async (command) => which(command, { nothrow: true }));
|
||||
|
||||
handle('addTokenToClientDaemon', async (data) =>
|
||||
clientDaemonManager.addToken(data)
|
||||
);
|
||||
|
||||
/**
|
||||
* Return an object containing helper fields for determining what OS we're running on
|
||||
*/
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* SPDX-License-Identifier: MPL-2.0
|
||||
*/
|
||||
|
||||
const netRequestPromise = require('../helpers/net-request-promise.js');
|
||||
const { netRequest } = require('../helpers/request-promise.js');
|
||||
|
||||
// Runtime settings are any configuration, especially user-specified,
|
||||
// set up at runtime. For example, the Boundary clusterUrl is a runtime setting.
|
||||
@@ -36,7 +36,7 @@ class RuntimeSettings {
|
||||
// test to see if the clusterUrl is API-compatible with the desktop client.
|
||||
const scopesEndpoint = `${clusterUrl}/v1/scopes`;
|
||||
try {
|
||||
const result = await netRequestPromise(scopesEndpoint);
|
||||
const result = await netRequest(scopesEndpoint);
|
||||
// Redirects (3xx) are handled in the netRequest Promise, so we only
|
||||
// throw an error for 4xx or 5xx responses.
|
||||
if (result.statusCode >= 400) {
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: MPL-2.0
|
||||
*/
|
||||
|
||||
const { spawnSync, spawn } = require('../helpers/spawn-promise');
|
||||
const jsonify = require('../utils/jsonify.js');
|
||||
const { unixSocketRequest } = require('../helpers/request-promise');
|
||||
const runtimeSettings = require('./runtime-settings');
|
||||
|
||||
class ClientDaemonManager {
|
||||
#socketPath;
|
||||
|
||||
get socketPath() {
|
||||
return this.#socketPath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks the status of the client daemon.
|
||||
* @returns {string}
|
||||
*/
|
||||
status() {
|
||||
const daemonStatusCommand = ['daemon', 'status', '-format=json'];
|
||||
const data = spawnSync(daemonStatusCommand);
|
||||
|
||||
const status = jsonify(data);
|
||||
this.#socketPath = status.socket_address;
|
||||
return status;
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts the daemon and checks the status to store the socket path.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async start() {
|
||||
const startDaemonCommand = ['daemon', 'start'];
|
||||
await spawn(startDaemonCommand);
|
||||
this.status();
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops the daemon.
|
||||
*/
|
||||
stop() {
|
||||
const stopDaemonCommand = ['daemon', 'stop'];
|
||||
spawnSync(stopDaemonCommand);
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes a request to the socket to add the token to the daemon.
|
||||
* If the token already exists, this will update the token in the
|
||||
* client daemon.
|
||||
* @param token
|
||||
* @param tokenId
|
||||
* @returns {Promise}
|
||||
*/
|
||||
addToken({ token, tokenId }) {
|
||||
const postBody = {
|
||||
auth_token_id: tokenId,
|
||||
boundary_addr: runtimeSettings.clusterUrl,
|
||||
auth_token: token,
|
||||
};
|
||||
const request = {
|
||||
method: 'POST',
|
||||
path: 'http://internal.boundary.local/v1/tokens',
|
||||
socketPath: this.#socketPath,
|
||||
};
|
||||
return unixSocketRequest(request, postBody);
|
||||
}
|
||||
}
|
||||
|
||||
// Export an instance so we get a singleton
|
||||
module.exports = new ClientDaemonManager();
|
||||
@@ -24,6 +24,7 @@ class MockIPC {
|
||||
resetClusterUrl() {}
|
||||
hasMacOSChrome() {}
|
||||
showWindowActions() {}
|
||||
addTokenToClientDaemon() {}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
import { module, test } from 'qunit';
|
||||
import { setupTest } from 'desktop/tests/helpers';
|
||||
|
||||
module('Unit | Service | session', function (hooks) {
|
||||
setupTest(hooks);
|
||||
|
||||
// TODO: Replace this with your real tests.
|
||||
test('it exists', function (assert) {
|
||||
let service = this.owner.lookup('service:session');
|
||||
assert.ok(service);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user