Add support for Windows
This commit is contained in:
parent
fd775d735c
commit
a57487d444
7 changed files with 239 additions and 23 deletions
20
README.md
20
README.md
|
@ -1,14 +1,18 @@
|
|||
# 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.
|
||||
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`.
|
||||
|
||||
![A sudo prompt on OS X for an app called "Ronomon"](./osx.png)
|
||||
## Cross-Platform
|
||||
`sudo-prompt` provides a native OS dialog prompt on **OS X**, **Linux** and **Windows**.
|
||||
|
||||
`sudo-prompt` provides a native OS dialog prompt on **OS X** and **Linux** with custom name and optional icon.
|
||||
![OS X](./osx.png)
|
||||
|
||||
`sudo-prompt` has no external dependencies and does not require any native bindings.
|
||||
![Linux](./linux.png)
|
||||
|
||||
![Windows](./windows.png)
|
||||
|
||||
## Installation
|
||||
`sudo-prompt` has no external dependencies and does not require any native bindings.
|
||||
```
|
||||
npm install sudo-prompt
|
||||
```
|
||||
|
@ -18,8 +22,8 @@ Note: Your command should not start with the `sudo` prefix.
|
|||
```
|
||||
var sudo = require('sudo-prompt');
|
||||
var options = {
|
||||
name: 'Ronomon',
|
||||
icns: '/path/to/icns/file', // (optional)
|
||||
name: 'Electron',
|
||||
icns: '/Applications/Electron.app/Contents/Resources/Electron.icns', // (optional)
|
||||
};
|
||||
sudo.exec('echo hello', options, function(error, stdout, stderr) {});
|
||||
```
|
||||
|
@ -33,6 +37,8 @@ On OS X, `sudo-prompt` should behave just like the `sudo` command in the shell.
|
|||
|
||||
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. 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).
|
||||
|
||||
|
@ -42,7 +48,7 @@ On systems where the user has opted to have `tty-tickets` enabled, each call to
|
|||
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:
|
||||
On OS X 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
|
||||
|
|
218
index.js
218
index.js
|
@ -9,6 +9,8 @@ var Node = {
|
|||
};
|
||||
|
||||
function Attempt(instance, end) {
|
||||
var platform = Node.process.platform;
|
||||
if (platform === 'win32') return Windows(instance, end);
|
||||
// The -n (non-interactive) option prevents sudo from prompting the user for
|
||||
// a password. If a password is required, sudo will return an error and exit.
|
||||
var command = [];
|
||||
|
@ -23,7 +25,6 @@ function Attempt(instance, end) {
|
|||
Node.child.exec(command,
|
||||
function(error, stdout, stderr) {
|
||||
if (/sudo: /i.test(stderr)) {
|
||||
var platform = Node.process.platform;
|
||||
if (platform === 'linux') {
|
||||
return Linux(instance, end);
|
||||
} else if (platform === 'darwin') {
|
||||
|
@ -97,7 +98,7 @@ function Exec() {
|
|||
}
|
||||
}
|
||||
var platform = Node.process.platform;
|
||||
if (platform !== 'darwin' && platform !== 'linux') {
|
||||
if (platform !== 'darwin' && platform !== 'linux' && platform !== 'win32') {
|
||||
return end(new Error('Platform not yet supported.'));
|
||||
}
|
||||
var instance = {
|
||||
|
@ -169,10 +170,6 @@ function Mac(instance, callback) {
|
|||
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.'));
|
||||
}
|
||||
instance.uuid = uuid;
|
||||
instance.path = Node.path.join(
|
||||
temp,
|
||||
|
@ -184,7 +181,7 @@ function Mac(instance, callback) {
|
|||
function(errorRemove) {
|
||||
if (error) return callback(error);
|
||||
if (errorRemove) return callback(errorRemove);
|
||||
callback(error, stdout, stderr);
|
||||
callback(undefined, stdout, stderr);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
@ -338,11 +335,20 @@ function MacResult(instance, end) {
|
|||
}
|
||||
|
||||
function Remove(path, end) {
|
||||
if (!path) return end(new Error('Remove: Path not defined.'));
|
||||
if (typeof path !== 'string' || !path.trim()) {
|
||||
return end(new Error('Argument path not defined.'));
|
||||
}
|
||||
var command = [];
|
||||
command.push('/bin/rm');
|
||||
command.push('-rf');
|
||||
command.push('"' + EscapeDoubleQuotes(Node.path.normalize(path)) + '"');
|
||||
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, end);
|
||||
}
|
||||
|
@ -356,7 +362,12 @@ function UUID(instance, end) {
|
|||
hash.update(instance.options.name);
|
||||
hash.update(instance.command);
|
||||
hash.update(random);
|
||||
end(undefined, hash.digest('hex').slice(-32));
|
||||
var 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);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
@ -370,6 +381,189 @@ function ValidName(string) {
|
|||
return true;
|
||||
}
|
||||
|
||||
function Windows(instance, callback) {
|
||||
var temp = Node.os.tmpdir();
|
||||
if (!temp) return callback(new Error('os.tmpdir() not defined.'));
|
||||
UUID(instance,
|
||||
function(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,
|
||||
function(error) {
|
||||
if (error) return callback(error);
|
||||
function end(error, stdout, stderr) {
|
||||
Remove(instance.path,
|
||||
function(errorRemove) {
|
||||
if (error) return callback(error);
|
||||
if (errorRemove) return callback(errorRemove);
|
||||
callback(undefined, stdout, stderr);
|
||||
}
|
||||
);
|
||||
}
|
||||
WindowsWriteElevateScript(instance,
|
||||
function(error) {
|
||||
if (error) return end(error);
|
||||
WindowsWriteExecuteScript(instance,
|
||||
function(error) {
|
||||
if (error) return end(error);
|
||||
WindowsWriteCommandScript(instance,
|
||||
function(error) {
|
||||
if (error) return end(error);
|
||||
WindowsElevate(instance,
|
||||
function(error) {
|
||||
if (error) return end(error);
|
||||
WindowsWaitForStatus(instance,
|
||||
function(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 + '"';
|
||||
var command = [];
|
||||
command.push('powershell.exe');
|
||||
command.push('Start-Process "' + instance.pathExecute + '"');
|
||||
command.push('-WindowStyle hidden');
|
||||
command.push('-Verb runAs');
|
||||
command = command.join(' ');
|
||||
var child = Node.child.exec(command,
|
||||
function(error, stdout, stderr) {
|
||||
if (error) {
|
||||
if (/canceled by the user/i.test(error)) {
|
||||
end(PERMISSION_DENIED);
|
||||
} else {
|
||||
end(error);
|
||||
}
|
||||
} else {
|
||||
end();
|
||||
}
|
||||
}
|
||||
);
|
||||
child.stdin.end(); // Otherwise PowerShell waits indefinitely on Windows 7.
|
||||
}
|
||||
|
||||
function WindowsResult(instance, end) {
|
||||
Node.fs.readFile(instance.pathStatus, 'utf-8',
|
||||
function(error, code) {
|
||||
if (error) return end(error);
|
||||
Node.fs.readFile(instance.pathStdout, 'utf-8',
|
||||
function(error, stdout) {
|
||||
if (error) return end(error);
|
||||
Node.fs.readFile(instance.pathStderr, 'utf-8',
|
||||
function(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);
|
||||
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,
|
||||
function(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(
|
||||
function() {
|
||||
WindowsWaitForStatus(instance, end);
|
||||
},
|
||||
1000
|
||||
);
|
||||
} else if (error) {
|
||||
end(error);
|
||||
} else {
|
||||
end();
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function WindowsWriteCommandScript(instance, end) {
|
||||
var 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.'));
|
||||
}
|
||||
var script = [];
|
||||
script.push('@echo off');
|
||||
script.push('cd "' + cwd + '"');
|
||||
script.push(instance.command);
|
||||
script = script.join('\r\n');
|
||||
Node.fs.writeFile(instance.pathCommand, script, 'utf-8', end);
|
||||
}
|
||||
|
||||
function WindowsWriteElevateScript(instance, end) {
|
||||
end();
|
||||
// We do not use VBScript to elevate since it does not return an error if
|
||||
// the user does not grant permission. This is here for reference.
|
||||
// var script = [];
|
||||
// script.push('Set objShell = CreateObject("Shell.Application")');
|
||||
// script.push(
|
||||
// 'objShell.ShellExecute "' + instance.pathExecute + '", "", "", "runas", 0'
|
||||
// );
|
||||
// script = script.join('\r\n');
|
||||
// Node.fs.writeFile(instance.pathElevate, script, 'utf-8', end);
|
||||
}
|
||||
|
||||
function WindowsWriteExecuteScript(instance, end) {
|
||||
var 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);
|
||||
}
|
||||
|
||||
module.exports.exec = Exec;
|
||||
|
||||
// We used to expect that applet.app would be included with this module.
|
||||
|
|
BIN
linux.png
Normal file
BIN
linux.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 107 KiB |
BIN
osx.png
BIN
osx.png
Binary file not shown.
Before Width: | Height: | Size: 298 KiB After Width: | Height: | Size: 85 KiB |
|
@ -2,6 +2,7 @@ var sudo = require('./');
|
|||
var exec = require('child_process').exec;
|
||||
|
||||
function kill(end) {
|
||||
if (process.platform === 'win32') return end();
|
||||
exec('sudo -k', end);
|
||||
}
|
||||
kill(
|
||||
|
@ -9,7 +10,12 @@ kill(
|
|||
var options = {
|
||||
name: 'Sudo Prompt'
|
||||
};
|
||||
sudo.exec('sleep 10 && echo world', options,
|
||||
if (process.platform === 'win32') {
|
||||
var sleep = 'timeout /t 10\r\necho world';
|
||||
} else {
|
||||
var sleep = 'sleep 10 && echo world';
|
||||
}
|
||||
sudo.exec(sleep, options,
|
||||
function(error, stdout, stderr) {
|
||||
console.log(error, stdout, stderr);
|
||||
}
|
||||
|
|
16
test.js
16
test.js
|
@ -3,24 +3,32 @@ var sudo = require('./');
|
|||
var exec = require('child_process').exec;
|
||||
|
||||
function kill(end) {
|
||||
if (process.platform === 'win32') return end();
|
||||
exec('sudo -k', end);
|
||||
}
|
||||
|
||||
function icns() {
|
||||
if (process.platform !== 'darwin') return undefined;
|
||||
var path = '/Applications/Chess.app/Contents/Resources/Chess.icns';
|
||||
var path = '/Applications/Electron.app/Contents/Resources/Electron.icns';
|
||||
try {
|
||||
fs.statSync(path);
|
||||
return path;
|
||||
} catch (error) {}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
kill(
|
||||
function() {
|
||||
var options = {
|
||||
icns: icns(),
|
||||
name: 'Sudo Prompt'
|
||||
name: 'Electron'
|
||||
};
|
||||
var command = 'echo hello';
|
||||
if (process.platform === 'win32') {
|
||||
var expected = 'hello\r\n';
|
||||
} else {
|
||||
var expected = 'hello\n';
|
||||
}
|
||||
console.log('sudo.exec(' + JSON.stringify(command) + ', ' + JSON.stringify(options) + ')');
|
||||
sudo.exec(command, options,
|
||||
function(error, stdout, stderr) {
|
||||
|
@ -30,7 +38,9 @@ kill(
|
|||
kill(
|
||||
function() {
|
||||
if (error) throw error;
|
||||
if (stdout !== 'hello\n') throw new Error('stdout != "hello\n"');
|
||||
if (stdout !== expected) {
|
||||
throw new Error('stdout != ' + JSON.stringify(expected));
|
||||
}
|
||||
if (stderr !== "") throw new Error('stderr != ""');
|
||||
console.log('OK');
|
||||
}
|
||||
|
|
BIN
windows.png
Normal file
BIN
windows.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 82 KiB |
Loading…
Reference in a new issue