From 0b5c7d736188643a37250f56bdeaf82a356e69e8 Mon Sep 17 00:00:00 2001 From: Joran Dirk Greef Date: Fri, 15 Apr 2016 13:11:23 +0200 Subject: [PATCH] Add support for `tty_tickets` on OS X (and major breaking changes). Add support for `tty_tickets` on OS X. Ensure all line lengths are less than 80 characters. Fix shell commands to use absolute path (e.g. `/bin/rm`, `/usr/bin/defaults`). Fix `options.icns` to work with asar packages. Fix internal method names to reduce chance of a clash with local variables. Remove `options.onChildProcess()` (no longer possible to support). Remove batching of password prompts on OS X. Remove deprecated `setName()`. Remove deprecated `touch()`. Fixes: https://github.com/jorangreef/sudo-prompt/issues/13 --- README.md | 20 +-- index.js | 443 ++++++++++++++++++++++++++++-------------------------- 2 files changed, 233 insertions(+), 230 deletions(-) diff --git a/README.md b/README.md index 4f3e294..4bdeba7 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ Run a non-graphical terminal command using `sudo`, prompting the user with a gra ![A sudo prompt on OS X for an app called "Ronomon"](./osx.png) -`sudo-prompt` provides a native OS dialog prompt on **OS X** and **Linux (beta)** with custom name and optional icon. +`sudo-prompt` provides a native OS dialog prompt on **OS X** and **Linux** with custom name and optional icon. `sudo-prompt` has no external dependencies and does not require any native bindings. @@ -20,32 +20,26 @@ var sudo = require('sudo-prompt'); var options = { name: 'Ronomon', icns: '/path/to/icns/file', // (optional) - onChildProcess: function(childProcess) {} // (optional) }; sudo.exec('echo hello', options, function(error, stdout, stderr) {}); ``` `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. -If `options.onChildProcess` is provided, then this callback will be called whenever the command is executed, which may be several times if a password prompt is required. For example, `options.onChildProcess` will be called when the command is first run, and if the command fails because of the lack of a sudo session, then `options.onChildProcess` will be called again the next time the command is run after the password prompt. - -*Please note that `sudo.setName()` and `sudo.touch()` have been deprecated to provide a completely functional interface to `exec()`. These calls will be removed in the next major release of `sudo-prompt`.* - ## Behavior -On OS X, `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 will 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). +Do not depend on any current working directory or environment variables, and use absolute paths not relative paths. -*Please note that Linux support is currently in beta and requires more testing across Linux distributions.* +On OS X, `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 `gksudo`, `pkexec`, or `kdesudo` to show the password prompt and run your command. Where possible, `sudo-prompt` will try and get these to mimic `sudo` as much as possible (for example by preserving environment), but your command should not rely on any environment variables or relative paths, in order to work correctly. 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. Passing `options.icns` is currently not supported by `sudo-prompt` on Linux. Patches are welcome to add support for icons based on `polkit`. +On Linux, `sudo-prompt` will use either `gksudo`, `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. Passing `options.icns` is currently not supported by `sudo-prompt` on Linux. Patches are welcome to add support for icons based on `polkit`. +## 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 OS X, you can issue multiple calls to `sudo.exec` concurrently, and `sudo-prompt` will batch up multiple permission requests into a single password prompt. These calls will be batched to the extent that they share the same `options.name` and `options.icns` arguments (including the actual content of `options.icns` if provided). +On systems where the user has opted to have `tty-tickets` enabled, each call to `exec()` will result in a separate password prompt. Where `tty-tickets` are disabled, subsequent calls to `exec()` (but not concurrent calls) will not require a password prompt, so long as the user's `sudo` timestamp file remains valid. -On Linux, `sudo` usually has `tty-tickets` enabled. This prevents `sudo-prompt` from batching up multiple permission requests, and will result in a separate password prompt for each call. - -While `sudo-prompt` may batch up calls, you should never rely on `sudo-prompt` to execute your calls in order. For example, several calls may be waiting on a password prompt, and the next call after the password prompt may execute before any of these calls. If you need to enforce ordering of calls, then you should explicitly order your calls in your application. +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 queue your calls to `exec()` to make sure your user is not overloaded with password prompts. ## Invalidating the timestamp You can invalidate the user's `sudo` timestamp file to force the prompt to appear by running the following command in your terminal: diff --git a/index.js b/index.js index c42dda9..735377f 100644 --- a/index.js +++ b/index.js @@ -8,50 +8,38 @@ var Node = { util: require('util') }; -function attempt(attempts, command, options, end) { - if (typeof attempts !== 'number' || Math.floor(attempts) !== attempts || attempts < 0) { - return end(new Error('Attempts argument should be a positive integer.')); - } +function Attempt(instance, end) { // The -n (non-interactive) option prevents sudo from prompting the user for - // a password. If a password is required for the command to run, sudo will - // display an error message and exit. - var childProcess = Node.child.exec('/usr/bin/sudo -n ' + command, + // a password. If a password is required, sudo will return an error and exit. + var command = []; + command.push('/usr/bin/sudo'); + command.push('-n'); + command.push(instance.command); + command = command.join(' '); + Node.child.exec(command, function(error, stdout, stderr) { if (/sudo: /i.test(stderr)) { - if (attempts > 0) return end(new Error('User did not grant permission.')); - if (Node.process.platform === 'linux') { - // Linux will probably use TTY tickets for sudo timestamps. - // If so, we cannot easily extend the sudo timestamp for the user. - // We prefer this since a single prompt can be used for multiple calls. - // Instead, we have to use a separate prompt for each call. - return linux(command, options, end); + var platform = Node.process.platform; + if (platform === 'linux') { + return Linux(instance, end); + } else if (platform === 'darwin') { + return Mac(instance, end); + } else { + end(new Error('Platform not yet supported.')); } - prompt(options, - function(error) { - if (error) return end(error); - attempt(++attempts, command, options, end); // Cannot use ++ suffix here. - } - ); } else { end(error, stdout, stderr); } } ); - if (options.onChildProcess) options.onChildProcess(childProcess); } -function copy(source, target, end) { - source = escapeDoubleQuotes(Node.path.normalize(source)); - target = escapeDoubleQuotes(Node.path.normalize(target)); - var command = '/bin/cp -R -p "' + source + '" "' + target + '"'; - Node.child.exec(command, end); -} - -function escapeDoubleQuotes(string) { +function EscapeDoubleQuotes(string) { + if (typeof string !== 'string') throw new Error('Expected a string.'); return string.replace(/"/g, '\\"'); } -function exec() { +function Exec() { if (arguments.length < 1 || arguments.length > 3) { throw new Error('Wrong number of arguments.'); } @@ -82,53 +70,72 @@ function exec() { } } if (/^sudo/i.test(command)) { - return end(new Error('Command should not contain "sudo".')); + return end(new Error('Command should not be prefixed with "sudo".')); } if (typeof options.name === 'undefined') { - if (typeof name === 'string') { - // If name is a string, it has been set and verified by setName. - options.name = name; + var title = Node.process.title; + if (ValidName(title)) { + options.name = title; } else { - var title = Node.process.title; - if (validName(title)) { - options.name = title; - } else { - return end(new Error('options.name must be provided (process.title is not valid).')); - } + return end(new Error('process.title cannot be used as a valid name.')); } - } else if (!validName(options.name)) { - return end(new Error('options.name must be alphanumeric only (spaces are allowed).')); + } else if (!ValidName(options.name)) { + var 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 be a non-empty string if provided.')); + return end(new Error('options.icns must not be empty if provided.')); } } - if (typeof options.onChildProcess !== 'undefined') { - if (typeof options.onChildProcess !== 'function') { - return end(new Error('options.onChildProcess must be a function if provided.')); - } - } - if (Node.process.platform !== 'darwin' && Node.process.platform !== 'linux') { + var platform = Node.process.platform; + if (platform !== 'darwin' && platform !== 'linux') { return end(new Error('Platform not yet supported.')); } - attempt(0, command, options, end); + var instance = { + command: command, + options: options, + uuid: undefined, + path: undefined + }; + Attempt(instance, end); } -function linux(command, options, end) { - linuxBinary( +function Linux(instance, end) { + LinuxBinary(instance, function(error, binary) { if (error) return end(error); - linuxExecute(binary, command, options, end); + var command = []; + command.push('"' + EscapeDoubleQuotes(binary) + '"'); + if (/gksudo/i.test(binary)) { + command.push('--preserve-env'); + command.push('--sudo-mode'); + var description = EscapeDoubleQuotes(instance.options.name); + command.push('--description="' + description + '"'); + } else if (/pkexec/i.test(binary)) { + command.push('--disable-internal-agent'); + } + command.push(instance.command); + command = command.join(' '); + Node.child.exec(command, + function(error, stdout, stderr) { + if (error && /Request dismissed|Command failed/i.test(error)) { + error = new Error(PERMISSION_DENIED); + } + end(error, stdout, stderr); + } + ); } ); } -function linuxBinary(end) { +function LinuxBinary(instance, end) { var index = 0; - // We prefer gksudo over pkexec since it gives a nicer prompt: + // We prefer gksudo over pkexec since it enables a better prompt: var paths = ['/usr/bin/gksudo', '/usr/bin/pkexec', '/usr/bin/kdesudo']; function test() { if (index === paths.length) { @@ -138,11 +145,9 @@ function linuxBinary(end) { Node.fs.stat(path, function(error) { if (error) { - if (error.code === 'ENOTDIR' || error.code === 'ENOENT') { - return test(); - } else { - return end(error); - } + if (error.code === 'ENOTDIR') return test(); + if (error.code === 'ENOENT') return test(); + end(error); } else { end(undefined, path); } @@ -152,80 +157,53 @@ function linuxBinary(end) { test(); } -function linuxExecute(binary, command, options, end) { - var string = ''; - string += '"' + escapeDoubleQuotes(binary) + '" '; - if (/gksudo/i.test(binary)) { - string += '--preserve-env '; - string += '--sudo-mode '; - string += '--description="' + escapeDoubleQuotes(options.name) + '" '; - } else if (/pkexec/i.test(binary)) { - string += '--disable-internal-agent '; - } - string += command; - var childProcess = Node.child.exec(string, - function(error, stdout, stderr) { - if (error && /Request dismissed|Command failed/i.test(error)) { - error = new Error('User did not grant permission.'); - } - end(error, stdout, stderr); - } - ); - if (options.onChildProcess) options.onChildProcess(childProcess); -} - -function macApplet(target, options, end) { - var source = Node.path.join(Node.path.dirname(target), 'sudo-prompt-applet.zip'); - Node.fs.writeFile(source, APPLET, 'base64', - function(error) { - if (error) return end(error); - var command = 'unzip -o '; // Overwrite any existing applet. - command += '"' + escapeDoubleQuotes(source) + '" '; - command += '-d "' + escapeDoubleQuotes(target) + '"'; - Node.child.exec(command, end); - } - ); -} - -function macIcon(target, options, end) { - if (!options.icns) return end(); - copy(options.icns, Node.path.join(target, 'Contents', 'Resources', 'applet.icns'), end); -} - -function macOpen(target, options, end) { - target = escapeDoubleQuotes(Node.path.normalize(target)); - var command = 'open -n -W "' + target + '"'; - Node.child.exec(command, end); -} - -function macPrompt(hash, options, callback) { +function Mac(instance, callback) { var temp = Node.os.tmpdir(); - if (!temp) return callback(new Error('Requires os.tmpdir() to be defined.')); - if (!Node.process.env.USER) return callback(new Error('Requires env[\'USER\'] to be defined.')); - var target = Node.path.join(temp, hash, options.name + '.app'); - function end(error) { - remove(Node.path.dirname(target), - function(errorRemove) { - if (error) return callback(error); - if (errorRemove) return callback(errorRemove); - callback(); + if (!temp) return callback(new Error('os.tmpdir() not defined.')); + var user = Node.process.env.USER; // Applet shell scripts require $USER. + if (!user) return callback(new Error('env[\'USER\'] not defined.')); + UUID(instance, + function(error, uuid) { + if (error) return callback(error); + if (!uuid || typeof uuid !== 'string' || uuid.length !== 32) { + // This is critical to ensure we don't remove the wrong temp directory. + return callback(new Error('Expected a valid UUID.')); } - ); - } - Node.fs.mkdir(Node.path.dirname(target), - function(error) { - if (error && error.code === 'EEXIST') error = undefined; - if (error) return end(error); - macApplet(target, options, + 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), + function(errorRemove) { + if (error) return callback(error); + if (errorRemove) return callback(errorRemove); + callback(error, stdout, stderr); + } + ); + } + MacApplet(instance, function(error) { if (error) return end(error); - macIcon(target, options, + MacIcon(instance, function(error) { if (error) return end(error); - macPropertyList(target, options, + MacPropertyList(instance, function(error) { if (error) return end(error); - macOpen(target, options, end); + MacCommand(instance, + function(error) { + if (error) return end(error); + MacOpen(instance, + function(error) { + if (error) return end(error); + MacResult(instance, end); + } + ); + } + ); } ); } @@ -236,125 +214,156 @@ function macPrompt(hash, options, callback) { ); } -function macPropertyList(target, options, end) { +function MacApplet(instance, end) { + var parent = Node.path.dirname(instance.path); + Node.fs.mkdir(parent, + function(error) { + if (error) return end(error); + var zip = Node.path.join(parent, 'sudo-prompt-applet.zip'); + Node.fs.writeFile(zip, APPLET, 'base64', + function(error) { + if (error) return end(error); + var 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, end); + } + ); + } + ); +} + +function MacCommand(instance, end) { + var path = Node.path.join( + instance.path, + 'Contents', + 'MacOS', + 'sudo-prompt-command' + ); + Node.fs.writeFile(path, instance.command, 'utf-8', end); +} + +function MacIcon(instance, end) { + if (!instance.options.icns) return end(); + Node.fs.readFile(instance.options.icns, + function(error, buffer) { + if (error) return end(error); + var 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. + var binary = Node.path.join(instance.path, 'Contents', 'MacOS', 'applet'); + // We must set the cwd so that the AppleScript can find the shell scripts. + var options = { cwd: Node.path.dirname(binary) }; + Node.child.exec(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. - var path = escapeDoubleQuotes(Node.path.join(target, 'Contents', 'Info.plist')); - var key = escapeDoubleQuotes('CFBundleName'); - var value = options.name + ' Password Prompt'; + var plist = Node.path.join(instance.path, 'Contents', 'Info.plist'); + var path = EscapeDoubleQuotes(plist); + var key = EscapeDoubleQuotes('CFBundleName'); + var value = instance.options.name + ' Password Prompt'; if (/'/.test(value)) { return end(new Error('Value should not contain single quotes.')); } - var command = 'defaults write "' + path + '" "' + key + '" \'' + value + '\''; + var 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, end); } -var name = null; - -function prompt(options, end) { - version(options, - function(error, hash) { - if (error) return end(error); - if (!prompting.hasOwnProperty(hash)) prompting[hash] = []; - prompting[hash].push(end); - // Already waiting for user to enter password... - // We expect that exec() may be called multiple times. - // If a prompt is already pending, then we wait for the result of the prompt - // and do not trigger another permission request dialog. - if (prompting[hash].length > 1) return; - function done(error) { - // We must clear prompting queue before looping, otherwise sudo calls which - // are synchronously issued by these callbacks may fail to be executed. - var callbacks = prompting[hash]; - delete prompting[hash]; - for (var index = 0, length = callbacks.length; index < length; index++) { - var callback = callbacks[index]; - callback(error); - } +function MacResult(instance, end) { + var cwd = Node.path.join(instance.path, 'Contents', 'MacOS'); + Node.fs.readFile(Node.path.join(cwd, 'code'), 'utf-8', + function(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', + function(error, stdout) { + if (error) return end(error); + Node.fs.readFile(Node.path.join(cwd, 'stderr'), 'utf-8', + function(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); + end(error, stdout, stderr); + } + } + ); + } + ); } - if (Node.process.platform === 'darwin') return macPrompt(hash, options, done); - if (Node.process.platform === 'linux') return linuxPrompt(hash, options, done); - end(new Error('Platform not supported (unexpected, should have been checked already).')); } ); } -var prompting = {}; - -function remove(target, end) { - if (!target) return end(new Error('Target not defined.')); - target = escapeDoubleQuotes(Node.path.normalize(target)); - var command = 'rm -rf "' + target + '"'; +function Remove(path, end) { + if (!path) return end(new Error('Remove: Path not defined.')); + var command = []; + command.push('/bin/rm'); + command.push('-rf'); + command.push('"' + EscapeDoubleQuotes(Node.path.normalize(path)) + '"'); + command = command.join(' '); Node.child.exec(command, end); } -function setName(string) { - // DEPRECATED to move away from a global variable towards a functional - // interface. Otherwise using setName could have rare race conditions when - // multiple calls need to use different names. - if (!validName(string)) { - throw new Error('Name must be alphanumeric only (spaces are allowed).'); - } - name = string; -} - -function touch(end) { - // DEPRECATED to reduce the surface area of the interface. - // Better to call exec() directly as this supports the options argument. - // touch() may fail if process.title is not valid. - // Depends on setName() which has also been deprecated. - // This is a convenience method to extend the sudo session. - // This uses existing sudo-prompt machinery. - exec('echo touchingsudotimestamp', {}, - function(error, stdout, stderr) { - if (error) return end(error); - end(); // Do not pass stdout and stderr back to callback. - } - ); -} - -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. - return /^[a-z0-9 ]+$/i.test(string) && string.trim().length > 0 && string.length < 70; -} - -function version(options, end) { - versionReadICNS(options, - function(error, buffer) { - if (error) return end(error); +function UUID(instance, end) { + Node.crypto.randomBytes(256, + function(error, random) { + if (error) random = Date.now() + '' + Math.random(); var hash = Node.crypto.createHash('SHA256'); - hash.update('sudo-prompt 2.0.0'); - hash.update(options.name); - hash.update(buffer); + hash.update('sudo-prompt-3'); + hash.update(instance.options.name); + hash.update(instance.command); + hash.update(random); end(undefined, hash.digest('hex').slice(-32)); } ); } -function versionReadICNS(options, end) { - if (!options.icns || Node.process.platform !== 'darwin') { - return end(undefined, new Buffer(0)); - } - // options.icns is supported only on Mac. - Node.fs.readFile(options.icns, end); +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; + if (string.length > 70) return false; + return true; } -module.exports.exec = exec; +module.exports.exec = Exec; -// DEPRECATED: -module.exports.setName = setName; - -// DEPRECATED: -module.exports.touch = touch; - -// We used to expect that applet.app would be included as a file with this module. -// This caused copying issues when sudo-prompt was packaged within an asar file. +// 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 the zip file: zip -r ../applet.zip Contents (with applet.app as CWD) +// 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. -var APPLET = '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'; +var APPLET = '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'; + +var PERMISSION_DENIED = 'User did not grant permission.';