This commit is contained in:
Joran Dirk Greef 2015-06-08 15:24:56 +02:00
parent f653da7e0c
commit dcb02caa3c
3 changed files with 173 additions and 1 deletions

View file

@ -1 +1,25 @@
# sudo
# Sudo
Run a command using sudo, prompting the user with an OS dialog if necessary.
Currently supports native OS dialog on Mac OS X (patches welcome for Linux).
Sudo has no external dependencies and does not contain any native bindings.
## Usage
Note that your command should not contain the sudo prefix.
```
// To run a command using sudo:
var sudo = require('sudo-prompt');
sudo.exec('echo hello', function(error) {});
// To update the sudo timestamp for the current user:
sudo.touch(function(error) {});
// To use something other than process.title as the app name:
// Must be alphanumeric (may contain spaces).
sudo.setName('Your app name')
```
## Concurrency
You can call Sudo.exec and Sudo.touch concurrently, Sudo will batch up permission requests into a single prompt.

124
index.js Normal file
View file

@ -0,0 +1,124 @@
var Node = {
child: require('child_process'),
fs: require('fs'),
os: require('os'),
path: require('path'),
process: process
};
function escapeDoubleQuotes(string) {
return string.replace(/"/g, '\\"');
}
function validName(string) {
return /^[a-z0-9 ]+$/i.test(string) && string.trim().length > 0;
}
var Sudo = function(command, end) {
if (Node.process.platform === 'darwin') return Sudo.Mac(command, end);
end('Platform not yet supported.');
// TO DO: Add support for linux.
};
Sudo.name = undefined;
Sudo.Mac = function(command, end, count) {
if (count === undefined) count = 0;
if (count >= 2) return end(new Error('Permission denied after several password prompts.'));
if (/^sudo/i.test(command)) return end(new Error('Command should not contain "sudo".'));
// Run sudo in non-interactive mode (-n).
Node.child.exec('sudo -n ' + command,
function(error, stdout, stderr) {
if (error) return end(error);
if (stderr !== 'sudo: a password is required\n') {
if (/^sudo:/i.test(stderr)) return end(stderr);
end(error, stdout, stderr);
} else {
Sudo.Mac.prompt(
function(error) {
if (error) return end(error);
Sudo.Mac(command, end, ++count); // Cannot use ++ suffix here.
}
);
}
}
);
};
Sudo.Mac.prompt = function(end) {
var self = this;
var title = Sudo.name || Node.process.title;
if (!validName(title)) return end(new Error('Please use sudo.setName(string) to set your app name (process.title contains invalid characters).'));
var temp = Node.os.tmpdir();
if (!temp) return end(new Error('Requires os.tmpdir() to be defined.'));
if (!Node.process.env.USER) return end(new Error('Requires env[\'USER\'] to be defined.'));
if (self.prompting) {
// Already waiting for user to enter password...
// We expect that Sudo.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.
self.prompting.push(end);
} else {
// Prompting user for password...
self.prompting = [end];
var finish = function(error) {
var callbacks = self.prompting;
self.prompting = false;
for (var index = 0, length = callbacks.length; index < length; index++) {
callbacks[index](error);
}
};
// We copy osascript to a new tmp location using the title of this process as the basename.
// We can then use this new binary to change the sudo timestamp, and OS X will use
// the title of this process when asking the user for permission.
Node.child.exec('which osascript',
function(error, stdout, stderr) {
if (error) return finish(error);
var source = stdout.trim();
var target = Node.path.join(temp, title);
Node.fs.readFile(source,
function(error, buffer) {
if (error) return finish(error);
Node.fs.writeFile(target, buffer,
function(error) {
if (error) return finish(error);
Node.fs.chmod(target, 0755,
function(error) {
if (error) return finish(error);
// Set the sudo timestamp for our user:
var command = '"' + escapeDoubleQuotes(target) + '" -e \'do shell script "mkdir -p /var/db/sudo/$USER; touch /var/db/sudo/$USER" with administrator privileges\'';
Node.child.exec(command,
function(error, stdout, stderr) {
if (/user canceled/i.test(error)) error = new Error('User did not grant permission.');
if (error) return finish(error);
Node.fs.unlink(target, finish);
}
);
}
);
}
);
}
);
}
);
}
};
Sudo.Mac.prompting = false;
exports.exec = Sudo;
exports.touch = function(end) {
Sudo('echo noop',
function(error) {
if (error) return end(error);
end();
}
);
};
exports.setName = function(string) {
if (!validName(string)) throw new Error('Name must be alphanumeric only (spaces are allowed).');
Sudo.name = string;
};

24
package.json Normal file
View file

@ -0,0 +1,24 @@
{
"name": "sudo-prompt",
"version": "1.0.0",
"description": "Run a command using sudo, prompting the user with an OS dialog if necessary",
"main": "index.js",
"repository": {
"type": "git",
"url": "git+https://github.com/jorangreef/sudo.git"
},
"keywords": [
"sudo",
"os",
"dialog",
"prompt",
"command",
"exec"
],
"author": "Joran Dirk Greef",
"license": "MIT",
"bugs": {
"url": "https://github.com/jorangreef/sudo/issues"
},
"homepage": "https://github.com/jorangreef/sudo#readme"
}