diff --git a/.github/actions/spelling/excludes.txt b/.github/actions/spelling/excludes.txt index 14615df8fc2..2fcd2d584ef 100644 --- a/.github/actions/spelling/excludes.txt +++ b/.github/actions/spelling/excludes.txt @@ -89,6 +89,7 @@ ^\QSECURITY.md\E$ ^pkg/rancher-desktop/assets/scripts/logrotate-k3s$ ^pkg/rancher-desktop/assets/scripts/logrotate-lima-guestagent$ +^pkg/rancher-desktop/sudo-prompt/ ignore$ /translations/(?!en) ^\Qpkg/rancher-desktop/router.js\E$ diff --git a/jest.config.js b/jest.config.js index c68f3d8bc61..0830014bb3e 100644 --- a/jest.config.js +++ b/jest.config.js @@ -30,5 +30,9 @@ module.exports = { setupFiles: [ '/pkg/rancher-desktop/utils/testUtils/setupElectron.ts', ], - testEnvironment: 'jsdom', + testEnvironment: 'jsdom', + testPathIgnorePatterns: [ + '/node_modules/', + '/pkg/rancher-desktop/sudo-prompt/', + ], }; diff --git a/package.json b/package.json index 38ca834efbb..44122475c2f 100644 --- a/package.json +++ b/package.json @@ -76,7 +76,6 @@ "proxy-agent": "^6.4.0", "rancher-icons": "rancher/icons#v2.0.21", "semver": "7.6.3", - "sudo-prompt": "9.2.1", "tar-stream": "3.1.7", "ufo": "1.5.4", "unfetch": "4.2.0", diff --git a/pkg/rancher-desktop/backend/lima.ts b/pkg/rancher-desktop/backend/lima.ts index ed07c94ea20..f7c6aca16e1 100644 --- a/pkg/rancher-desktop/backend/lima.ts +++ b/pkg/rancher-desktop/backend/lima.ts @@ -15,7 +15,6 @@ import merge from 'lodash/merge'; import omit from 'lodash/omit'; import zip from 'lodash/zip'; import semver from 'semver'; -import sudo from 'sudo-prompt'; import tar from 'tar-stream'; import yaml from 'yaml'; @@ -41,6 +40,7 @@ import NGINX_CONF from '@pkg/assets/scripts/nginx.conf'; import { ContainerEngine, MountType, VMType } from '@pkg/config/settings'; import { getServerCredentialsPath, ServerState } from '@pkg/main/credentialServer/httpCredentialHelperServer'; import mainEvents from '@pkg/main/mainEvents'; +import { exec as sudo } from '@pkg/sudo-prompt'; import * as childProcess from '@pkg/utils/childProcess'; import clone from '@pkg/utils/clone'; import DockerDirManager from '@pkg/utils/dockerDirManager'; @@ -1383,7 +1383,7 @@ export default class LimaBackend extends events.EventEmitter implements VMBacken await new Promise((resolve, reject) => { const iconPath = path.join(paths.resources, 'icons', 'logo-square-512.png'); - sudo.exec(command, { name: 'Rancher Desktop', icns: iconPath }, (error, stdout, stderr) => { + sudo(command, { name: 'Rancher Desktop', icns: iconPath }, (error, stdout, stderr) => { if (stdout) { console.log(`Prompt for sudo: stdout: ${ stdout }`); } diff --git a/pkg/rancher-desktop/sudo-prompt/CHANGELOG.md b/pkg/rancher-desktop/sudo-prompt/CHANGELOG.md new file mode 100644 index 00000000000..46ba454c45e --- /dev/null +++ b/pkg/rancher-desktop/sudo-prompt/CHANGELOG.md @@ -0,0 +1,143 @@ +## [9.2.0] 2020-04-29 + +### Fixed + +- Update TypeScript types to accommodate recent changes, see +[#117](https://github.com/jorangreef/sudo-prompt/issues/117). + +## [9.1.0] 2019-11-13 + +### Added + +- Add TypeScript types. + +## [9.0.0] 2019-06-03 + +### Changed + +- Make cross-platform `stdout`, `stderr` behavior consistent, see +[#89](https://github.com/jorangreef/sudo-prompt/issues/89). + +- Preserve current working directory on all platforms. + +- Improve kdesudo dialog appearance. + +### Added + +- Add `options.env` to set environment variables on all platforms, see +[#91](https://github.com/jorangreef/sudo-prompt/issues/91). + +### Fixed + +- Always return PERMISSION_DENIED as an Error object. + +- Support multiple commands separated by semicolons on Linux, see +[#39](https://github.com/jorangreef/sudo-prompt/issues/39). + +- Distinguish between elevation errors and command errors on Linux, see +[#88](https://github.com/jorangreef/sudo-prompt/issues/88). + +- Fix Windows to return `PERMISSION_DENIED` Error even when Windows' error +messages are internationalized, see +[#96](https://github.com/jorangreef/sudo-prompt/issues/96). + +## [8.2.5] 2018-12-12 + +### Fixed + +- Whitelist package.json files. + +## [8.2.4] 2018-12-12 + +### Added + +- A CHANGELOG.md file, see +[#78](https://github.com/jorangreef/sudo-prompt/issues/78). + +## [8.2.3] 2018-09-11 + +### Fixed + +- README: Link to concurrency discussion. + +## [8.2.2] 2018-09-11 + +### Fixed + +- README: Details on concurrency. + +## [8.2.1] 2018-09-11 + +### Fixed + +- A rare idempotency edge case where a command might have been run more than +once, given a very specific OS environment setup. + +## [8.2.0] 2018-03-22 + +### Added + +- Windows: Fix `cd` when `cwd` is on another drive, see +[#70](https://github.com/jorangreef/sudo-prompt/issues/70). + +## [8.1.0] 2018-01-10 + +### Added + +- Linux: Increase `maxBuffer` limit to 128 MiB, see +[#66](https://github.com/jorangreef/sudo-prompt/issues/66). + +## [8.0.0] 2018-11-02 + +### Changed + +- Windows: Set code page of command batch script to UTF-8. + +## [7.1.1] 2017-07-18 + +### Fixed + +- README: Explicitly mention that no child process is returned. + +## [7.0.0] 2017-03-15 + +### Changed + +- Add status code to errors on Windows and macOS. + +## [6.2.1] 2016-12-16 + +### Fixed + +- README: Syntax highlighting. + +## [6.2.0] 2016-08-17 + +### Fixed + +- README: Rename OS X to macOS. + +## [6.1.0] 2016-08-02 + +### Added + +- Yield an error if no polkit authentication agent is found, see +[#29](https://github.com/jorangreef/sudo-prompt/issues/29). + +## [6.0.2] 2016-07-21 + +### Fixed + +- README: Update explanation of Linux behavior. + +## [6.0.1] 2016-07-15 + +### Fixed + +- Update keywords in package.json. + +## [6.0.0] 2016-07-15 + +### Changed + +- Add support for Windows. diff --git a/pkg/rancher-desktop/sudo-prompt/LICENSE b/pkg/rancher-desktop/sudo-prompt/LICENSE new file mode 100644 index 00000000000..e3b751f2841 --- /dev/null +++ b/pkg/rancher-desktop/sudo-prompt/LICENSE @@ -0,0 +1,22 @@ +The MIT License (MIT) + +Copyright (c) 2015 Joran Dirk Greef + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + diff --git a/pkg/rancher-desktop/sudo-prompt/README.md b/pkg/rancher-desktop/sudo-prompt/README.md new file mode 100644 index 00000000000..2ae7536ebb8 --- /dev/null +++ b/pkg/rancher-desktop/sudo-prompt/README.md @@ -0,0 +1,62 @@ +# sudo-prompt + +Run a non-graphical terminal command using `sudo`, prompting the user with a graphical OS dialog if necessary. Useful for background Node.js applications or native Electron apps that need `sudo`. + +## Cross-Platform +`sudo-prompt` provides a native OS dialog prompt on **macOS**, **Linux** and **Windows**. + +![macOS](https://raw.githubusercontent.com/jorangreef/sudo-prompt/master/macos.png) + +![Linux](https://raw.githubusercontent.com/jorangreef/sudo-prompt/master/linux.png) + +![Windows](https://raw.githubusercontent.com/jorangreef/sudo-prompt/master/windows.png) + +## Installation +`sudo-prompt` has no external dependencies and does not require any native bindings. +``` +npm install sudo-prompt +``` + +## Usage +Note: Your command should not start with the `sudo` prefix. +```javascript +var sudo = require('sudo-prompt'); +var options = { + name: 'Electron', + icns: '/Applications/Electron.app/Contents/Resources/Electron.icns', // (optional) +}; +sudo.exec('echo hello', options, + function(error, stdout, stderr) { + if (error) throw error; + console.log('stdout: ' + stdout); + } +); +``` + +`sudo-prompt` will use `process.title` as `options.name` if `options.name` is not provided. `options.name` must be alphanumeric only (spaces are supported) and at most 70 characters. + +`sudo-prompt` will preserve the current working directory on all platforms. Environment variables can be set explicitly using `options.env`. + +**`sudo-prompt.exec()` is different to `child-process.exec()` in that no child process is returned (due to platform and permissions constraints).** + +## Behavior +On macOS, `sudo-prompt` should behave just like the `sudo` command in the shell. If your command does not work with the `sudo` command in the shell (perhaps because it uses `>` redirection to a restricted file), then it may not work with `sudo-prompt`. However, it is still possible to use sudo-prompt to get a privileged shell, [see this closed issue for more information](https://github.com/jorangreef/sudo-prompt/issues/1). + +On Linux, `sudo-prompt` will use either `pkexec` or `kdesudo` to show the password prompt and run your command. Where possible, `sudo-prompt` will try and get these to mimic `sudo`. Depending on which binary is used, and due to the limitations of some binaries, the name of your program or the command itself may be displayed to your user. `sudo-prompt` will not use `gksudo` since `gksudo` does not support concurrent prompts. Passing `options.icns` is currently not supported by `sudo-prompt` on Linux. Patches are welcome to add support for icons based on `polkit`. + +On Windows, `sudo-prompt` will elevate your command using User Account Control (UAC). Passing `options.name` or `options.icns` is currently not supported by `sudo-prompt` on Windows. + +## Non-graphical terminal commands only +Just as you should never use `sudo` to launch any graphical applications, you should never use `sudo-prompt` to launch any graphical applications. Doing so could cause files in your home directory to become owned by root. `sudo-prompt` is explicitly designed to launch non-graphical terminal commands. For more information, [read this post](http://www.psychocats.net/ubuntu/graphicalsudo). + +## Concurrency +On systems where the user has opted to have `tty-tickets` enabled (most systems), each call to `exec()` will result in a separate password prompt. Where `tty-tickets` are disabled, subsequent calls to `exec()` will still require a password prompt, even where the user's `sudo` timestamp file remains valid, due to edge cases with `sudo` itself, [see this discussion for more information](https://github.com/jorangreef/sudo-prompt/pull/76). + +You should never rely on `sudo-prompt` to execute your calls in order. If you need to enforce ordering of calls, then you should explicitly order your calls in your application. Where your commands are short-lived, you should always queue your calls to `exec()` to make sure your user is not overloaded with password prompts. + +## Invalidating the timestamp +On macOS and Linux, you can invalidate the user's `sudo` timestamp file to force the prompt to appear by running the following command in your terminal: + +```sh +$ sudo -k +``` diff --git a/pkg/rancher-desktop/sudo-prompt/index.d.ts b/pkg/rancher-desktop/sudo-prompt/index.d.ts new file mode 100644 index 00000000000..329c12217c7 --- /dev/null +++ b/pkg/rancher-desktop/sudo-prompt/index.d.ts @@ -0,0 +1,4 @@ +export function exec(cmd: string, + options?: ((error?: Error, stdout?: string | Buffer, stderr?: string | Buffer) => void) + | { name?: string, icns?: string, env?: { [key: string]: string } }, + callback?: (error?: Error, stdout?: string | Buffer, stderr?: string | Buffer) => void): void; diff --git a/pkg/rancher-desktop/sudo-prompt/index.js b/pkg/rancher-desktop/sudo-prompt/index.js new file mode 100644 index 00000000000..4c4555b480f --- /dev/null +++ b/pkg/rancher-desktop/sudo-prompt/index.js @@ -0,0 +1,783 @@ +const Node = { + child: require('child_process'), + crypto: require('crypto'), + fs: require('fs'), + os: require('os'), + path: require('path'), + process, + util: require('util'), +}; + +function Attempt(instance, end) { + const platform = Node.process.platform; + + if (platform === 'darwin') { + return Mac(instance, end); + } + if (platform === 'linux') { + return Linux(instance, end); + } + if (platform === 'win32') { + return Windows(instance, end); + } + end(new Error('Platform not yet supported.')); +} + +function EscapeDoubleQuotes(string) { + if (typeof string !== 'string') { + throw new TypeError('Expected a string.'); + } + + return string.replace(/"/g, '\\"'); +} + +function Exec() { + if (arguments.length < 1 || arguments.length > 3) { + throw new Error('Wrong number of arguments.'); + } + const command = arguments[0]; + let options = {}; + let end = function() {}; + + if (typeof command !== 'string') { + throw new TypeError('Command should be a string.'); + } + if (arguments.length === 2) { + if (arguments[1] !== null && typeof arguments[1] === 'object') { + options = arguments[1]; + } else if (typeof arguments[1] === 'function') { + end = arguments[1]; + } else { + throw new TypeError('Expected options or callback.'); + } + } else if (arguments.length === 3) { + if (arguments[1] !== null && typeof arguments[1] === 'object') { + options = arguments[1]; + } else { + throw new TypeError('Expected options to be an object.'); + } + if (typeof arguments[2] === 'function') { + end = arguments[2]; + } else { + throw new TypeError('Expected callback to be a function.'); + } + } + if (/^sudo/i.test(command)) { + return end(new Error('Command should not be prefixed with "sudo".')); + } + if (typeof options.name === 'undefined') { + const title = Node.process.title; + + if (ValidName(title)) { + options.name = title; + } else { + return end(new Error('process.title cannot be used as a valid name.')); + } + } else if (!ValidName(options.name)) { + let error = ''; + + error += 'options.name must be alphanumeric only '; + error += '(spaces are allowed) and <= 70 characters.'; + + return end(new Error(error)); + } + if (typeof options.icns !== 'undefined') { + if (typeof options.icns !== 'string') { + return end(new Error('options.icns must be a string if provided.')); + } else if (options.icns.trim().length === 0) { + return end(new Error('options.icns must not be empty if provided.')); + } + } + if (typeof options.env !== 'undefined') { + if (typeof options.env !== 'object') { + return end(new Error('options.env must be an object if provided.')); + } else if (Object.keys(options.env).length === 0) { + return end(new Error('options.env must not be empty if provided.')); + } else { + for (const key in options.env) { + const value = options.env[key]; + + if (typeof key !== 'string' || typeof value !== 'string') { + return end( + new Error('options.env environment variables must be strings.'), + ); + } + // "Environment variable names used by the utilities in the Shell and + // Utilities volume of IEEE Std 1003.1-2001 consist solely of uppercase + // letters, digits, and the '_' (underscore) from the characters defined + // in Portable Character Set and do not begin with a digit. Other + // characters may be permitted by an implementation; applications shall + // tolerate the presence of such names." + if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(key)) { + return end( + new Error( + `options.env has an invalid environment variable name: ${ + JSON.stringify(key) }`, + ), + ); + } + if (/[\r\n]/.test(value)) { + return end( + new Error( + `options.env has an invalid environment variable value: ${ + JSON.stringify(value) }`, + ), + ); + } + } + } + } + const platform = Node.process.platform; + + if (platform !== 'darwin' && platform !== 'linux' && platform !== 'win32') { + return end(new Error('Platform not yet supported.')); + } + const instance = { + command, + options, + uuid: undefined, + path: undefined, + }; + + Attempt(instance, end); +} + +function Linux(instance, end) { + LinuxBinary(instance, + (error, binary) => { + if (error) { + return end(error); + } + let command = []; + + // Preserve current working directory: + command.push(`cd "${ EscapeDoubleQuotes(Node.process.cwd()) }";`); + // Export environment variables: + for (const key in instance.options.env) { + const value = instance.options.env[key]; + + command.push(`export ${ key }="${ EscapeDoubleQuotes(value) }";`); + } + command.push(`"${ EscapeDoubleQuotes(binary) }"`); + if (/kdesudo/i.test(binary)) { + command.push( + '--comment', + `"${ instance.options.name } wants to make changes. ` + + `Enter your password to allow this."`, + ); + command.push('-d'); // Do not show the command to be run in the dialog. + command.push('--'); + } else if (/pkexec/i.test(binary)) { + command.push('--disable-internal-agent'); + } + const magic = 'SUDOPROMPT\n'; + + command.push( + `/bin/bash -c "echo ${ EscapeDoubleQuotes(magic.trim()) }; ${ + EscapeDoubleQuotes(instance.command) + }"`, + ); + command = command.join(' '); + Node.child.exec(command, { encoding: 'utf-8', maxBuffer: MAX_BUFFER }, + (error, stdout, stderr) => { + // ISSUE 88: + // We must distinguish between elevation errors and command errors. + // + // KDESUDO: + // kdesudo provides no way to do this. We add a magic marker to know + // if elevation succeeded. Any error thereafter is a command error. + // + // PKEXEC: + // "Upon successful completion, the return value is the return value of + // PROGRAM. If the calling process is not authorized or an + // authorization could not be obtained through authentication or an + // error occured, pkexec exits with a return value of 127. If the + // authorization could not be obtained because the user dismissed the + // authentication dialog, pkexec exits with a return value of 126." + // + // However, we do not rely on pkexec's return of 127 since our magic + // marker is more reliable, and we already use it for kdesudo. + const elevated = stdout && stdout.slice(0, magic.length) === magic; + + if (elevated) { + stdout = stdout.slice(magic.length); + } + // Only normalize the error if it is definitely not a command error: + // In other words, if we know that the command was never elevated. + // We do not inspect error messages beyond NO_POLKIT_AGENT. + // We cannot rely on English errors because of internationalization. + if (error && !elevated) { + if (/No authentication agent found/.test(stderr)) { + error.message = NO_POLKIT_AGENT; + } else { + error.message = PERMISSION_DENIED; + } + } + end(error, stdout, stderr); + }, + ); + }, + ); +} + +function LinuxBinary(_, end) { + let index = 0; + // We used to prefer gksudo over pkexec since it enabled a better prompt. + // However, gksudo cannot run multiple commands concurrently. + const paths = ['/usr/bin/kdesudo', '/usr/bin/pkexec']; + + function test() { + if (index === paths.length) { + return end(new Error('Unable to find pkexec or kdesudo.')); + } + const path = paths[index++]; + + Node.fs.stat(path, + (error) => { + if (error) { + if (error.code === 'ENOTDIR') { + return test(); + } + if (error.code === 'ENOENT') { + return test(); + } + end(error); + } else { + end(undefined, path); + } + }, + ); + } + test(); +} + +function Mac(instance, callback) { + const temp = Node.os.tmpdir(); + + if (!temp) { + return callback(new Error('os.tmpdir() not defined.')); + } + const user = Node.process.env.USER; // Applet shell scripts require $USER. + + if (!user) { + return callback(new Error('env[\'USER\'] not defined.')); + } + UUID(instance, + (error, uuid) => { + if (error) { + return callback(error); + } + instance.uuid = uuid; + instance.path = Node.path.join( + temp, + instance.uuid, + `${ instance.options.name }.app`, + ); + function end(error, stdout, stderr) { + Remove(Node.path.dirname(instance.path), + (errorRemove) => { + if (error) { + return callback(error); + } + if (errorRemove) { + return callback(errorRemove); + } + callback(undefined, stdout, stderr); + }, + ); + } + MacApplet(instance, + (error, stdout, stderr) => { + if (error) { + return end(error, stdout, stderr); + } + MacIcon(instance, + (error) => { + if (error) { + return end(error); + } + MacPropertyList(instance, + (error, stdout, stderr) => { + if (error) { + return end(error, stdout, stderr); + } + MacCommand(instance, + (error) => { + if (error) { + return end(error); + } + MacOpen(instance, + (error, stdout, stderr) => { + if (error) { + return end(error, stdout, stderr); + } + MacResult(instance, end); + }, + ); + }, + ); + }, + ); + }, + ); + }, + ); + }, + ); +} + +function MacApplet(instance, end) { + const parent = Node.path.dirname(instance.path); + + Node.fs.mkdir(parent, + (error) => { + if (error) { + return end(error); + } + const zip = Node.path.join(parent, 'sudo-prompt-applet.zip'); + + Node.fs.writeFile(zip, APPLET, 'base64', + (error) => { + if (error) { + return end(error); + } + let command = []; + + command.push('/usr/bin/unzip'); + command.push('-o'); // Overwrite any existing applet. + command.push(`"${ EscapeDoubleQuotes(zip) }"`); + command.push(`-d "${ EscapeDoubleQuotes(instance.path) }"`); + command = command.join(' '); + Node.child.exec(command, { encoding: 'utf-8' }, end); + }, + ); + }, + ); +} + +function MacCommand(instance, end) { + const path = Node.path.join( + instance.path, + 'Contents', + 'MacOS', + 'sudo-prompt-command', + ); + let script = []; + + // Preserve current working directory: + // We do this for commands that rely on relative paths. + // This runs in a subshell and will not change the cwd of sudo-prompt-script. + script.push(`cd "${ EscapeDoubleQuotes(Node.process.cwd()) }"`); + // Export environment variables: + for (const key in instance.options.env) { + const value = instance.options.env[key]; + + script.push(`export ${ key }="${ EscapeDoubleQuotes(value) }"`); + } + script.push(instance.command); + script = script.join('\n'); + Node.fs.writeFile(path, script, 'utf-8', end); +} + +function MacIcon(instance, end) { + if (!instance.options.icns) { + return end(); + } + Node.fs.readFile(instance.options.icns, + (error, buffer) => { + if (error) { + return end(error); + } + const icns = Node.path.join( + instance.path, + 'Contents', + 'Resources', + 'applet.icns', + ); + + Node.fs.writeFile(icns, buffer, end); + }, + ); +} + +function MacOpen(instance, end) { + // We must run the binary directly so that the cwd will apply. + const binary = Node.path.join(instance.path, 'Contents', 'MacOS', 'applet'); + // We must set the cwd so that the AppleScript can find the shell scripts. + const options = { + cwd: Node.path.dirname(binary), + encoding: 'utf-8', + }; + + // We use the relative path rather than the absolute path. The instance.path + // may contain spaces which the cwd can handle, but which exec() cannot. + Node.child.exec(`./${ Node.path.basename(binary) }`, options, end); +} + +function MacPropertyList(instance, end) { + // Value must be in single quotes (not double quotes) according to man entry. + // e.g. defaults write com.companyname.appname "Default Color" '(255, 0, 0)' + // The defaults command will be changed in an upcoming major release to only + // operate on preferences domains. General plist manipulation utilities will + // be folded into a different command-line program. + const plist = Node.path.join(instance.path, 'Contents', 'Info.plist'); + const path = EscapeDoubleQuotes(plist); + const key = EscapeDoubleQuotes('CFBundleName'); + const value = `${ instance.options.name } Password Prompt`; + + if (/'/.test(value)) { + return end(new Error('Value should not contain single quotes.')); + } + let command = []; + + command.push('/usr/bin/defaults'); + command.push('write'); + command.push(`"${ path }"`); + command.push(`"${ key }"`); + command.push(`'${ value }'`); // We must use single quotes for value. + command = command.join(' '); + Node.child.exec(command, { encoding: 'utf-8' }, end); +} + +function MacResult(instance, end) { + const cwd = Node.path.join(instance.path, 'Contents', 'MacOS'); + + Node.fs.readFile(Node.path.join(cwd, 'code'), 'utf-8', + (error, code) => { + if (error) { + if (error.code === 'ENOENT') { + return end(new Error(PERMISSION_DENIED)); + } + end(error); + } else { + Node.fs.readFile(Node.path.join(cwd, 'stdout'), 'utf-8', + (error, stdout) => { + if (error) { + return end(error); + } + Node.fs.readFile(Node.path.join(cwd, 'stderr'), 'utf-8', + (error, stderr) => { + if (error) { + return end(error); + } + code = parseInt(code.trim(), 10); // Includes trailing newline. + if (code === 0) { + end(undefined, stdout, stderr); + } else { + error = new Error( + `Command failed: ${ instance.command }\n${ stderr }`, + ); + error.code = String(code); + end(error, stdout, stderr); + } + }, + ); + }, + ); + } + }, + ); +} + +function Remove(path, end) { + if (typeof path !== 'string' || !path.trim()) { + return end(new Error('Argument path not defined.')); + } + let command = []; + + if (Node.process.platform === 'win32') { + if (/"/.test(path)) { + return end(new Error('Argument path cannot contain double-quotes.')); + } + command.push(`rmdir /s /q "${ path }"`); + } else { + command.push('/bin/rm'); + command.push('-rf'); + command.push(`"${ EscapeDoubleQuotes(Node.path.normalize(path)) }"`); + } + command = command.join(' '); + Node.child.exec(command, { encoding: 'utf-8' }, end); +} + +function UUID(instance, end) { + Node.crypto.randomBytes(256, + (error, random) => { + if (error) { + random = `${ Date.now() }${ Math.random() }`; + } + const hash = Node.crypto.createHash('SHA256'); + + hash.update('sudo-prompt-3'); + hash.update(instance.options.name); + hash.update(instance.command); + hash.update(random); + const uuid = hash.digest('hex').slice(-32); + + if (!uuid || typeof uuid !== 'string' || uuid.length !== 32) { + // This is critical to ensure we don't remove the wrong temp directory. + return end(new Error('Expected a valid UUID.')); + } + end(undefined, uuid); + }, + ); +} + +function ValidName(string) { + // We use 70 characters as a limit to side-step any issues with Unicode + // normalization form causing a 255 character string to exceed the fs limit. + if (!/^[a-z0-9 ]+$/i.test(string)) { + return false; + } + if (string.trim().length === 0) { + return false; + } + + return string.length <= 70; +} + +function Windows(instance, callback) { + const temp = Node.os.tmpdir(); + + if (!temp) { + return callback(new Error('os.tmpdir() not defined.')); + } + UUID(instance, + (error, uuid) => { + if (error) { + return callback(error); + } + instance.uuid = uuid; + instance.path = Node.path.join(temp, instance.uuid); + if (/"/.test(instance.path)) { + // We expect double quotes to be reserved on Windows. + // Even so, we test for this and abort if they are present. + return callback( + new Error('instance.path cannot contain double-quotes.'), + ); + } + instance.pathElevate = Node.path.join(instance.path, 'elevate.vbs'); + instance.pathExecute = Node.path.join(instance.path, 'execute.bat'); + instance.pathCommand = Node.path.join(instance.path, 'command.bat'); + instance.pathStdout = Node.path.join(instance.path, 'stdout'); + instance.pathStderr = Node.path.join(instance.path, 'stderr'); + instance.pathStatus = Node.path.join(instance.path, 'status'); + Node.fs.mkdir(instance.path, + (error) => { + if (error) { + return callback(error); + } + function end(error, stdout, stderr) { + Remove(instance.path, + (errorRemove) => { + if (error) { + return callback(error); + } + if (errorRemove) { + return callback(errorRemove); + } + callback(undefined, stdout, stderr); + }, + ); + } + WindowsWriteExecuteScript(instance, + (error) => { + if (error) { + return end(error); + } + WindowsWriteCommandScript(instance, + (error) => { + if (error) { + return end(error); + } + WindowsElevate(instance, + (error, stdout, stderr) => { + if (error) { + return end(error, stdout, stderr); + } + WindowsWaitForStatus(instance, + (error) => { + if (error) { + return end(error); + } + WindowsResult(instance, end); + }, + ); + }, + ); + }, + ); + }, + ); + }, + ); + }, + ); +} + +function WindowsElevate(instance, end) { + // We used to use this for executing elevate.vbs: + // var command = 'cscript.exe //NoLogo "' + instance.pathElevate + '"'; + let command = []; + + command.push('powershell.exe'); + command.push('Start-Process'); + command.push('-FilePath'); + // Escape characters for cmd using double quotes: + // Escape characters for PowerShell using single quotes: + // Escape single quotes for PowerShell using backtick: + // See: https://ss64.com/ps/syntax-esc.html + command.push(`"'${ instance.pathExecute.replace(/'/g, "`'") }'"`); + command.push('-WindowStyle hidden'); + command.push('-Verb runAs'); + command = command.join(' '); + const child = Node.child.exec(command, { encoding: 'utf-8' }, + (error, stdout, stderr) => { + // We used to return PERMISSION_DENIED only for error messages containing + // the string 'canceled by the user'. However, Windows internationalizes + // error messages (issue 96) so now we must assume all errors here are + // permission errors. This seems reasonable, given that we already run the + // user's command in a subshell. + if (error) { + return end(new Error(PERMISSION_DENIED), stdout, stderr); + } + end(); + }, + ); + + child.stdin.end(); // Otherwise PowerShell waits indefinitely on Windows 7. +} + +function WindowsResult(instance, end) { + Node.fs.readFile(instance.pathStatus, 'utf-8', + (error, code) => { + if (error) { + return end(error); + } + Node.fs.readFile(instance.pathStdout, 'utf-8', + (error, stdout) => { + if (error) { + return end(error); + } + Node.fs.readFile(instance.pathStderr, 'utf-8', + (error, stderr) => { + if (error) { + return end(error); + } + code = parseInt(code.trim(), 10); + if (code === 0) { + end(undefined, stdout, stderr); + } else { + error = new Error( + `Command failed: ${ instance.command }\r\n${ stderr }`, + ); + error.code = String(code); + end(error, stdout, stderr); + } + }, + ); + }, + ); + }, + ); +} + +function WindowsWaitForStatus(instance, end) { + // VBScript cannot wait for the elevated process to finish so we have to poll. + // VBScript cannot return error code if user does not grant permission. + // PowerShell can be used to elevate and wait on Windows 10. + // PowerShell can be used to elevate on Windows 7 but it cannot wait. + // powershell.exe Start-Process cmd.exe -Verb runAs -Wait + Node.fs.stat(instance.pathStatus, + (error, stats) => { + if ((error && error.code === 'ENOENT') || stats.size < 2) { + // Retry if file does not exist or is not finished writing. + // We expect a file size of 2. That should cover at least "0\r". + // We use a 1 second timeout to keep a light footprint for long-lived + // sudo-prompt processes. + setTimeout( + () => { + // If administrator has no password and user clicks Yes, then + // PowerShell returns no error and execute (and command) never runs. + // We check that command output has been redirected to stdout file: + Node.fs.stat(instance.pathStdout, + (error) => { + if (error) { + return end(new Error(PERMISSION_DENIED)); + } + WindowsWaitForStatus(instance, end); + }, + ); + }, + 1000, + ); + } else if (error) { + end(error); + } else { + end(); + } + }, + ); +} + +function WindowsWriteCommandScript(instance, end) { + const cwd = Node.process.cwd(); + + if (/"/.test(cwd)) { + // We expect double quotes to be reserved on Windows. + // Even so, we test for this and abort if they are present. + return end(new Error('process.cwd() cannot contain double-quotes.')); + } + let script = []; + + script.push('@echo off'); + // Set code page to UTF-8: + script.push('chcp 65001>nul'); + // Preserve current working directory: + // We pass /d as an option in case the cwd is on another drive (issue 70). + script.push(`cd /d "${ cwd }"`); + // Export environment variables: + for (const key in instance.options.env) { + // "The characters <, >, |, &, ^ are special command shell characters, and + // they must be preceded by the escape character (^) or enclosed in + // quotation marks. If you use quotation marks to enclose a string that + // contains one of the special characters, the quotation marks are set as + // part of the environment variable value." + // In other words, Windows assigns everything that follows the equals sign + // to the value of the variable, whereas Unix systems ignore double quotes. + const value = instance.options.env[key]; + + script.push(`set ${ key }=${ value.replace(/([<>\\|&^])/g, '^$1') }`); + } + script.push(instance.command); + script = script.join('\r\n'); + Node.fs.writeFile(instance.pathCommand, script, 'utf-8', end); +} + +function WindowsWriteExecuteScript(instance, end) { + let script = []; + + script.push('@echo off'); + script.push( + `call "${ instance.pathCommand }"` + + ` > "${ instance.pathStdout }" 2> "${ instance.pathStderr }"`, + ); + script.push(`(echo %ERRORLEVEL%) > "${ instance.pathStatus }"`); + script = script.join('\r\n'); + Node.fs.writeFile(instance.pathExecute, script, 'utf-8', end); +} + +export const exec = Exec; + +// We used to expect that applet.app would be included with this module. +// This could not be copied when sudo-prompt was packaged within an asar file. +// We now store applet.app as a zip file in base64 within index.js instead. +// To recreate: "zip -r ../applet.zip Contents" (with applet.app as CWD). +// The zip file must not include applet.app as the root directory so that we +// can extract it directly to the target app directory. +const APPLET = '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'; + +const PERMISSION_DENIED = 'User did not grant permission.'; +const NO_POLKIT_AGENT = 'No polkit authentication agent found.'; + +// See issue 66: +const MAX_BUFFER = 134217728; diff --git a/pkg/rancher-desktop/sudo-prompt/linux.png b/pkg/rancher-desktop/sudo-prompt/linux.png new file mode 100644 index 00000000000..cd5b94ad698 Binary files /dev/null and b/pkg/rancher-desktop/sudo-prompt/linux.png differ diff --git a/pkg/rancher-desktop/sudo-prompt/macos.png b/pkg/rancher-desktop/sudo-prompt/macos.png new file mode 100644 index 00000000000..d32dffaa41a Binary files /dev/null and b/pkg/rancher-desktop/sudo-prompt/macos.png differ diff --git a/pkg/rancher-desktop/sudo-prompt/package.json b/pkg/rancher-desktop/sudo-prompt/package.json new file mode 100644 index 00000000000..16ab3acc5a9 --- /dev/null +++ b/pkg/rancher-desktop/sudo-prompt/package.json @@ -0,0 +1,41 @@ +{ + "name": "sudo-prompt", + "version": "9.2.1", + "description": "Run a command using sudo, prompting the user with an OS dialog if necessary", + "main": "index.js", + "types": "index.d.ts", + "files": [ + "LICENSE", + "README.md", + "index.d.ts", + "index.js", + "package.json", + "test.js", + "test-concurrent.js" + ], + "repository": { + "type": "git", + "url": "git+https://github.com/jorangreef/sudo-prompt.git" + }, + "keywords": [ + "sudo", + "os", + "dialog", + "prompt", + "command", + "exec", + "user access control", + "UAC", + "privileges", + "administrative", + "elevate", + "run as administrator" + ], + "author": "Joran Dirk Greef", + "license": "MIT", + "bugs": { + "url": "https://github.com/jorangreef/sudo-prompt/issues" + }, + "homepage": "https://github.com/jorangreef/sudo-prompt#readme", + "scripts": {} +} diff --git a/pkg/rancher-desktop/sudo-prompt/test-concurrent.js b/pkg/rancher-desktop/sudo-prompt/test-concurrent.js new file mode 100644 index 00000000000..1424009b78e --- /dev/null +++ b/pkg/rancher-desktop/sudo-prompt/test-concurrent.js @@ -0,0 +1,34 @@ +import { exec } from 'child_process'; + +import { exec as sudo } from './'; + +function kill(end) { + if (process.platform === 'win32') { + return end(); + } + exec('sudo -k', end); +} + +kill( + () => { + const options = { name: 'Sudo Prompt' }; + + let sleep; + + if (process.platform === 'win32') { + sleep = 'timeout /t 10\r\necho world'; + } else { + sleep = 'sleep 10 && echo world'; + } + sudo(sleep, options, + (error, stdout, stderr) => { + console.log(error, stdout, stderr); + }, + ); + sudo('echo hello', options, + (error, stdout, stderr) => { + console.log(error, stdout, stderr); + }, + ); + }, +); diff --git a/pkg/rancher-desktop/sudo-prompt/test.js b/pkg/rancher-desktop/sudo-prompt/test.js new file mode 100644 index 00000000000..39d987a013a --- /dev/null +++ b/pkg/rancher-desktop/sudo-prompt/test.js @@ -0,0 +1,80 @@ +import assert from 'assert'; +import { exec } from 'child_process'; +import { statSync } from 'fs'; + +import { exec as sudo } from './'; + +function kill(end) { + if (process.platform === 'win32') { + return end(); + } + exec('sudo -k', end); +} + +function icns() { + if (process.platform !== 'darwin') { + return undefined; + } + const path = '/Applications/Electron.app/Contents/Resources/Electron.icns'; + + try { + statSync(path); + + return path; + } catch (error) { + } + + return undefined; +} + +kill( + () => { + const options = { + env: { SUDO_PROMPT_TEST_ENV: 'hello world' }, + icns: icns(), + name: 'Electron', + }; + + let command; + let expected; + + if (process.platform === 'win32') { + command = 'echo %SUDO_PROMPT_TEST_ENV%'; + expected = 'hello world\r\n'; + } else { + // We use double quotes to tell echo to preserve internal space: + command = 'echo "$SUDO_PROMPT_TEST_ENV"'; + expected = 'hello world\n'; + } + console.log( + `sudo.exec(${ + JSON.stringify(command) }, ${ + JSON.stringify(options) + })`, + ); + sudo(command, options, + (error, stdout, stderr) => { + console.log('error:', error); + console.log(`stdout: ${ JSON.stringify(stdout) }`); + console.log(`stderr: ${ JSON.stringify(stderr) }`); + assert(error === undefined || typeof error === 'object'); + assert(stdout === undefined || typeof stdout === 'string'); + assert(stderr === undefined || typeof stderr === 'string'); + kill( + () => { + if (error) { + throw error; + } + if (stdout !== expected) { + throw new Error(`stdout != ${ JSON.stringify(expected) }`); + } + if (stderr !== '') { + throw new Error('stderr != ""'); + } + console.log('OK'); + }, + ); + }, + ); + }, +); diff --git a/pkg/rancher-desktop/sudo-prompt/windows.png b/pkg/rancher-desktop/sudo-prompt/windows.png new file mode 100644 index 00000000000..c3dbc0f07f7 Binary files /dev/null and b/pkg/rancher-desktop/sudo-prompt/windows.png differ diff --git a/yarn.lock b/yarn.lock index 59c962c3936..4475d09ce09 100644 --- a/yarn.lock +++ b/yarn.lock @@ -12084,11 +12084,6 @@ stylehacks@^5.1.1: browserslist "^4.21.4" postcss-selector-parser "^6.0.4" -sudo-prompt@9.2.1: - version "9.2.1" - resolved "https://registry.yarnpkg.com/sudo-prompt/-/sudo-prompt-9.2.1.tgz#77efb84309c9ca489527a4e749f287e6bdd52afd" - integrity sha512-Mu7R0g4ig9TUuGSxJavny5Rv0egCEtpZRNMrZaYS1vxkiIxGiGUwoezU3LazIQ+KE04hTrTfNPgxU5gzi7F5Pw== - sumchecker@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/sumchecker/-/sumchecker-3.0.1.tgz#6377e996795abb0b6d348e9b3e1dfb24345a8e42"