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
|
# 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
|
## Installation
|
||||||
|
`sudo-prompt` has no external dependencies and does not require any native bindings.
|
||||||
```
|
```
|
||||||
npm install sudo-prompt
|
npm install sudo-prompt
|
||||||
```
|
```
|
||||||
|
@ -18,8 +22,8 @@ Note: Your command should not start with the `sudo` prefix.
|
||||||
```
|
```
|
||||||
var sudo = require('sudo-prompt');
|
var sudo = require('sudo-prompt');
|
||||||
var options = {
|
var options = {
|
||||||
name: 'Ronomon',
|
name: 'Electron',
|
||||||
icns: '/path/to/icns/file', // (optional)
|
icns: '/Applications/Electron.app/Contents/Resources/Electron.icns', // (optional)
|
||||||
};
|
};
|
||||||
sudo.exec('echo hello', options, function(error, stdout, stderr) {});
|
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 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
|
## 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).
|
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.
|
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
|
## 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
|
```sh
|
||||||
$ sudo -k
|
$ sudo -k
|
||||||
|
|
212
index.js
212
index.js
|
@ -9,6 +9,8 @@ var Node = {
|
||||||
};
|
};
|
||||||
|
|
||||||
function Attempt(instance, end) {
|
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
|
// 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.
|
// a password. If a password is required, sudo will return an error and exit.
|
||||||
var command = [];
|
var command = [];
|
||||||
|
@ -23,7 +25,6 @@ function Attempt(instance, end) {
|
||||||
Node.child.exec(command,
|
Node.child.exec(command,
|
||||||
function(error, stdout, stderr) {
|
function(error, stdout, stderr) {
|
||||||
if (/sudo: /i.test(stderr)) {
|
if (/sudo: /i.test(stderr)) {
|
||||||
var platform = Node.process.platform;
|
|
||||||
if (platform === 'linux') {
|
if (platform === 'linux') {
|
||||||
return Linux(instance, end);
|
return Linux(instance, end);
|
||||||
} else if (platform === 'darwin') {
|
} else if (platform === 'darwin') {
|
||||||
|
@ -97,7 +98,7 @@ function Exec() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
var platform = Node.process.platform;
|
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.'));
|
return end(new Error('Platform not yet supported.'));
|
||||||
}
|
}
|
||||||
var instance = {
|
var instance = {
|
||||||
|
@ -169,10 +170,6 @@ function Mac(instance, callback) {
|
||||||
UUID(instance,
|
UUID(instance,
|
||||||
function(error, uuid) {
|
function(error, uuid) {
|
||||||
if (error) return callback(error);
|
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.uuid = uuid;
|
||||||
instance.path = Node.path.join(
|
instance.path = Node.path.join(
|
||||||
temp,
|
temp,
|
||||||
|
@ -184,7 +181,7 @@ function Mac(instance, callback) {
|
||||||
function(errorRemove) {
|
function(errorRemove) {
|
||||||
if (error) return callback(error);
|
if (error) return callback(error);
|
||||||
if (errorRemove) return callback(errorRemove);
|
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) {
|
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 = [];
|
var command = [];
|
||||||
|
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('/bin/rm');
|
||||||
command.push('-rf');
|
command.push('-rf');
|
||||||
command.push('"' + EscapeDoubleQuotes(Node.path.normalize(path)) + '"');
|
command.push('"' + EscapeDoubleQuotes(Node.path.normalize(path)) + '"');
|
||||||
|
}
|
||||||
command = command.join(' ');
|
command = command.join(' ');
|
||||||
Node.child.exec(command, end);
|
Node.child.exec(command, end);
|
||||||
}
|
}
|
||||||
|
@ -356,7 +362,12 @@ function UUID(instance, end) {
|
||||||
hash.update(instance.options.name);
|
hash.update(instance.options.name);
|
||||||
hash.update(instance.command);
|
hash.update(instance.command);
|
||||||
hash.update(random);
|
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;
|
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;
|
module.exports.exec = Exec;
|
||||||
|
|
||||||
// We used to expect that applet.app would be included with this module.
|
// 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;
|
var exec = require('child_process').exec;
|
||||||
|
|
||||||
function kill(end) {
|
function kill(end) {
|
||||||
|
if (process.platform === 'win32') return end();
|
||||||
exec('sudo -k', end);
|
exec('sudo -k', end);
|
||||||
}
|
}
|
||||||
kill(
|
kill(
|
||||||
|
@ -9,7 +10,12 @@ kill(
|
||||||
var options = {
|
var options = {
|
||||||
name: 'Sudo Prompt'
|
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) {
|
function(error, stdout, stderr) {
|
||||||
console.log(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;
|
var exec = require('child_process').exec;
|
||||||
|
|
||||||
function kill(end) {
|
function kill(end) {
|
||||||
|
if (process.platform === 'win32') return end();
|
||||||
exec('sudo -k', end);
|
exec('sudo -k', end);
|
||||||
}
|
}
|
||||||
|
|
||||||
function icns() {
|
function icns() {
|
||||||
if (process.platform !== 'darwin') return undefined;
|
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 {
|
try {
|
||||||
fs.statSync(path);
|
fs.statSync(path);
|
||||||
return path;
|
return path;
|
||||||
} catch (error) {}
|
} catch (error) {}
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
kill(
|
kill(
|
||||||
function() {
|
function() {
|
||||||
var options = {
|
var options = {
|
||||||
icns: icns(),
|
icns: icns(),
|
||||||
name: 'Sudo Prompt'
|
name: 'Electron'
|
||||||
};
|
};
|
||||||
var command = 'echo hello';
|
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) + ')');
|
console.log('sudo.exec(' + JSON.stringify(command) + ', ' + JSON.stringify(options) + ')');
|
||||||
sudo.exec(command, options,
|
sudo.exec(command, options,
|
||||||
function(error, stdout, stderr) {
|
function(error, stdout, stderr) {
|
||||||
|
@ -30,7 +38,9 @@ kill(
|
||||||
kill(
|
kill(
|
||||||
function() {
|
function() {
|
||||||
if (error) throw error;
|
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 != ""');
|
if (stderr !== "") throw new Error('stderr != ""');
|
||||||
console.log('OK');
|
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