mirror of
https://github.com/LizardByte/Sunshine.git
synced 2025-03-29 22:20:24 +00:00
Elevated Commands Redesign (#1123)
This commit is contained in:
parent
18ab7dcf6c
commit
430a439698
@ -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")
|
||||
|
@ -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": ""
|
||||
}
|
||||
|
@ -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)));
|
||||
}
|
||||
}
|
||||
|
||||
|
10
src/config.h
10
src/config.h
@ -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;
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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,
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
|
@ -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
|
@ -1,6 +1,5 @@
|
||||
#ifndef SUNSHINE_WINDOWS_MISC_H
|
||||
#define SUNSHINE_WINDOWS_MISC_H
|
||||
|
||||
#include <string_view>
|
||||
#include <windows.h>
|
||||
#include <winnt.h>
|
||||
|
@ -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
|
||||
|
@ -48,6 +48,7 @@ namespace proc {
|
||||
std::string output;
|
||||
std::string image_path;
|
||||
std::string id;
|
||||
bool elevated;
|
||||
};
|
||||
|
||||
class proc_t {
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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)"
|
||||
>
|
||||
×
|
||||
<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"
|
||||
>
|
||||
+ 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 = [];
|
||||
|
@ -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)"
|
||||
>
|
||||
×
|
||||
<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">
|
||||
+ 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);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user