mirror of
https://github.com/balena-io/etcher.git
synced 2025-04-22 06:17:20 +00:00

- upgrade pretty_bytes to 6.1.1 - upgrade electron-remote to 2.1.0 - upgrade semver to 7.5.4 + @types/semver to 7.5.6 - upgrade chai to 4.3.11 + @types/chai to 4.3.10 - upgrade mocha to 10.2.0 + @types/mocha to 10.0.6 - upgrade sinon to 17.0.1 + @types/sinon to 17.0.2 - remove useless @types - upgrade @svgr/webpack to 8.1.0 - upgrade @sentry/electron to 4.15.1 - upgrade tslib to 2.6.2 - upgrade immutable to 4.3.4 - upgrade redux to 4.2.1 - upgrade ts-node to 10.9.2 & ts-loader to 9.5.1 - remove mini-css-extract-plugin - upgrade husky to 8.0.3 - upgrade uuid to 9.0.1 - upgrade lint-staged to 15.2.1 - upgrade @types/node to 18.11.9 - upgrade @fortawesome/fontawesome-free to 6.5.1 - upgrade i18next to 23.7.8 & react-i18next to 11.18.6 - bump react, react-dom + related @types to 17.0.2 and rendition to 35.1.0 - fix getuid for ts - fix @types/react being in wrong deps - upgrade @types/tmp to 0.2.6 - upgrade typescript to 5.3.3 - upgrade @types/mime-types to 2.1.4 - remove d3 from deps - upgrade electron-updater to 6.1.7 - upgrade rendition to 35.1.2 - upgrade node-ipc to 9.2.3 - upgrade @types/node-ipc to 9.2.3 - upgrade electron to 27.1.3 - upgrade @electron-forge/* to 7.2.0 - upgrade @reforged/marker-appimage to 3.3.2 - upgrade style-loader to 3.3.3 - upgrade balena-lint to 7.2.4 - run CI with node 18.19 - add xxhash-addon to sidecar assets Change-type: patch
226 lines
6.4 KiB
TypeScript
Executable File
226 lines
6.4 KiB
TypeScript
Executable File
/*
|
|
* Copyright 2017 balena.io
|
|
*
|
|
* 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 childProcess from 'child_process';
|
|
import { withTmpFile } from 'etcher-sdk/build/tmp';
|
|
import { promises as fs } from 'fs';
|
|
import * as _ from 'lodash';
|
|
import * as os from 'os';
|
|
import * as semver from 'semver';
|
|
import * as sudoPrompt from '@balena/sudo-prompt';
|
|
import { promisify } from 'util';
|
|
|
|
import { sudo as catalinaSudo } from './catalina-sudo/sudo';
|
|
import * as errors from './errors';
|
|
|
|
const execAsync = promisify(childProcess.exec);
|
|
const execFileAsync = promisify(childProcess.execFile);
|
|
|
|
type Std = string | Buffer | undefined;
|
|
|
|
function sudoExecAsync(
|
|
cmd: string,
|
|
options: { name: string },
|
|
): Promise<{ stdout: Std; stderr: Std }> {
|
|
return new Promise((resolve, reject) => {
|
|
sudoPrompt.exec(
|
|
cmd,
|
|
options,
|
|
(error: Error | undefined, stdout: Std, stderr: Std) => {
|
|
if (error) {
|
|
reject(error);
|
|
} else {
|
|
resolve({ stdout, stderr });
|
|
}
|
|
},
|
|
);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* @summary The user id of the UNIX "superuser"
|
|
*/
|
|
const UNIX_SUPERUSER_USER_ID = 0;
|
|
|
|
export async function isElevated(): Promise<boolean> {
|
|
if (os.platform() === 'win32') {
|
|
// `fltmc` is available on WinPE, XP, Vista, 7, 8, and 10
|
|
// Works even when the "Server" service is disabled
|
|
// See http://stackoverflow.com/a/28268802
|
|
try {
|
|
await execAsync('fltmc');
|
|
} catch (error: any) {
|
|
if (error.code === os.constants.errno.EPERM) {
|
|
return false;
|
|
}
|
|
throw error;
|
|
}
|
|
return true;
|
|
}
|
|
return process.geteuid!() === UNIX_SUPERUSER_USER_ID;
|
|
}
|
|
|
|
/**
|
|
* @summary Check if the current process is running with elevated permissions
|
|
*/
|
|
export function isElevatedUnixSync(): boolean {
|
|
return process.geteuid!() === UNIX_SUPERUSER_USER_ID;
|
|
}
|
|
|
|
function escapeSh(value: any): string {
|
|
// Make sure it's a string
|
|
// Replace ' -> '\'' (closing quote, escaped quote, opening quote)
|
|
// Surround with quotes
|
|
return `'${String(value).replace(/'/g, "'\\''")}'`;
|
|
}
|
|
|
|
function escapeParamCmd(value: any): string {
|
|
// Make sure it's a string
|
|
// Escape " -> \"
|
|
// Surround with double quotes
|
|
return `"${String(value).replace(/"/g, '\\"')}"`;
|
|
}
|
|
|
|
function setEnvVarSh(value: any, name: string): string {
|
|
return `export ${name}=${escapeSh(value)}`;
|
|
}
|
|
|
|
function setEnvVarCmd(value: any, name: string): string {
|
|
return `set "${name}=${String(value)}"`;
|
|
}
|
|
|
|
// Exported for tests
|
|
export function createLaunchScript(
|
|
command: string,
|
|
argv: string[],
|
|
environment: _.Dictionary<string | undefined>,
|
|
): string {
|
|
const isWindows = os.platform() === 'win32';
|
|
const lines = [];
|
|
if (isWindows) {
|
|
// Switch to utf8
|
|
lines.push('chcp 65001');
|
|
}
|
|
const [setEnvVarFn, escapeFn] = isWindows
|
|
? [setEnvVarCmd, escapeParamCmd]
|
|
: [setEnvVarSh, escapeSh];
|
|
lines.push(..._.map(environment, setEnvVarFn));
|
|
lines.push([command, ...argv].map(escapeFn).join(' '));
|
|
return lines.join(os.EOL);
|
|
}
|
|
|
|
async function elevateScriptWindows(
|
|
path: string,
|
|
name: string,
|
|
): Promise<{ cancelled: false }> {
|
|
// '&' needs to be escaped here (but not when written to a .cmd file)
|
|
const cmd = ['cmd', '/c', escapeParamCmd(path).replace(/&/g, '^&')].join(' ');
|
|
await sudoExecAsync(cmd, { name });
|
|
return { cancelled: false };
|
|
}
|
|
|
|
async function elevateScriptUnix(
|
|
path: string,
|
|
name: string,
|
|
): Promise<{ cancelled: boolean }> {
|
|
const cmd = ['bash', escapeSh(path)].join(' ');
|
|
await sudoExecAsync(cmd, { name });
|
|
return { cancelled: false };
|
|
}
|
|
|
|
async function elevateScriptCatalina(
|
|
path: string,
|
|
): Promise<{ cancelled: boolean }> {
|
|
const cmd = ['bash', escapeSh(path)].join(' ');
|
|
try {
|
|
const { cancelled } = await catalinaSudo(cmd);
|
|
return { cancelled };
|
|
} catch (error: any) {
|
|
throw errors.createError({ title: error.stderr });
|
|
}
|
|
}
|
|
|
|
export async function elevateCommand(
|
|
command: string[],
|
|
options: {
|
|
environment: _.Dictionary<string | undefined>;
|
|
applicationName: string;
|
|
},
|
|
): Promise<{ cancelled: boolean }> {
|
|
if (await isElevated()) {
|
|
await execFileAsync(command[0], command.slice(1), {
|
|
env: options.environment,
|
|
});
|
|
return { cancelled: false };
|
|
}
|
|
const isWindows = os.platform() === 'win32';
|
|
const launchScript = createLaunchScript(
|
|
command[0],
|
|
command.slice(1),
|
|
options.environment,
|
|
);
|
|
return await withTmpFile(
|
|
{
|
|
keepOpen: false,
|
|
prefix: 'balena-etcher-electron-',
|
|
postfix: '.cmd',
|
|
},
|
|
async ({ path }) => {
|
|
await fs.writeFile(path, launchScript);
|
|
if (isWindows) {
|
|
return elevateScriptWindows(path, options.applicationName);
|
|
}
|
|
if (
|
|
os.platform() === 'darwin' &&
|
|
semver.compare(os.release(), '19.0.0') >= 0
|
|
) {
|
|
// >= macOS Catalina
|
|
return elevateScriptCatalina(path);
|
|
}
|
|
try {
|
|
return await elevateScriptUnix(path, options.applicationName);
|
|
} catch (error: any) {
|
|
// We're hardcoding internal error messages declared by `sudo-prompt`.
|
|
// There doesn't seem to be a better way to handle these errors, so
|
|
// for now, we should make sure we double check if the error messages
|
|
// have changed every time we upgrade `sudo-prompt`.
|
|
console.log('error', error);
|
|
if (_.includes(error.message, 'is not in the sudoers file')) {
|
|
throw errors.createUserError({
|
|
title: "Your user doesn't have enough privileges to proceed",
|
|
description:
|
|
'This application requires sudo privileges to be able to write to drives',
|
|
});
|
|
} else if (_.startsWith(error.message, 'Command failed:')) {
|
|
throw errors.createUserError({
|
|
title: 'The elevated process died unexpectedly',
|
|
description: `The process error code was ${error.code}`,
|
|
});
|
|
} else if (error.message === 'User did not grant permission.') {
|
|
return { cancelled: true };
|
|
} else if (error.message === 'No polkit authentication agent found.') {
|
|
throw errors.createUserError({
|
|
title: 'No polkit authentication agent found',
|
|
description:
|
|
'Please install a polkit authentication agent for your desktop environment of choice to continue',
|
|
});
|
|
}
|
|
throw error;
|
|
}
|
|
},
|
|
);
|
|
}
|