126 lines
4.3 KiB
JavaScript
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;
|
|
};
|