Elevated Commands Redesign (#1123)

This commit is contained in:
Chase Payne 2023-04-29 00:22:01 -05:00 committed by GitHub
parent 18ab7dcf6c
commit 430a439698
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 568 additions and 425 deletions

View File

@ -639,6 +639,8 @@ if(WIN32)
set_target_properties(sunshine PROPERTIES LINK_SEARCH_START_STATIC 1)
set(CMAKE_FIND_LIBRARY_SUFFIXES ".dll")
find_library(ZLIB ZLIB1)
list(APPEND SUNSHINE_EXTERNAL_LIBRARIES
Wtsapi32.lib)
endif()
target_link_libraries(sunshine ${SUNSHINE_EXTERNAL_LIBRARIES} ${EXTRA_LIBS})
@ -694,7 +696,6 @@ if(WIN32) # see options at: https://cmake.org/cmake/help/latest/cpack_gen/nsis.h
install(TARGETS dxgi-info RUNTIME DESTINATION "tools" COMPONENT dxgi)
install(TARGETS audio-info RUNTIME DESTINATION "tools" COMPONENT audio)
install(TARGETS sunshinesvc RUNTIME DESTINATION "tools" COMPONENT sunshinesvc)
install(TARGETS elevator RUNTIME DESTINATION "tools" COMPONENT elevator)
# Mandatory tools
install(TARGETS ddprobe RUNTIME DESTINATION "tools" COMPONENT application)
@ -730,15 +731,16 @@ if(WIN32) # see options at: https://cmake.org/cmake/help/latest/cpack_gen/nsis.h
# Extra install commands
# Restores permissions on the install directory
# Migrates config files from the root into the new config folder
# Sets permissions on the config folder so that we can write in it
# Install service
SET(CPACK_NSIS_EXTRA_INSTALL_COMMANDS
"${CPACK_NSIS_EXTRA_INSTALL_COMMANDS}
IfSilent +2 0
ExecShell 'open' 'https://sunshinestream.readthedocs.io/'
nsExec::ExecToLog 'icacls \\\"$INSTDIR\\\" /reset'
nsExec::ExecToLog 'icacls \\\"$INSTDIR\\\" /reset /T'
nsExec::ExecToLog 'icacls \\\"$INSTDIR\\\\config\\\\credentials\\\" /inheritance:r'
nsExec::ExecToLog 'icacls \\\"$INSTDIR\\\\config\\\\credentials\\\" \
/grant:r Administrators:\\\(OI\\\)\\\(CI\\\)\\\(F\\\)'
nsExec::ExecToLog '\\\"$INSTDIR\\\\scripts\\\\migrate-config.bat\\\"'
nsExec::ExecToLog 'icacls \\\"$INSTDIR\\\\config\\\" /grant:r Users:\\\(OI\\\)\\\(CI\\\)\\\(F\\\)'
nsExec::ExecToLog '\\\"$INSTDIR\\\\scripts\\\\add-firewall-rule.bat\\\"'
nsExec::ExecToLog '\\\"$INSTDIR\\\\scripts\\\\install-service.bat\\\"'
nsExec::ExecToLog '\\\"$INSTDIR\\\\scripts\\\\install-vigembus.bat\\\"'
@ -763,21 +765,6 @@ if(WIN32) # see options at: https://cmake.org/cmake/help/latest/cpack_gen/nsis.h
NoDelete:
")
# Adding an option for the start menu and PATH
# TODO: it asks to add it to the PATH but is not working https://gitlab.kitware.com/cmake/cmake/-/issues/15635
set(CPACK_NSIS_MODIFY_PATH "OFF")
set(CPACK_NSIS_EXECUTABLES_DIRECTORY ".")
# This will be shown on the installed apps Windows settings
set(CPACK_NSIS_INSTALLED_ICON_NAME "${CMAKE_PROJECT_NAME}.exe")
set(CPACK_NSIS_CREATE_ICONS_EXTRA
"${CPACK_NSIS_CREATE_ICONS_EXTRA}
CreateShortCut '\$SMPROGRAMS\\\\$STARTMENU_FOLDER\\\\${CMAKE_PROJECT_NAME} (Foreground Mode).lnk' \
'\$INSTDIR\\\\${CMAKE_PROJECT_NAME}.exe'
")
set(CPACK_NSIS_DELETE_ICONS_EXTRA
"${CPACK_NSIS_DELETE_ICONS_EXTRA}
Delete '\$SMPROGRAMS\\\\$MUI_TEMP\\\\${CMAKE_PROJECT_NAME} (Foreground Mode).lnk'
")
# Checking for previous installed versions
set(CPACK_NSIS_ENABLE_UNINSTALL_BEFORE_INSTALL "ON")
@ -811,12 +798,6 @@ if(WIN32) # see options at: https://cmake.org/cmake/help/latest/cpack_gen/nsis.h
set(CPACK_COMPONENT_AUDIO_DESCRIPTION "CLI tool providing information about sound devices.")
set(CPACK_COMPONENT_AUDIO_GROUP "tools")
# elevation tool
set(CPACK_COMPONENT_ELEVATOR_DISPLAY_NAME "elevator")
set(CPACK_COMPONENT_ELEVATOR_DESCRIPTION "CLI tool that assists with elevating \
commands when permissions have been denied.")
set(CPACK_COMPONENT_ELEVATOR_GROUP "tools")
# display tool
set(CPACK_COMPONENT_DXGI_DISPLAY_NAME "dxgi-info")
set(CPACK_COMPONENT_DXGI_DESCRIPTION "CLI tool providing information about graphics cards and displays.")
@ -824,14 +805,19 @@ if(WIN32) # see options at: https://cmake.org/cmake/help/latest/cpack_gen/nsis.h
# service
set(CPACK_COMPONENT_SUNSHINESVC_DISPLAY_NAME "sunshinesvc")
set(CPACK_COMPONENT_SUNSHINESVC_DESCRIPTION "CLI tool providing ability to enable/disable the Sunshine service.")
set(CPACK_COMPONENT_SUNSHINESVC_DESCRIPTION "Installs sunshine as a service as SYSTEM, \
so that it can allow usage before logon and other benefits.")
set(CPACK_COMPONENT_SUNSHINESVC_GROUP "tools")
# This is required because we've changed the ACL for config to require admin to protect against EoP exploits.
set(CPACK_COMPONENT_SUNSHINESVC_REQUIRED true)
# service scripts
set(CPACK_COMPONENT_SERVICE_DISPLAY_NAME "service-scripts")
set(CPACK_COMPONENT_SERVICE_DESCRIPTION "Scripts to enable/disable the service.")
set(CPACK_COMPONENT_SERVICE_DESCRIPTION "Scripts for installing and enabling the service.")
set(CPACK_COMPONENT_SERVICE_GROUP "scripts")
set(CPACK_COMPONENT_SERVICE_DEPENDS sunshinesvc)
# This is required because we've changed the ACL for config to require admin to protect against EoP exploits.
set(CPACK_COMPONENT_SERVICE_REQUIRED true)
# firewall scripts
set(CPACK_COMPONENT_FIREWALL_DISPLAY_NAME "firewall-scripts")

View File

@ -187,3 +187,32 @@ Changing Resolution and Refresh Rate (Windows)
.. Tip:: You can change your host resolution to match the client resolution automatically using the
`Nonary/ResolutionAutomation <https://github.com/Nonary/ResolutionAutomation/>`_ project.
Elevating Commands (Windows)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
If you've installed Sunshine as a service (default), you can now specify if a command should be elevated with adminsitrative privileges.
Simply enable the elevated option in the WEB UI, or add it to the JSON configuration.
This is an option for both prep-cmd and regular commands and will launch the process with the current user without a UAC prompt.
.. Note:: It's important to write the values "true" and "false" as string values, not as the typical true/false values in most JSON.
**Example**
.. code-block:: json
{
"name": "Game With AntiCheat that Requires Admin",
"output": "",
"cmd": "ping 127.0.0.1",
"exclude-global-prep-cmd": "false",
"elevated": "true",
"prep-cmd": [
{
"do": "powershell.exe -command \"Start-Streaming\"",
"undo": "powershell.exe -command \"Stop-Streaming\"",
"elevated": "false"
}
],
"image-path": ""
}

View File

@ -816,12 +816,14 @@ namespace config {
boost::property_tree::read_json(jsonStream, jsonTree);
for (auto &[_, prep_cmd] : jsonTree.get_child("prep_cmd"s)) {
auto do_cmd = prep_cmd.get<std::string>("do"s);
auto undo_cmd = prep_cmd.get<std::string>("undo"s);
auto do_cmd = prep_cmd.get_optional<std::string>("do"s);
auto undo_cmd = prep_cmd.get_optional<std::string>("undo"s);
auto elevated = prep_cmd.get_optional<bool>("elevated"s);
input.emplace_back(
std::move(do_cmd),
std::move(undo_cmd));
std::move(do_cmd.value_or("")),
std::move(undo_cmd.value_or("")),
std::move(elevated.value_or(false)));
}
}

View File

@ -119,14 +119,14 @@ namespace config {
}
struct prep_cmd_t {
prep_cmd_t(std::string &&do_cmd, std::string &&undo_cmd):
do_cmd(std::move(do_cmd)), undo_cmd(std::move(undo_cmd)) {}
explicit prep_cmd_t(std::string &&do_cmd):
do_cmd(std::move(do_cmd)) {}
prep_cmd_t(std::string &&do_cmd, std::string &&undo_cmd, bool &&elevated):
do_cmd(std::move(do_cmd)), undo_cmd(std::move(undo_cmd)), elevated(std::move(elevated)) {}
explicit prep_cmd_t(std::string &&do_cmd, bool &&elevated):
do_cmd(std::move(do_cmd)), elevated(std::move(elevated)) {}
std::string do_cmd;
std::string undo_cmd;
bool elevated;
};
struct sunshine_t {
int min_log_level;
std::bitset<flag::FLAG_SIZE> flags;

View File

@ -89,7 +89,7 @@ namespace http {
pt::write_json(file, outputTree);
}
catch (std::exception &e) {
BOOST_LOG(error) << "generating user credentials: "sv << e.what();
BOOST_LOG(error) << "error writing to the credentials file, perhaps try this again as an administrator? Details: "sv << e.what();
return -1;
}

View File

@ -382,7 +382,7 @@ namespace platf {
display_names(mem_type_e hwdevice_type);
boost::process::child
run_unprivileged(const std::string &cmd, boost::filesystem::path &working_dir, boost::process::environment &env, FILE *file, std::error_code &ec, boost::process::group *group);
run_command(bool elevated, const std::string &cmd, boost::filesystem::path &working_dir, boost::process::environment &env, FILE *file, std::error_code &ec, boost::process::group *group);
enum class thread_priority_e : int {
low,

View File

@ -159,8 +159,7 @@ namespace platf {
}
bp::child
run_unprivileged(const std::string &cmd, boost::filesystem::path &working_dir, bp::environment &env, FILE *file, std::error_code &ec, bp::group *group) {
BOOST_LOG(warning) << "run_unprivileged() is not yet implemented for this platform. The new process will run with Sunshine's permissions."sv;
run_command(bool elevated, const std::string &cmd, boost::filesystem::path &working_dir, bp::environment &env, FILE *file, std::error_code &ec, bp::group *group) {
if (!group) {
if (!file) {
return bp::child(cmd, env, bp::start_dir(working_dir), bp::std_out > bp::null, bp::std_err > bp::null, ec);

View File

@ -158,8 +158,7 @@ namespace platf {
}
bp::child
run_unprivileged(const std::string &cmd, boost::filesystem::path &working_dir, bp::environment &env, FILE *file, std::error_code &ec, bp::group *group) {
BOOST_LOG(warning) << "run_unprivileged() is not yet implemented for this platform. The new process will run with Sunshine's permissions."sv;
run_command(bool elevated, const std::string &cmd, boost::filesystem::path &working_dir, bp::environment &env, FILE *file, std::error_code &ec, bp::group *group) {
if (!group) {
if (!file) {
return bp::child(cmd, env, bp::start_dir(working_dir), bp::std_out > bp::null, bp::std_err > bp::null, ec);

View File

@ -19,6 +19,8 @@
#include <winuser.h>
#include <wlanapi.h>
#include <ws2tcpip.h>
#include <wtsapi32.h>
#include <sddl.h>
// clang-format on
#include "src/main.h"
@ -200,103 +202,99 @@ namespace platf {
return std::string(buffer, bytes);
}
HANDLE
duplicate_shell_token() {
// Get the shell window (will usually be owned by explorer.exe)
HWND shell_window = GetShellWindow();
if (!shell_window) {
BOOST_LOG(error) << "No shell window found. Is explorer.exe running?"sv;
return NULL;
}
// Open a handle to the explorer.exe process
DWORD shell_pid;
GetWindowThreadProcessId(shell_window, &shell_pid);
HANDLE shell_process = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, shell_pid);
if (!shell_process) {
BOOST_LOG(error) << "Failed to open shell process: "sv << GetLastError();
return NULL;
}
// Open explorer's token to clone for process creation
HANDLE shell_token;
BOOL ret = OpenProcessToken(shell_process, TOKEN_DUPLICATE, &shell_token);
CloseHandle(shell_process);
if (!ret) {
BOOST_LOG(error) << "Failed to open shell process token: "sv << GetLastError();
return NULL;
}
// Duplicate the token to make it usable for process creation
HANDLE new_token;
ret = DuplicateTokenEx(shell_token, TOKEN_ALL_ACCESS, NULL, SecurityImpersonation, TokenPrimary, &new_token);
CloseHandle(shell_token);
if (!ret) {
BOOST_LOG(error) << "Failed to duplicate shell process token: "sv << GetLastError();
return NULL;
}
return new_token;
}
PTOKEN_USER
get_token_user(HANDLE token) {
DWORD return_length;
if (GetTokenInformation(token, TokenUser, NULL, 0, &return_length) || GetLastError() != ERROR_INSUFFICIENT_BUFFER) {
auto winerr = GetLastError();
BOOST_LOG(error) << "Failed to get token information size: "sv << winerr;
return nullptr;
}
auto user = (PTOKEN_USER) HeapAlloc(GetProcessHeap(), 0, return_length);
if (!user) {
return nullptr;
}
if (!GetTokenInformation(token, TokenUser, user, return_length, &return_length)) {
auto winerr = GetLastError();
BOOST_LOG(error) << "Failed to get token information: "sv << winerr;
HeapFree(GetProcessHeap(), 0, user);
return nullptr;
}
return user;
}
void
free_token_user(PTOKEN_USER user) {
HeapFree(GetProcessHeap(), 0, user);
}
bool
is_token_same_user_as_process(HANDLE other_token) {
HANDLE process_token;
if (!OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &process_token)) {
auto winerr = GetLastError();
BOOST_LOG(error) << "Failed to open process token: "sv << winerr;
return false;
IsUserAdmin(HANDLE user_token) {
WINBOOL ret;
SID_IDENTIFIER_AUTHORITY NtAuthority = SECURITY_NT_AUTHORITY;
PSID AdministratorsGroup;
ret = AllocateAndInitializeSid(
&NtAuthority,
2,
SECURITY_BUILTIN_DOMAIN_RID,
DOMAIN_ALIAS_RID_ADMINS,
0, 0, 0, 0, 0, 0,
&AdministratorsGroup);
if (ret) {
if (!CheckTokenMembership(user_token, AdministratorsGroup, &ret)) {
ret = false;
BOOST_LOG(error) << "Failed to verify token membership for administrative access: " << GetLastError();
}
FreeSid(AdministratorsGroup);
}
auto process_user = get_token_user(process_token);
CloseHandle(process_token);
if (!process_user) {
return false;
else {
BOOST_LOG(error) << "Unable to allocate SID to check administrative access: " << GetLastError();
}
auto token_user = get_token_user(other_token);
if (!token_user) {
free_token_user(process_user);
return false;
}
bool ret = EqualSid(process_user->User.Sid, token_user->User.Sid);
free_token_user(process_user);
free_token_user(token_user);
return ret;
}
/**
* @brief A function to obtain the current sessions user's primary token with elevated privileges
*
* @return The users token, if user has admin capability it will be elevated. If not, it will return back a limited token. On error, nullptrs
*/
HANDLE
retrieve_users_token(bool elevated) {
DWORD consoleSessionId;
HANDLE userToken;
TOKEN_ELEVATION_TYPE elevationType;
DWORD dwSize;
// Get the session ID of the active console session
consoleSessionId = WTSGetActiveConsoleSessionId();
if (0xFFFFFFFF == consoleSessionId) {
// If there is no active console session, log a warning and return null
BOOST_LOG(warning) << "There isn't an active user session, therefore it is not possible to execute commands under the users profile.";
return nullptr;
}
// Get the user token for the active console session
if (!WTSQueryUserToken(consoleSessionId, &userToken)) {
BOOST_LOG(debug) << "QueryUserToken failed, this would prevent commands from launching under the users profile.";
return nullptr;
}
// We need to know if this is an elevated token or not.
// Get the elevation type of the user token
// Elevation - Default: User is not an admin, UAC enabled/disabled does not matter.
// Elevation - Limited: User is an admin, has UAC enabled.
// Elevation - Full: User is an admin, has UAC disabled.
if (!GetTokenInformation(userToken, TokenElevationType, &elevationType, sizeof(TOKEN_ELEVATION_TYPE), &dwSize)) {
BOOST_LOG(debug) << "Retrieving token information failed: " << GetLastError();
CloseHandle(userToken);
return nullptr;
}
// User is currently not an administrator
// The documentation for this scenario is conflicting, so we'll double check to see if user is actually an admin.
if (elevated && (elevationType == TokenElevationTypeDefault && !IsUserAdmin(userToken))) {
// We don't have to strip the token or do anything here, but let's give the user a warning so they're aware what is happening.
BOOST_LOG(warning) << "This command requires elevation and the current user account logged in does not have administrator rights. "
<< "For security reasons Sunshine will retain the same access level as the current user and will not elevate it.";
}
// User has a limited token, this means they have UAC enabled and is an Administrator
if (elevated && elevationType == TokenElevationTypeLimited) {
TOKEN_LINKED_TOKEN linkedToken;
// Retrieve the administrator token that is linked to the limited token
if (!GetTokenInformation(userToken, TokenLinkedToken, reinterpret_cast<void *>(&linkedToken), sizeof(TOKEN_LINKED_TOKEN), &dwSize)) {
// If the retrieval failed, log an error message and return null
BOOST_LOG(error) << "Retrieving linked token information failed: " << GetLastError();
CloseHandle(userToken);
// There is no scenario where this should be hit, except for an actual error.
return nullptr;
}
// Since we need the elevated token, we'll replace it with their administrative token.
CloseHandle(userToken);
userToken = linkedToken.LinkedToken;
}
// We don't need to do anything for TokenElevationTypeFull users here, because they're already elevated.
return userToken;
}
bool
merge_user_environment_block(bp::environment &env, HANDLE shell_token) {
// Get the target user's environment block
@ -334,6 +332,42 @@ namespace platf {
return true;
}
/**
@brief Check if the current process is running with system-level privileges.
@return true if the current process has system-level privileges, false otherwise.
*/
bool
is_running_as_system() {
BOOL ret;
PSID SystemSid;
DWORD dwSize = SECURITY_MAX_SID_SIZE;
// Allocate memory for the SID structure
SystemSid = LocalAlloc(LMEM_FIXED, dwSize);
if (SystemSid == nullptr) {
BOOST_LOG(error) << "Failed to allocate memory for the SID structure: " << GetLastError();
return false;
}
// Create a SID for the local system account
ret = CreateWellKnownSid(WinLocalSystemSid, nullptr, SystemSid, &dwSize);
if (ret) {
// Check if the current process token contains this SID
if (!CheckTokenMembership(nullptr, SystemSid, &ret)) {
BOOST_LOG(error) << "Failed to check token membership: " << GetLastError();
ret = false;
}
}
else {
BOOST_LOG(error) << "Failed to create a SID for the local system account. This may happen if the system is out of memory or if the SID buffer is too small: " << GetLastError();
}
// Free the memory allocated for the SID structure
LocalFree(SystemSid);
return ret;
}
// Note: This does NOT append a null terminator
void
append_string_to_environment_block(wchar_t *env_block, int &offset, const std::wstring &wstr) {
@ -395,46 +429,114 @@ namespace platf {
HeapFree(GetProcessHeap(), 0, list);
}
/**
* @brief Creates a bp::child object from the results of launching a process
*
* @param process_launched A boolean indicating whether the launch was successful or not
* @param cmd The command that was used to launch the process
* @param ec A reference to an std::error_code object that will store any error that occurred during the launch
* @param process_info A reference to a PROCESS_INFORMATION structure that contains information about the new process
* @param group A pointer to a bp::group object that will add the new process to its group, if not null
* @return A bp::child object representing the new process, or an empty bp::child object if the launch failed or an error occurred
*/
bp::child
run_unprivileged(const std::string &cmd, boost::filesystem::path &working_dir, bp::environment &env, FILE *file, std::error_code &ec, bp::group *group) {
HANDLE shell_token = duplicate_shell_token();
if (!shell_token) {
// This can happen if the shell has crashed. Fail the launch rather than risking launching with
// Sunshine's permissions unmodified.
ec = std::make_error_code(std::errc::no_such_process);
return bp::child();
}
auto token_close = util::fail_guard([shell_token]() {
CloseHandle(shell_token);
create_boost_child_from_results(bool process_launched, const std::string &cmd, std::error_code &ec, PROCESS_INFORMATION &process_info, bp::group *group) {
// Use RAII to ensure the process is closed when we're done with it, even if there was an error.
auto close_process_handles = util::fail_guard([process_launched, process_info]() {
if (process_launched) {
CloseHandle(process_info.hThread);
CloseHandle(process_info.hProcess);
}
});
// Populate env with user-specific environment variables
if (!merge_user_environment_block(env, shell_token)) {
ec = std::make_error_code(std::errc::not_enough_memory);
if (ec) {
// If there was an error, return an empty bp::child object
return bp::child();
}
// Most Win32 APIs can't consume UTF-8 strings directly, so we must convert them into UTF-16
std::wstring wcmd = utf8_to_wide_string(cmd);
std::wstring env_block = create_environment_block(env);
std::wstring start_dir = utf8_to_wide_string(working_dir.string());
if (process_launched) {
// If the launch was successful, create a new bp::child object representing the new process
auto child = bp::child((bp::pid_t) process_info.dwProcessId);
if (group) {
// If a group was provided, add the new process to the group
group->add(child);
}
BOOST_LOG(info) << cmd << " running with PID "sv << child.id();
return child;
}
else {
auto winerror = GetLastError();
BOOST_LOG(error) << "Failed to launch process: "sv << winerror;
ec = std::make_error_code(std::errc::invalid_argument);
// We must NOT attach the failed process here, since this case can potentially be induced by ACL
// manipulation (denying yourself execute permission) to cause an escalation of privilege.
// So to protect ourselves against that, we'll return an empty child process instead.
return bp::child();
}
}
/**
* @brief Impersonate the current user, invoke the callback function, then returns back to system context.
*
* @param user_token A handle to the user's token that was obtained from the shell
* @param callback A function that will be executed while impersonating the user
* @return An std::error_code object that will store any error that occurred during the impersonation
*/
std::error_code
impersonate_current_user(HANDLE user_token, std::function<void()> callback) {
std::error_code ec;
// Impersonate the user when launching the process. This will ensure that appropriate access
// checks are done against the user token, not our SYSTEM token. It will also allow network
// shares and mapped network drives to be used as launch targets, since those credentials
// are stored per-user.
if (!ImpersonateLoggedOnUser(user_token)) {
auto winerror = GetLastError();
// Log the failure of impersonating the user and its error code
BOOST_LOG(error) << "Failed to impersonate user: "sv << winerror;
ec = std::make_error_code(std::errc::permission_denied);
return ec;
}
// Execute the callback function while impersonating the user
callback();
// End impersonation of the logged on user. If this fails (which is extremely unlikely),
// we will be running with an unknown user token. The only safe thing to do in that case
// is terminate ourselves.
if (!RevertToSelf()) {
auto winerror = GetLastError();
// Log the failure of reverting to self and its error code
BOOST_LOG(fatal) << "Failed to revert to self after impersonation: "sv << winerror;
std::abort();
}
return ec;
}
/**
* @brief A function to create a STARTUPINFOEXW structure for launching a process
*
* @param file A pointer to a FILE object that will be used as the standard output and error for the new process, or null if not needed
* @param ec A reference to an std::error_code object that will store any error that occurred during the creation of the structure
* @return A STARTUPINFOEXW structure that contains information about how to launch the new process
*/
STARTUPINFOEXW
create_startup_info(FILE *file, std::error_code &ec) {
// Initialize a zeroed-out STARTUPINFOEXW structure and set its size
STARTUPINFOEXW startup_info = {};
startup_info.StartupInfo.cb = sizeof(startup_info);
// Allocate a process attribute list with space for 1 element
startup_info.lpAttributeList = allocate_proc_thread_attr_list(1);
if (startup_info.lpAttributeList == NULL) {
// If the allocation failed, set ec to an appropriate error code and return the structure
ec = std::make_error_code(std::errc::not_enough_memory);
return bp::child();
return startup_info;
}
auto attr_list_free = util::fail_guard([list = startup_info.lpAttributeList]() {
free_proc_thread_attr_list(list);
});
if (file) {
// If a file was provided, get its handle and use it as the standard output and error for the new process
HANDLE log_file_handle = (HANDLE) _get_osfhandle(_fileno(file));
// Populate std handles if the caller gave us a log file to use
@ -454,74 +556,84 @@ namespace platf {
NULL);
}
// If we're running with the same user account as the shell, just use CreateProcess().
// This will launch the child process elevated if Sunshine is elevated.
PROCESS_INFORMATION process_info;
return startup_info;
}
/**
* @brief Runs a command on the users profile
*
* This function launches a child process as the user, using the current user's environment
* and a specific working directory. If the launch is successful, a `bp::child` object representing the new
* process is returned. Otherwise, an error code is returned.
*
* @param elevated Specify to elevate the process or not
* @param cmd The command to run
* @param working_dir The working directory for the new process
* @param env The environment variables to use for the new process
* @param file A file object to redirect the child process's output to (may be nullptr)
* @param ec An error code, set to indicate any errors that occur during the launch process
* @param group A pointer to a `bp::group` object to which the new process should belong (may be nullptr)
*
* @return A `bp::child` object representing the new process, or an empty `bp::child` object if the launch fails
*/
bp::child
run_command(bool elevated, const std::string &cmd, boost::filesystem::path &working_dir, bp::environment &env, FILE *file, std::error_code &ec, bp::group *group) {
BOOL ret;
if (!is_token_same_user_as_process(shell_token)) {
// Impersonate the user when launching the process. This will ensure that appropriate access
// checks are done against the user token, not our SYSTEM token. It will also allow network
// shares and mapped network drives to be used as launch targets, since those credentials
// are stored per-user.
if (!ImpersonateLoggedOnUser(shell_token)) {
auto winerror = GetLastError();
BOOST_LOG(error) << "Failed to impersonate user: "sv << winerror;
ec = std::make_error_code(std::errc::permission_denied);
// Convert cmd, env, and working_dir to the appropriate character sets for Win32 APIs
std::wstring wcmd = utf8_to_wide_string(cmd);
std::wstring env_block = create_environment_block(env);
std::wstring start_dir = utf8_to_wide_string(working_dir.string());
STARTUPINFOEXW startup_info = create_startup_info(file, ec);
PROCESS_INFORMATION process_info;
if (ec) {
// In the event that startup_info failed, return a blank child process.
return bp::child();
}
// Use RAII to ensure the attribute list is freed when we're done with it
auto attr_list_free = util::fail_guard([list = startup_info.lpAttributeList]() {
free_proc_thread_attr_list(list);
});
if (is_running_as_system()) {
// Duplicate the current user's token
HANDLE user_token = retrieve_users_token(elevated);
if (!user_token) {
// Fail the launch rather than risking launching with Sunshine's permissions unmodified.
ec = std::make_error_code(std::errc::no_such_process);
return bp::child();
}
// Launch the process with the duplicated shell token.
// Set CREATE_BREAKAWAY_FROM_JOB to avoid the child being killed if SunshineSvc.exe is terminated.
// Set CREATE_NEW_CONSOLE to avoid writing stdout to Sunshine's log if 'file' is not specified.
ret = CreateProcessAsUserW(shell_token,
NULL,
(LPWSTR) wcmd.c_str(),
NULL,
NULL,
!!(startup_info.StartupInfo.dwFlags & STARTF_USESTDHANDLES),
EXTENDED_STARTUPINFO_PRESENT | CREATE_UNICODE_ENVIRONMENT | CREATE_NEW_CONSOLE | CREATE_BREAKAWAY_FROM_JOB,
env_block.data(),
start_dir.empty() ? NULL : start_dir.c_str(),
(LPSTARTUPINFOW) &startup_info,
&process_info);
// Use RAII to ensure the shell token is closed when we're done with it
auto token_close = util::fail_guard([user_token]() {
CloseHandle(user_token);
});
if (!ret) {
auto error = GetLastError();
if (error == 740) {
BOOST_LOG(info) << "Could not execute previous command because it required elevation. Running the command again with elevation, for security reasons this will prompt user interaction."sv;
startup_info.StartupInfo.wShowWindow = SW_HIDE;
startup_info.StartupInfo.dwFlags = startup_info.StartupInfo.dwFlags | STARTF_USESHOWWINDOW;
std::wstring elevated_command = L"tools\\elevator.exe ";
elevated_command += wcmd;
// For security reasons, Windows enforces that an application can have only one "interactive thread" responsible for processing user input and managing the user interface (UI).
// Since UAC prompts are interactive, we cannot have a UAC prompt while Sunshine is already running because it would block the thread.
// To work around this issue, we will launch a separate process that will elevate the command, which will prompt the user to confirm the elevation.
// This is our intended behavior: to require interaction before elevating the command.
ret = CreateProcessAsUserW(shell_token,
nullptr,
(LPWSTR) elevated_command.c_str(),
nullptr,
nullptr,
!!(startup_info.StartupInfo.dwFlags & STARTF_USESTDHANDLES),
EXTENDED_STARTUPINFO_PRESENT | CREATE_UNICODE_ENVIRONMENT | CREATE_NEW_CONSOLE | CREATE_BREAKAWAY_FROM_JOB,
env_block.data(),
start_dir.empty() ? nullptr : start_dir.c_str(),
(LPSTARTUPINFOW) &startup_info,
&process_info);
}
// Populate env with user-specific environment variables
if (!merge_user_environment_block(env, user_token)) {
ec = std::make_error_code(std::errc::not_enough_memory);
return bp::child();
}
// End impersonation of the logged on user. If this fails (which is extremely unlikely),
// we will be running with an unknown user token. The only safe thing to do in that case
// is terminate ourselves.
if (!RevertToSelf()) {
auto winerror = GetLastError();
BOOST_LOG(fatal) << "Failed to revert to self after impersonation: "sv << winerror;
std::abort();
}
// Open the process as the current user account, elevation is handled in the token itself.
ec = impersonate_current_user(user_token, [&]() {
ret = CreateProcessAsUserW(user_token,
NULL,
(LPWSTR) wcmd.c_str(),
NULL,
NULL,
!!(startup_info.StartupInfo.dwFlags & STARTF_USESTDHANDLES),
EXTENDED_STARTUPINFO_PRESENT | CREATE_UNICODE_ENVIRONMENT | CREATE_NEW_CONSOLE | CREATE_BREAKAWAY_FROM_JOB,
env_block.data(),
start_dir.empty() ? NULL : start_dir.c_str(),
(LPSTARTUPINFOW) &startup_info,
&process_info);
});
}
// Otherwise, launch the process using CreateProcessW()
// This will inherit the elevation of whatever the user launched Sunshine with.
else {
ret = CreateProcessW(NULL,
(LPWSTR) wcmd.c_str(),
@ -535,30 +647,8 @@ namespace platf {
&process_info);
}
if (ret) {
// Since we are always spawning a process with a less privileged token than ourselves,
// bp::child() should have no problem opening it with any access rights it wants.
auto child = bp::child((bp::pid_t) process_info.dwProcessId);
if (group) {
group->add(child);
}
// Only close handles after bp::child() has opened the process. If the process terminates
// quickly, the PID could be reused if we close the process handle.
CloseHandle(process_info.hThread);
CloseHandle(process_info.hProcess);
BOOST_LOG(info) << cmd << " running with PID "sv << child.id();
return child;
}
else {
// We must NOT try bp::child() here, since this case can potentially be induced by ACL
// manipulation (denying yourself execute permission) to cause an escalation of privilege.
auto winerror = GetLastError();
BOOST_LOG(error) << "Failed to launch process: "sv << winerror;
ec = std::make_error_code(std::errc::invalid_argument);
return bp::child();
}
// Use the results of the launch to create a bp::child object
return create_boost_child_from_results(ret, cmd, ec, process_info, group);
}
void
@ -900,5 +990,4 @@ namespace platf {
return std::make_unique<qos_t>(flow_id);
}
} // namespace platf

View File

@ -1,6 +1,5 @@
#ifndef SUNSHINE_WINDOWS_MISC_H
#define SUNSHINE_WINDOWS_MISC_H
#include <string_view>
#include <windows.h>
#include <winnt.h>

View File

@ -125,16 +125,21 @@ namespace proc {
});
for (; _app_prep_it != std::end(_app.prep_cmds); ++_app_prep_it) {
auto &cmd = _app_prep_it->do_cmd;
auto &cmd = *_app_prep_it;
// Skip empty commands
if (cmd.do_cmd.empty()) {
continue;
}
boost::filesystem::path working_dir = _app.working_dir.empty() ?
find_working_directory(cmd, _env) :
find_working_directory(cmd.do_cmd, _env) :
boost::filesystem::path(_app.working_dir);
BOOST_LOG(info) << "Executing Do Cmd: ["sv << cmd << ']';
auto child = platf::run_unprivileged(cmd, working_dir, _env, _pipe.get(), ec, nullptr);
BOOST_LOG(info) << "Executing Do Cmd: ["sv << cmd.do_cmd << ']';
auto child = platf::run_command(cmd.elevated, cmd.do_cmd, working_dir, _env, _pipe.get(), ec, nullptr);
if (ec) {
BOOST_LOG(error) << "Couldn't run ["sv << cmd << "]: System: "sv << ec.message();
BOOST_LOG(error) << "Couldn't run ["sv << cmd.do_cmd << "]: System: "sv << ec.message();
return -1;
}
@ -142,7 +147,7 @@ namespace proc {
auto ret = child.exit_code();
if (ret != 0) {
BOOST_LOG(error) << '[' << cmd << "] failed with code ["sv << ret << ']';
BOOST_LOG(error) << '[' << cmd.do_cmd << "] failed with code ["sv << ret << ']';
return -1;
}
}
@ -152,7 +157,7 @@ namespace proc {
find_working_directory(cmd, _env) :
boost::filesystem::path(_app.working_dir);
BOOST_LOG(info) << "Spawning ["sv << cmd << "] in ["sv << working_dir << ']';
auto child = platf::run_unprivileged(cmd, working_dir, _env, _pipe.get(), ec, nullptr);
auto child = platf::run_command(_app.elevated, cmd, working_dir, _env, _pipe.get(), ec, nullptr);
if (ec) {
BOOST_LOG(warning) << "Couldn't spawn ["sv << cmd << "]: System: "sv << ec.message();
}
@ -170,7 +175,7 @@ namespace proc {
find_working_directory(_app.cmd, _env) :
boost::filesystem::path(_app.working_dir);
BOOST_LOG(info) << "Executing: ["sv << _app.cmd << "] in ["sv << working_dir << ']';
_process = platf::run_unprivileged(_app.cmd, working_dir, _env, _pipe.get(), ec, &_process_handle);
_process = platf::run_command(_app.elevated, _app.cmd, working_dir, _env, _pipe.get(), ec, &_process_handle);
if (ec) {
BOOST_LOG(warning) << "Couldn't run ["sv << _app.cmd << "]: System: "sv << ec.message();
return -1;
@ -208,17 +213,17 @@ namespace proc {
_app_id = -1;
for (; _app_prep_it != _app_prep_begin; --_app_prep_it) {
auto &cmd = (_app_prep_it - 1)->undo_cmd;
auto &cmd = *(_app_prep_it - 1);
if (cmd.empty()) {
if (cmd.undo_cmd.empty()) {
continue;
}
boost::filesystem::path working_dir = _app.working_dir.empty() ?
find_working_directory(cmd, _env) :
find_working_directory(cmd.undo_cmd, _env) :
boost::filesystem::path(_app.working_dir);
BOOST_LOG(info) << "Executing Undo Cmd: ["sv << cmd << ']';
auto child = platf::run_unprivileged(cmd, working_dir, _env, _pipe.get(), ec, nullptr);
BOOST_LOG(info) << "Executing Undo Cmd: ["sv << cmd.undo_cmd << ']';
auto child = platf::run_command(cmd.elevated, cmd.undo_cmd, working_dir, _env, _pipe.get(), ec, nullptr);
if (ec) {
BOOST_LOG(warning) << "System: "sv << ec.message();
@ -481,6 +486,7 @@ namespace proc {
auto cmd = app_node.get_optional<std::string>("cmd"s);
auto image_path = app_node.get_optional<std::string>("image-path"s);
auto working_dir = app_node.get_optional<std::string>("working-dir"s);
auto elevated = app_node.get_optional<bool>("elevated"s);
std::vector<proc::cmd_t> prep_cmds;
if (!exclude_global_prep.value_or(false)) {
@ -489,7 +495,10 @@ namespace proc {
auto do_cmd = parse_env_val(this_env, prep_cmd.do_cmd);
auto undo_cmd = parse_env_val(this_env, prep_cmd.undo_cmd);
prep_cmds.emplace_back(std::move(do_cmd), std::move(undo_cmd));
prep_cmds.emplace_back(
std::move(do_cmd),
std::move(undo_cmd),
std::move(prep_cmd.elevated));
}
}
@ -498,15 +507,14 @@ namespace proc {
prep_cmds.reserve(prep_cmds.size() + prep_nodes.size());
for (auto &[_, prep_node] : prep_nodes) {
auto do_cmd = parse_env_val(this_env, prep_node.get<std::string>("do"s));
auto do_cmd = prep_node.get_optional<std::string>("do"s);
auto undo_cmd = prep_node.get_optional<std::string>("undo"s);
auto elevated = prep_node.get_optional<bool>("elevated");
if (undo_cmd) {
prep_cmds.emplace_back(std::move(do_cmd), parse_env_val(this_env, *undo_cmd));
}
else {
prep_cmds.emplace_back(std::move(do_cmd));
}
prep_cmds.emplace_back(
parse_env_val(this_env, do_cmd.value_or("")),
parse_env_val(this_env, undo_cmd.value_or("")),
std::move(elevated.value_or(false)));
}
}
@ -536,6 +544,8 @@ namespace proc {
ctx.image_path = parse_env_val(this_env, *image_path);
}
ctx.elevated = elevated.value_or(false);
auto possible_ids = calculate_app_id(name, ctx.image_path, i++);
if (ids.count(std::get<0>(possible_ids)) == 0) {
// Avoid using index to generate id if possible

View File

@ -48,6 +48,7 @@ namespace proc {
std::string output;
std::string image_path;
std::string id;
bool elevated;
};
class proc_t {

View File

@ -62,7 +62,7 @@ namespace system_tray {
boost::process::environment _env = boost::this_process::environment();
std::error_code ec;
auto child = platf::run_unprivileged(cmd, working_dir, _env, nullptr, ec, nullptr);
auto child = platf::run_command(false, cmd, working_dir, _env, nullptr, ec, nullptr);
if (ec) {
BOOST_LOG(warning) << "Couldn't open url ["sv << url << "]: System: "sv << ec.message();
}

View File

@ -15,9 +15,11 @@
<tr v-for="(app,i) in apps" :key="i">
<td>{{app.name}}</td>
<td>
<button class="btn btn-primary" @click="editApp(i)">Edit</button>
<button class="btn btn-primary" @click="editApp(i)">
<i class="fas fa-edit"></i> Edit
</button>
<button class="btn btn-danger" @click="showDeleteForm(i)">
Delete
<i class="fas fa-trash"></i> Delete
</button>
</td>
</tr>
@ -56,29 +58,51 @@
</div>
</div>
<!--prep-cmd-->
<div class="mb-3 d-flex flex-column">
<div class="mb-3">
<label for="excludeGlobalPrep" class="form-label">Global Prep Commands</label>
<select id="excludeGlobalPrep" class="form-select" v-model="editForm['exclude-global-prep-cmd']">
<option v-for="val in [false, true]" :value="val">{{ !val ? 'Enabled' : 'Disabled' }}</option>
</select>
<div class="form-text">
Enable/Disable the execution of Global Prep Commands for this application.
</div>
<div class="mb-3">
<label for="excludeGlobalPrep" class="form-label"
>Global Prep Commands</label
>
<select
id="excludeGlobalPrep"
class="form-select"
v-model="editForm['exclude-global-prep-cmd']"
>
<option v-for="val in [false, true]" :value="val">
{{ !val ? 'Enabled' : 'Disabled' }}
</option>
</select>
<div class="form-text">
Enable/Disable the execution of Global Prep Commands for this
application.
</div>
</div>
<div class="mb-3">
<label for="appName" class="form-label">Command Preparations</label>
<div class="form-text">
A list of commands to be run before/after this application. <br />
If any of the prep-commands fail, starting the application is aborted
A list of commands to be run before/after this application.<br />
If any of the prep-commands fail, starting the application is aborted.
</div>
<table v-if="editForm['prep-cmd'].length > 0">
<div
class="d-flex justify-content-start mb-3 mt-3"
v-if="editForm['prep-cmd'].length === 0"
>
<button class="btn btn-success" @click="addPrepCmd">
<i class="fas fa-plus mr-1"></i> Add Commands
</button>
</div>
<table class="table" v-if="editForm['prep-cmd'].length > 0">
<thead>
<th class="precmd-head">Do</th>
<th class="precmd-head">Undo</th>
<th style="width: 48px"></th>
<tr>
<th scope="col"><i class="fas fa-play"></i> Do Command</th>
<th scope="col"><i class="fas fa-undo"></i> Undo Command</th>
<th scope="col" v-if="platform === 'windows'">
<i class="fas fa-shield-alt"></i> Run as Admin
</th>
<th scope="col"></th>
</tr>
</thead>
<tbody>
<tr v-for="(c,i) in editForm['prep-cmd']">
<tr v-for="(c, i) in editForm['prep-cmd']">
<td>
<input
type="text"
@ -93,24 +117,35 @@
v-model="c.undo"
/>
</td>
<td v-if="platform === 'windows'">
<div class="form-check">
<input
type="checkbox"
class="form-check-input"
:id="'prep-cmd-admin-' + i"
v-model="c.elevated"
true-value="true"
false-value="false"
/>
<label :for="'prep-cmd-admin-' + i" class="form-check-label"
>Elevated</label
>
</div>
</td>
<td>
<button
class="btn btn-danger"
@click="editForm['prep-cmd'].splice(i,1)"
@click="$delete(editForm['prep-cmd'], i)"
>
&times;
<i class="fas fa-trash"></i>
</button>
<button class="btn btn-success" @click="addPrepCmd">
<i class="fas fa-plus"></i>
</button>
</td>
</tr>
</tbody>
</table>
<button
class="mt-2 btn btn-success"
style="margin: 0 auto"
@click="addPrepCmd"
>
&plus; Add
</button>
</div>
<!--detatched-->
<div class="mb-3">
@ -170,9 +205,28 @@
v-model="editForm['working-dir']"
/>
<div id="appWorkingDirHelp" class="form-text">
The working directory that should be passed to the process.
For example, some applications use the working directory to search for configuration files.
If not set, Sunshine will default to the parent directory of the command
The working directory that should be passed to the process. For
example, some applications use the working directory to search for
configuration files. If not set, Sunshine will default to the parent
directory of the command
</div>
</div>
<!-- elevation -->
<div class="mb-3 form-check" v-if="platform === 'windows'">
<label for="appElevation" class="form-check-label"
>Run as administrator</label
>
<input
type="checkbox"
class="form-check-input"
id="appElevation"
v-model="editForm.elevated"
true-value="true"
false-value="false"
/>
<div class="form-text">
This can be necessary for some applications that require administrator
permissions to run properly.
</div>
</div>
<!-- Image path -->
@ -180,36 +234,60 @@
<label for="appImagePath" class="form-label">Image</label>
<div class="input-group dropup">
<input
type="text"
class="form-control monospace"
id="appImagePath"
aria-describedby="appImagePathHelp"
v-model="editForm['image-path']"
type="text"
class="form-control monospace"
id="appImagePath"
aria-describedby="appImagePathHelp"
v-model="editForm['image-path']"
/>
<button class="btn btn-secondary dropdown-toggle" type="button" id="findCoverToggle" data-bs-toggle="dropdown"
data-bs-auto-close="outside" aria-expanded="false" v-dropdown-show="showCoverFinder"
ref="coverFinderDropdown">
<button
class="btn btn-secondary dropdown-toggle"
type="button"
id="findCoverToggle"
data-bs-toggle="dropdown"
data-bs-auto-close="outside"
aria-expanded="false"
v-dropdown-show="showCoverFinder"
ref="coverFinderDropdown"
>
Find Cover
</button>
<div class="dropdown-menu dropdown-menu-end w-50 cover-finder overflow-hidden"
aria-labelledby="findCoverToggle">
<div
class="dropdown-menu dropdown-menu-end w-50 cover-finder overflow-hidden"
aria-labelledby="findCoverToggle"
>
<div class="modal-header">
<h4 class="modal-title">Covers Found</h4>
<button type="button" class="btn-close" aria-label="Close" @click="closeCoverFinder"></button>
<button
type="button"
class="btn-close"
aria-label="Close"
@click="closeCoverFinder"
></button>
</div>
<div class="modal-body cover-results px-3 pt-3" :class="{ busy: coverFinderBusy }">
<div
class="modal-body cover-results px-3 pt-3"
:class="{ busy: coverFinderBusy }"
>
<div class="row">
<div v-if="coverSearching" class="col-12 col-sm-6 col-lg-4 mb-3">
<div
v-if="coverSearching"
class="col-12 col-sm-6 col-lg-4 mb-3"
>
<div class="cover-container">
<div class="spinner-border" role="status">
<span class="visually-hidden">Loading...</span>
</div>
</div>
</div>
<div v-for="(cover,i) in coverCandidates" :key="i" class="col-12 col-sm-6 col-lg-4 mb-3"
@click="useCover(cover)">
<div
v-for="(cover,i) in coverCandidates"
:key="'i'"
class="col-12 col-sm-6 col-lg-4 mb-3"
@click="useCover(cover)"
>
<div class="cover-container result">
<img class="rounded" :src="cover.url"/>
<img class="rounded" :src="cover.url" />
</div>
<label class="d-block text-nowrap text-center text-truncate">
{{cover.name}}
@ -220,8 +298,8 @@
</div>
</div>
<div id="appImagePathHelp" class="form-text">
Application icon/picture/image path that will be sent to client. Image must be a PNG file.
If not set, Sunshine will send default box image.
Application icon/picture/image path that will be sent to client. Image
must be a PNG file. If not set, Sunshine will send default box image.
</div>
</div>
<!--buttons-->
@ -234,10 +312,11 @@
</div>
</div>
<div class="mt-2" v-else>
<button class="btn btn-primary" @click="newApp">+ Add New</button>
<button class="btn btn-primary" @click="newApp">
<i class="fas fa-plus"></i> Add New
</button>
</div>
</div>
<script>
Vue.directive('dropdown-show', {
bind: function (el, binding) {
@ -255,7 +334,8 @@
detachedCmd: "",
coverSearching: false,
coverFinderBusy: false,
coverCandidates: []
coverCandidates: [],
platform: "",
};
},
created() {
@ -265,6 +345,10 @@
console.log(r);
this.apps = r.apps;
});
fetch("/api/config")
.then(r => r.json())
.then(r => this.platform = r.platform);
},
methods: {
newApp() {
@ -274,6 +358,7 @@
cmd: [],
index: -1,
"exclude-global-prep-cmd": false,
elevated: false,
"prep-cmd": [],
detached: [],
"image-path": ""
@ -290,6 +375,9 @@
this.$set(this.editForm, "detached", []);
if (this.editForm["exclude-global-prep-cmd"] === undefined)
this.$set(this.editForm, "exclude-global-prep-cmd", false);
if(this.editForm["elevated"] === undefined && this.platform === 'windows'){
this.$set(this.editForm, "elevated", false);
}
this.showEditForm = true;
},
showDeleteForm(id) {
@ -303,10 +391,16 @@
}
},
addPrepCmd() {
this.editForm["prep-cmd"].push({
let template = {
do: "",
undo: "",
});
undo: ""
};
if(this.platform === 'windows'){
template = {...template, elevated: false};
}
this.editForm["prep-cmd"].push(template);
},
showCoverFinder($event) {
this.coverCandidates = [];

View File

@ -222,47 +222,55 @@
<div class="mb-3 d-flex flex-column">
<label class="form-label">Command Preparations</label>
<div class="form-text">
A list of commands to be run before/after all applications. <br />
If any of the prep-commands fail, starting the application is aborted.
Configure a list of commands to be executed before or after running any application.
If any of the specified preparation commands fail, the application launch process will be aborted.
</div>
<table v-if="global_prep_cmd.length > 0">
<table class="table" v-if="global_prep_cmd.length > 0">
<thead>
<th>Do</th>
<th>Undo</th>
<th style="width: 48px"></th>
<tr>
<th scope="col"><i class="fas fa-play"></i> Do Command</th>
<th scope="col"><i class="fas fa-undo"></i> Undo Command</th>
<th scope="col" v-if="platform === 'windows'">
<i class="fas fa-shield-alt"></i> Run as Admin
</th>
<th scope="col"></th>
</tr>
</thead>
<tbody>
<tr v-for="(c,i) in global_prep_cmd">
<tr v-for="(c, i) in global_prep_cmd">
<td>
<input
type="text"
class="form-control monospace"
v-model="c.do"
/>
<input type="text" class="form-control monospace" v-model="c.do" />
</td>
<td>
<input
type="text"
class="form-control monospace"
v-model="c.undo"
/>
<input type="text" class="form-control monospace" v-model="c.undo" />
</td>
<td v-if="platform === 'windows'">
<div class="form-check">
<input
type="checkbox"
class="form-check-input"
:id="'prep-cmd-admin-' + i"
v-model="c.elevated"
true-value="true"
false-value="false"
/>
<label :for="'prep-cmd-admin-' + i" class="form-check-label"
>Elevated</label
>
</div>
</td>
<td>
<button
class="btn btn-danger"
@click="global_prep_cmd.splice(i,1)"
>
&times;
<button class="btn btn-danger" @click="$delete(global_prep_cmd, i)">
<i class="fas fa-trash"></i>
</button>
<button class="btn btn-success" @click="add_global_prep_cmd">
<i class="fas fa-plus"></i>
</button>
</td>
</tr>
</tbody>
</table>
<button
class="mt-2 btn btn-success"
style="margin: 0 auto"
@click="add_global_prep_cmd"
>
</table>
<button class="mt-2 btn btn-success" style="margin: 0 auto" @click="add_global_prep_cmd">
&plus; Add
</button>
</div>
@ -1198,10 +1206,15 @@
});
},
add_global_prep_cmd() {
this.global_prep_cmd.push({
let template = {
do: "",
undo: "",
});
};
if(this.platform === 'windows'){
template = {...template, elevated: false};
}
this.global_prep_cmd.push(template);
},
},
});

View File

@ -12,13 +12,6 @@ target_link_libraries(dxgi-info
${PLATFORM_LIBRARIES})
target_compile_options(dxgi-info PRIVATE ${SUNSHINE_COMPILE_OPTIONS})
add_executable(elevator elevator.cpp)
set_target_properties(elevator PROPERTIES CXX_STANDARD 17)
target_link_libraries(elevator
shell32
${PLATFORM_LIBRARIES})
target_compile_options(elevator PRIVATE ${SUNSHINE_COMPILE_OPTIONS})
add_executable(audio-info audio.cpp)
set_target_properties(audio-info PROPERTIES CXX_STANDARD 17)
target_link_libraries(audio-info

View File

@ -1,71 +0,0 @@
#include <Windows.h>
#include <iostream>
#include <string>
/**
* @file elevator.cpp
* @brief A simple command line utility to run a given command with administrative privileges.
*
* This utility helps run a command with administrative privileges on Windows
* by leveraging the ShellExecuteExW function. The program accepts a command
* and optional arguments, then attempts to run the command with elevated
* privileges. If successful, it waits for the process to complete and
* returns the exit code of the launched process.
*
* @example
* To run the command prompt with administrative privileges, execute the following command:
* elevator.exe cmd
*
* To run a command, such as 'ipconfig /flushdns', with administrative privileges, execute:
* elevator.exe cmd /C "ipconfig /flushdns"
*/
int
main(int argc, char *argv[]) {
// Check if the user provided at least one argument (the command to run)
if (argc < 2) {
std::cout << "Usage: " << argv[0] << " <command> [arguments]" << std::endl;
return 1;
}
// Convert the command and arguments from char* to wstring for use with ShellExecuteExW
std::wstring command = std::wstring(argv[1], argv[1] + strlen(argv[1]));
std::wstring arguments;
// Concatenate the remaining arguments (if any) into a single wstring
for (int i = 2; i < argc; ++i) {
arguments += std::wstring(argv[i], argv[i] + strlen(argv[i]));
if (i < argc - 1) {
arguments += L" ";
}
}
// Prepare the SHELLEXECUTEINFOW structure with the necessary information
SHELLEXECUTEINFOW info = { sizeof(SHELLEXECUTEINFOW) };
info.lpVerb = L"runas"; // Request elevation
info.lpFile = command.c_str();
info.lpParameters = arguments.empty() ? nullptr : arguments.c_str();
info.nShow = SW_SHOW;
info.fMask = SEE_MASK_NOCLOSEPROCESS; // So we can wait for the process to finish
// Attempt to execute the command with elevation
if (!ShellExecuteExW(&info)) {
std::cout << "Error: ShellExecuteExW failed with code " << GetLastError() << std::endl;
return 1;
}
// Wait for the launched process to finish
WaitForSingleObject(info.hProcess, INFINITE);
DWORD exitCode = 0;
// Retrieve the exit code of the launched process
if (!GetExitCodeProcess(info.hProcess, &exitCode)) {
std::cout << "Error: GetExitCodeProcess failed with code " << GetLastError() << std::endl;
}
// Close the process handle
CloseHandle(info.hProcess);
// Return the exit code of the launched process
return exitCode;
}