sudo-prompt/index.js
2015-08-05 09:18:34 +02:00

126 lines
4.3 KiB
JavaScript

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 Name = undefined;
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.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 = 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) {
// This is a convenience method to extend the sudo session.
// This uses existing sudo-prompt machinery.
Sudo('echo touchingsudotimestamp',
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).');
Name = string;
};