├── .gitignore ├── .npmignore ├── .travis.yml ├── Gruntfile.coffee ├── LICENSE.md ├── README.md ├── binding.gyp ├── package.json ├── spec └── runas-spec.coffee └── src ├── fork.cc ├── fork.h ├── main.cc ├── runas.coffee ├── runas.h ├── runas_darwin.cc ├── runas_posix.cc └── runas_win.cc /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /build 3 | /lib 4 | *.swp 5 | *.log 6 | *~ 7 | .node-version 8 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | /build 2 | /spec 3 | *.coffee 4 | *.log 5 | *~ 6 | .node-version 7 | .npmignore 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | git: 2 | depth: 10 3 | 4 | notifications: 5 | email: false 6 | 7 | sudo: false 8 | 9 | language: node_js 10 | 11 | node_js: 12 | - "0.12" 13 | - "0.10" 14 | - "iojs" 15 | 16 | env: 17 | - CC=clang CXX=clang++ npm_config_clang=1 18 | 19 | script: grunt 20 | -------------------------------------------------------------------------------- /Gruntfile.coffee: -------------------------------------------------------------------------------- 1 | module.exports = (grunt) -> 2 | grunt.initConfig 3 | pkg: grunt.file.readJSON('package.json') 4 | 5 | coffee: 6 | glob_to_multiple: 7 | expand: true 8 | cwd: 'src' 9 | src: ['*.coffee'] 10 | dest: 'lib' 11 | ext: '.js' 12 | 13 | cpplint: 14 | files: ['src/**/*.cc'] 15 | reporter: 'spec' 16 | verbosity: 1 17 | filters: 18 | build: 19 | include: false 20 | namespaces: false 21 | legal: 22 | copyright: false 23 | readability: 24 | braces: false 25 | 26 | shell: 27 | rebuild: 28 | command: 'npm build .' 29 | options: 30 | stdout: true 31 | stderr: true 32 | failOnError: true 33 | 34 | clean: 35 | command: 'rm -fr build lib' 36 | options: 37 | stdout: true 38 | stderr: true 39 | failOnError: true 40 | 41 | 42 | test: 43 | command: 'npm test' 44 | options: 45 | stdout: true 46 | stderr: true 47 | failOnError: true 48 | 49 | grunt.loadNpmTasks('grunt-contrib-coffee') 50 | grunt.loadNpmTasks('grunt-shell') 51 | grunt.loadNpmTasks('node-cpplint') 52 | grunt.registerTask('default', ['coffee', 'cpplint', 'shell:rebuild']) 53 | grunt.registerTask('test', ['default', 'shell:test']) 54 | grunt.registerTask('clean', ['shell:clean']) 55 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013 GitHub Inc. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Runas 2 | 3 | Run command synchronously with administrator privilege. 4 | 5 | ## Installing 6 | 7 | ```sh 8 | npm install runas 9 | ``` 10 | 11 | ## Building 12 | * Clone the repository 13 | * Run `npm install` 14 | * Run `grunt` to compile the native and CoffeeScript code 15 | * Run `grunt test` to run the specs 16 | 17 | ## Docs 18 | 19 | ```coffeescript 20 | runas = require 'runas' 21 | ``` 22 | 23 | ### runas(command, args[, options]) 24 | 25 | * `options` Object 26 | * `hide` Boolean - Hide the console window, `true` by default. 27 | * `admin` Boolean - Run command as administrator, `false` by default. 28 | * `catchOutput` Boolean - Catch the stdout and stderr of the command, `false` 29 | by default. 30 | * `stdin` String - String which would be passed as stdin input. 31 | 32 | Launches a new process with the given `command`, with command line arguments in 33 | `args`. 34 | 35 | This function is synchronous and returns the exit code when the `command` 36 | finished. 37 | 38 | When the `catchOutput` option is specified to `true`, an object that contains 39 | `exitCode`, `stdout` and `stderr` will be returned. 40 | 41 | ## Limitations 42 | 43 | * The `admin` option has only been implemented on Windows and OS X. 44 | * The `stdin` option has only been implemented on POSIX systems. 45 | * The `hide` option is only meaningful on Windows. 46 | * When `catchOutput` is `true`, 47 | * on Linux `exitCode`, `stdout` and `stderr` will be returned, 48 | * on OS X 49 | * if `admin` is `false`, `exitCode`, `stdout` and `stderr` will be returned, 50 | * if `admin` is `true`, `exitCode` and `stdout` will be returned, 51 | * on Windows only `exitCode` will be returned. 52 | -------------------------------------------------------------------------------- /binding.gyp: -------------------------------------------------------------------------------- 1 | { 2 | 'target_defaults': { 3 | 'win_delay_load_hook': 'false', 4 | 'conditions': [ 5 | ['OS=="win"', { 6 | 'msvs_disabled_warnings': [ 7 | 4530, # C++ exception handler used, but unwind semantics are not enabled 8 | 4506, # no definition for inline function 9 | ], 10 | }], 11 | ], 12 | }, 13 | 'targets': [ 14 | { 15 | 'target_name': 'runas', 16 | 'sources': [ 17 | 'src/main.cc', 18 | ], 19 | 'include_dirs': [ 20 | ' 4 | #include 5 | #include 6 | 7 | #include 8 | #include 9 | #include 10 | 11 | namespace runas { 12 | 13 | namespace { 14 | 15 | void child(int* stdin_fds, 16 | int* stdout_fds, 17 | int* stderr_fds, 18 | const std::string& command, 19 | const std::vector& args) { 20 | // Redirect stdin to the pipe. 21 | close(stdin_fds[1]); 22 | dup2(stdin_fds[0], 0); 23 | close(stdin_fds[0]); 24 | 25 | // Redirect stdout to the pipe. 26 | close(stdout_fds[0]); 27 | dup2(stdout_fds[1], 1); 28 | close(stdout_fds[1]); 29 | 30 | // Redirect stderr to the pipe. 31 | close(stderr_fds[0]); 32 | dup2(stderr_fds[1], 2); 33 | close(stderr_fds[1]); 34 | 35 | std::vector argv(StringVectorToCharStarVector(args)); 36 | argv.insert(argv.begin(), const_cast(command.c_str())); 37 | 38 | execvp(command.c_str(), &argv[0]); 39 | perror("execvp()"); 40 | exit(127); 41 | } 42 | 43 | int parent(int pid, 44 | int* stdin_fds, const std::string& std_input, 45 | int* stdout_fds, std::string* std_output, 46 | int* stderr_fds, std::string* std_error) { 47 | // Write string to child's stdin. 48 | int want = std_input.size(); 49 | if (want > 0) { 50 | const char* p = std_input.data(); 51 | close(stdin_fds[0]); 52 | while (want > 0) { 53 | int r = write(stdin_fds[1], p, want); 54 | if (r > 0) { 55 | want -= r; 56 | p += r; 57 | } else if (errno != EAGAIN && errno != EINTR) { 58 | break; 59 | } 60 | } 61 | close(stdin_fds[1]); 62 | } 63 | 64 | // Read from child's stdout 65 | close(stdout_fds[1]); 66 | if (std_output) { 67 | char buffer[512]; 68 | while (true) { 69 | int r = read(stdout_fds[0], buffer, 512); 70 | if (r > 0) 71 | std_output->append(buffer, r); 72 | else if (errno != EAGAIN && errno != EINTR) 73 | break; 74 | } 75 | } 76 | close(stdout_fds[0]); 77 | 78 | // Read from child's stderr 79 | close(stderr_fds[1]); 80 | if (std_error) { 81 | char buffer[512]; 82 | while (true) { 83 | int r = read(stderr_fds[0], buffer, 512); 84 | if (r > 0) 85 | std_error->append(buffer, r); 86 | else if (errno != EAGAIN && errno != EINTR) 87 | break; 88 | } 89 | } 90 | close(stderr_fds[0]); 91 | 92 | // Wait for child. 93 | int r, status; 94 | do { 95 | r = waitpid(pid, &status, 0); 96 | } while (r == -1 && errno == EINTR); 97 | 98 | if (r == -1 || !WIFEXITED(status)) 99 | return -1; 100 | 101 | return WEXITSTATUS(status); 102 | } 103 | 104 | } // namespace 105 | 106 | 107 | std::vector StringVectorToCharStarVector( 108 | const std::vector& args) { 109 | std::vector argv(1 + args.size(), NULL); 110 | for (size_t i = 0; i < args.size(); ++i) 111 | argv[i] = const_cast(args[i].c_str()); 112 | return argv; 113 | } 114 | 115 | bool Fork(const std::string& command, 116 | const std::vector& args, 117 | const std::string& std_input, 118 | std::string* std_output, 119 | std::string* std_error, 120 | int options, 121 | int* exit_code) { 122 | int stdin_fds[2]; 123 | if (pipe(stdin_fds) == -1) 124 | return false; 125 | 126 | int stdout_fds[2]; 127 | if (pipe(stdout_fds) == -1) 128 | return false; 129 | 130 | int stderr_fds[2]; 131 | if (pipe(stderr_fds) == -1) 132 | return false; 133 | 134 | // execvp 135 | int pid = fork(); 136 | switch (pid) { 137 | case 0: // child 138 | child(stdin_fds, stdout_fds, stderr_fds, command, args); 139 | break; 140 | 141 | case -1: // error 142 | return false; 143 | 144 | default: // parent 145 | *exit_code = parent(pid, 146 | stdin_fds, std_input, 147 | stdout_fds, std_output, 148 | stderr_fds, std_error); 149 | return true; 150 | }; 151 | 152 | return false; 153 | } 154 | 155 | } // namespace runas 156 | -------------------------------------------------------------------------------- /src/fork.h: -------------------------------------------------------------------------------- 1 | #ifndef SRC_FORK_H_ 2 | #define SRC_FORK_H_ 3 | 4 | #include 5 | #include 6 | 7 | namespace runas { 8 | 9 | std::vector StringVectorToCharStarVector( 10 | const std::vector& args); 11 | 12 | bool Fork(const std::string& command, 13 | const std::vector& args, 14 | const std::string& std_input, 15 | std::string* std_output, 16 | std::string* std_error, 17 | int options, 18 | int* exit_code); 19 | 20 | } // namespace runas 21 | 22 | #endif // SRC_FORK_H_ 23 | -------------------------------------------------------------------------------- /src/main.cc: -------------------------------------------------------------------------------- 1 | #include "nan.h" 2 | using namespace v8; 3 | 4 | #include "runas.h" 5 | 6 | namespace { 7 | 8 | inline 9 | bool GetProperty(Local obj, const char* key, Local* value) { 10 | return Nan::Get(obj, Nan::New(key).ToLocalChecked()).ToLocal(value); 11 | } 12 | 13 | void Runas(const Nan::FunctionCallbackInfo& info) { 14 | if (!info[0]->IsString() || !info[1]->IsArray() || !info[2]->IsObject()) { 15 | Nan::ThrowTypeError("Bad argument"); 16 | return; 17 | } 18 | 19 | std::string command(*String::Utf8Value(info[0])); 20 | std::vector c_args; 21 | 22 | Local v_args = Local::Cast(info[1]); 23 | uint32_t length = v_args->Length(); 24 | 25 | c_args.reserve(length); 26 | for (uint32_t i = 0; i < length; ++i) { 27 | std::string arg(*String::Utf8Value(v_args->Get(i))); 28 | c_args.push_back(arg); 29 | } 30 | 31 | Local v_value; 32 | Local v_options = info[2]->ToObject(); 33 | int options = runas::OPTION_NONE; 34 | if (GetProperty(v_options, "hide", &v_value) && v_value->BooleanValue()) 35 | options |= runas::OPTION_HIDE; 36 | if (GetProperty(v_options, "admin", &v_value) && v_value->BooleanValue()) 37 | options |= runas::OPTION_ADMIN; 38 | 39 | std::string std_input; 40 | if (GetProperty(v_options, "stdin", &v_value) && v_value->IsString()) 41 | std_input = *String::Utf8Value(v_value); 42 | 43 | std::string std_output, std_error; 44 | bool catch_output = GetProperty(v_options, "catchOutput", &v_value) && 45 | v_value->BooleanValue(); 46 | if (catch_output) 47 | options |= runas::OPTION_CATCH_OUTPUT; 48 | 49 | int code = -1; 50 | runas::Runas(command, c_args, std_input, &std_output, &std_error, options, 51 | &code); 52 | 53 | if (catch_output) { 54 | Local result = Nan::New(); 55 | Nan::Set(result, 56 | Nan::New("exitCode").ToLocalChecked(), 57 | Nan::New(code)); 58 | Nan::Set(result, 59 | Nan::New("stdout").ToLocalChecked(), 60 | Nan::New(std_output).ToLocalChecked()); 61 | Nan::Set(result, 62 | Nan::New("stderr").ToLocalChecked(), 63 | Nan::New(std_error).ToLocalChecked()); 64 | info.GetReturnValue().Set(result); 65 | } else { 66 | info.GetReturnValue().Set(Nan::New(code)); 67 | } 68 | } 69 | 70 | void Init(Handle exports) { 71 | Nan::SetMethod(exports, "runas", Runas); 72 | } 73 | 74 | } // namespace 75 | 76 | NODE_MODULE(runas, Init) 77 | -------------------------------------------------------------------------------- /src/runas.coffee: -------------------------------------------------------------------------------- 1 | runas = require('../build/Release/runas.node') 2 | 3 | searchCommand = (command) -> 4 | return command if command[0] is '/' 5 | 6 | fs = require('fs') 7 | path = require('path') 8 | paths = process.env.PATH.split(path.delimiter) 9 | for p in paths 10 | try 11 | filename = path.join(p, command) 12 | return filename if fs.statSync(filename).isFile() 13 | catch e 14 | '' 15 | 16 | module.exports = (command, args=[], options={}) -> 17 | options.hide ?= true 18 | options.admin ?= false 19 | 20 | # Convert command to its full path when using authorization service 21 | if process.platform is 'darwin' and options.admin is true 22 | command = searchCommand(command) 23 | 24 | runas.runas(command, args, options) 25 | -------------------------------------------------------------------------------- /src/runas.h: -------------------------------------------------------------------------------- 1 | #ifndef SRC_RUNAS_H_ 2 | #define SRC_RUNAS_H_ 3 | 4 | #include 5 | #include 6 | 7 | namespace runas { 8 | 9 | enum Options { 10 | OPTION_NONE = 0, 11 | // Hide the command's window. 12 | OPTION_HIDE = 1 << 1, 13 | // Run as administrator. 14 | OPTION_ADMIN = 1 << 2, 15 | // Catch the output. 16 | OPTION_CATCH_OUTPUT = 1 << 3, 17 | }; 18 | 19 | bool Runas(const std::string& command, 20 | const std::vector& args, 21 | const std::string& std_input, 22 | std::string* std_output, 23 | std::string* std_error, 24 | int options, 25 | int* exit_code); 26 | 27 | } // namespace runas 28 | 29 | #endif // SRC_RUNAS_H_ 30 | -------------------------------------------------------------------------------- /src/runas_darwin.cc: -------------------------------------------------------------------------------- 1 | #include "runas.h" 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | #include "fork.h" 9 | 10 | namespace runas { 11 | 12 | namespace { 13 | 14 | AuthorizationRef g_auth = NULL; 15 | 16 | OSStatus ExecuteWithPrivileges(AuthorizationRef authorization, 17 | const std::string& command, 18 | AuthorizationFlags options, 19 | char* const* arguments, 20 | FILE** pipe) { 21 | #pragma clang diagnostic push 22 | #pragma clang diagnostic ignored "-Wdeprecated-declarations" 23 | return AuthorizationExecuteWithPrivileges(authorization, 24 | command.c_str(), 25 | options, 26 | arguments, 27 | pipe); 28 | #pragma clang diagnostic pop 29 | } 30 | 31 | } // namespace 32 | 33 | bool Runas(const std::string& command, 34 | const std::vector& args, 35 | const std::string& std_input, 36 | std::string* std_output, 37 | std::string* std_error, 38 | int options, 39 | int* exit_code) { 40 | // Use fork when "admin" is false. 41 | if (!(options & OPTION_ADMIN)) 42 | return Fork(command, args, std_input, std_output, std_error, options, 43 | exit_code); 44 | 45 | if (!g_auth && AuthorizationCreate(NULL, 46 | kAuthorizationEmptyEnvironment, 47 | kAuthorizationFlagDefaults, 48 | &g_auth) != errAuthorizationSuccess) 49 | return false; 50 | 51 | 52 | FILE* pipe; 53 | std::vector argv(StringVectorToCharStarVector(args)); 54 | if (ExecuteWithPrivileges(g_auth, 55 | command, 56 | kAuthorizationFlagDefaults, 57 | &argv[0], 58 | &pipe) != errAuthorizationSuccess) 59 | return false; 60 | 61 | int pid = fcntl(fileno(pipe), F_GETOWN, 0); 62 | 63 | // Write to stdin. 64 | size_t want = std_input.size(); 65 | if (want > 0) { 66 | const char*p = &std_input[0]; 67 | while (true) { 68 | size_t r = fwrite(p, sizeof(*p), want, pipe); 69 | if (r == 0) 70 | break; 71 | want -= r; 72 | p += r; 73 | } 74 | } 75 | 76 | // Read from stdout. 77 | if (std_output && (options & OPTION_CATCH_OUTPUT)) { 78 | char buffer[512]; 79 | while (true) { 80 | size_t r = fread(buffer, sizeof(buffer[0]), 512, pipe); 81 | if (r == 0) 82 | break; 83 | std_output->append(buffer, r); 84 | } 85 | } 86 | 87 | fclose(pipe); 88 | 89 | int r, status; 90 | do { 91 | r = waitpid(pid, &status, 0); 92 | } while (r == -1 && errno == EINTR); 93 | 94 | if (r == -1 || !WIFEXITED(status)) 95 | return false; 96 | 97 | *exit_code = WEXITSTATUS(status); 98 | return true; 99 | } 100 | 101 | } // namespace runas 102 | -------------------------------------------------------------------------------- /src/runas_posix.cc: -------------------------------------------------------------------------------- 1 | #include "runas.h" 2 | 3 | #include "fork.h" 4 | 5 | namespace runas { 6 | 7 | bool Runas(const std::string& command, 8 | const std::vector& args, 9 | const std::string& std_input, 10 | std::string* std_output, 11 | std::string* std_error, 12 | int options, 13 | int* exit_code) { 14 | return Fork(command, args, std_input, std_output, std_error, options, 15 | exit_code); 16 | } 17 | 18 | } // namespace runas 19 | -------------------------------------------------------------------------------- /src/runas_win.cc: -------------------------------------------------------------------------------- 1 | #include "runas.h" 2 | 3 | #include 4 | 5 | namespace runas { 6 | 7 | std::string QuoteCmdArg(const std::string& arg) { 8 | if (arg.size() == 0) 9 | return arg; 10 | 11 | // No quotation needed. 12 | if (arg.find_first_of(" \t\"") == std::string::npos) 13 | return arg; 14 | 15 | // No embedded double quotes or backlashes, just wrap quote marks around 16 | // the whole thing. 17 | if (arg.find_first_of("\"\\") == std::string::npos) 18 | return std::string("\"") + arg + '"'; 19 | 20 | // Expected input/output: 21 | // input : hello"world 22 | // output: "hello\"world" 23 | // input : hello""world 24 | // output: "hello\"\"world" 25 | // input : hello\world 26 | // output: hello\world 27 | // input : hello\\world 28 | // output: hello\\world 29 | // input : hello\"world 30 | // output: "hello\\\"world" 31 | // input : hello\\"world 32 | // output: "hello\\\\\"world" 33 | // input : hello world\ 34 | // output: "hello world\" 35 | std::string quoted; 36 | bool quote_hit = true; 37 | for (size_t i = arg.size(); i > 0; --i) { 38 | quoted.push_back(arg[i - 1]); 39 | 40 | if (quote_hit && arg[i - 1] == '\\') { 41 | quoted.push_back('\\'); 42 | } else if (arg[i - 1] == '"') { 43 | quote_hit = true; 44 | quoted.push_back('\\'); 45 | } else { 46 | quote_hit = false; 47 | } 48 | } 49 | 50 | return std::string("\"") + std::string(quoted.rbegin(), quoted.rend()) + '"'; 51 | } 52 | 53 | bool Runas(const std::string& command, 54 | const std::vector& args, 55 | const std::string& std_input, 56 | std::string* std_output, 57 | std::string* std_error, 58 | int options, 59 | int* exit_code) { 60 | CoInitializeEx(NULL, COINIT_APARTMENTTHREADED | COINIT_DISABLE_OLE1DDE); 61 | 62 | std::string parameters; 63 | for (size_t i = 0; i < args.size(); ++i) 64 | parameters += QuoteCmdArg(args[i]) + ' '; 65 | 66 | SHELLEXECUTEINFO sei = { sizeof(sei) }; 67 | sei.fMask = SEE_MASK_NOASYNC | SEE_MASK_NOCLOSEPROCESS; 68 | sei.lpVerb = (options & OPTION_ADMIN) ? "runas" : "open"; 69 | sei.lpFile = command.c_str(); 70 | sei.lpParameters = parameters.c_str(); 71 | sei.nShow = SW_NORMAL; 72 | 73 | if (options & OPTION_HIDE) 74 | sei.nShow = SW_HIDE; 75 | 76 | if (::ShellExecuteEx(&sei) == FALSE || sei.hProcess == NULL) 77 | return false; 78 | 79 | // Wait for the process to complete. 80 | ::WaitForSingleObject(sei.hProcess, INFINITE); 81 | 82 | DWORD code; 83 | if (::GetExitCodeProcess(sei.hProcess, &code) == 0) 84 | return false; 85 | 86 | *exit_code = code; 87 | return true; 88 | } 89 | 90 | } // namespace runas 91 | --------------------------------------------------------------------------------