Elevated Commands Redesign (#1123)
This commit is contained in:
parent
18ab7dcf6c
commit
430a439698
17 changed files with 568 additions and 425 deletions
|
|
@ -639,6 +639,8 @@ if(WIN32)
|
||||||
set_target_properties(sunshine PROPERTIES LINK_SEARCH_START_STATIC 1)
|
set_target_properties(sunshine PROPERTIES LINK_SEARCH_START_STATIC 1)
|
||||||
set(CMAKE_FIND_LIBRARY_SUFFIXES ".dll")
|
set(CMAKE_FIND_LIBRARY_SUFFIXES ".dll")
|
||||||
find_library(ZLIB ZLIB1)
|
find_library(ZLIB ZLIB1)
|
||||||
|
list(APPEND SUNSHINE_EXTERNAL_LIBRARIES
|
||||||
|
Wtsapi32.lib)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
target_link_libraries(sunshine ${SUNSHINE_EXTERNAL_LIBRARIES} ${EXTRA_LIBS})
|
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 dxgi-info RUNTIME DESTINATION "tools" COMPONENT dxgi)
|
||||||
install(TARGETS audio-info RUNTIME DESTINATION "tools" COMPONENT audio)
|
install(TARGETS audio-info RUNTIME DESTINATION "tools" COMPONENT audio)
|
||||||
install(TARGETS sunshinesvc RUNTIME DESTINATION "tools" COMPONENT sunshinesvc)
|
install(TARGETS sunshinesvc RUNTIME DESTINATION "tools" COMPONENT sunshinesvc)
|
||||||
install(TARGETS elevator RUNTIME DESTINATION "tools" COMPONENT elevator)
|
|
||||||
|
|
||||||
# Mandatory tools
|
# Mandatory tools
|
||||||
install(TARGETS ddprobe RUNTIME DESTINATION "tools" COMPONENT application)
|
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
|
# Extra install commands
|
||||||
# Restores permissions on the install directory
|
# Restores permissions on the install directory
|
||||||
# Migrates config files from the root into the new config folder
|
# 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
|
# Install service
|
||||||
SET(CPACK_NSIS_EXTRA_INSTALL_COMMANDS
|
SET(CPACK_NSIS_EXTRA_INSTALL_COMMANDS
|
||||||
"${CPACK_NSIS_EXTRA_INSTALL_COMMANDS}
|
"${CPACK_NSIS_EXTRA_INSTALL_COMMANDS}
|
||||||
IfSilent +2 0
|
IfSilent +2 0
|
||||||
ExecShell 'open' 'https://sunshinestream.readthedocs.io/'
|
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 '\\\"$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\\\\add-firewall-rule.bat\\\"'
|
||||||
nsExec::ExecToLog '\\\"$INSTDIR\\\\scripts\\\\install-service.bat\\\"'
|
nsExec::ExecToLog '\\\"$INSTDIR\\\\scripts\\\\install-service.bat\\\"'
|
||||||
nsExec::ExecToLog '\\\"$INSTDIR\\\\scripts\\\\install-vigembus.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:
|
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
|
# Checking for previous installed versions
|
||||||
set(CPACK_NSIS_ENABLE_UNINSTALL_BEFORE_INSTALL "ON")
|
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_DESCRIPTION "CLI tool providing information about sound devices.")
|
||||||
set(CPACK_COMPONENT_AUDIO_GROUP "tools")
|
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
|
# display tool
|
||||||
set(CPACK_COMPONENT_DXGI_DISPLAY_NAME "dxgi-info")
|
set(CPACK_COMPONENT_DXGI_DISPLAY_NAME "dxgi-info")
|
||||||
set(CPACK_COMPONENT_DXGI_DESCRIPTION "CLI tool providing information about graphics cards and displays.")
|
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
|
# service
|
||||||
set(CPACK_COMPONENT_SUNSHINESVC_DISPLAY_NAME "sunshinesvc")
|
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")
|
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
|
# service scripts
|
||||||
set(CPACK_COMPONENT_SERVICE_DISPLAY_NAME "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_GROUP "scripts")
|
||||||
set(CPACK_COMPONENT_SERVICE_DEPENDS sunshinesvc)
|
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
|
# firewall scripts
|
||||||
set(CPACK_COMPONENT_FIREWALL_DISPLAY_NAME "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
|
.. Tip:: You can change your host resolution to match the client resolution automatically using the
|
||||||
`Nonary/ResolutionAutomation <https://github.com/Nonary/ResolutionAutomation/>`_ project.
|
`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);
|
boost::property_tree::read_json(jsonStream, jsonTree);
|
||||||
|
|
||||||
for (auto &[_, prep_cmd] : jsonTree.get_child("prep_cmd"s)) {
|
for (auto &[_, prep_cmd] : jsonTree.get_child("prep_cmd"s)) {
|
||||||
auto do_cmd = prep_cmd.get<std::string>("do"s);
|
auto do_cmd = prep_cmd.get_optional<std::string>("do"s);
|
||||||
auto undo_cmd = prep_cmd.get<std::string>("undo"s);
|
auto undo_cmd = prep_cmd.get_optional<std::string>("undo"s);
|
||||||
|
auto elevated = prep_cmd.get_optional<bool>("elevated"s);
|
||||||
|
|
||||||
input.emplace_back(
|
input.emplace_back(
|
||||||
std::move(do_cmd),
|
std::move(do_cmd.value_or("")),
|
||||||
std::move(undo_cmd));
|
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 {
|
struct prep_cmd_t {
|
||||||
prep_cmd_t(std::string &&do_cmd, std::string &&undo_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)) {}
|
do_cmd(std::move(do_cmd)), undo_cmd(std::move(undo_cmd)), elevated(std::move(elevated)) {}
|
||||||
explicit prep_cmd_t(std::string &&do_cmd):
|
explicit prep_cmd_t(std::string &&do_cmd, bool &&elevated):
|
||||||
do_cmd(std::move(do_cmd)) {}
|
do_cmd(std::move(do_cmd)), elevated(std::move(elevated)) {}
|
||||||
std::string do_cmd;
|
std::string do_cmd;
|
||||||
std::string undo_cmd;
|
std::string undo_cmd;
|
||||||
|
bool elevated;
|
||||||
};
|
};
|
||||||
|
|
||||||
struct sunshine_t {
|
struct sunshine_t {
|
||||||
int min_log_level;
|
int min_log_level;
|
||||||
std::bitset<flag::FLAG_SIZE> flags;
|
std::bitset<flag::FLAG_SIZE> flags;
|
||||||
|
|
|
||||||
|
|
@ -89,7 +89,7 @@ namespace http {
|
||||||
pt::write_json(file, outputTree);
|
pt::write_json(file, outputTree);
|
||||||
}
|
}
|
||||||
catch (std::exception &e) {
|
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;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -382,7 +382,7 @@ namespace platf {
|
||||||
display_names(mem_type_e hwdevice_type);
|
display_names(mem_type_e hwdevice_type);
|
||||||
|
|
||||||
boost::process::child
|
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 {
|
enum class thread_priority_e : int {
|
||||||
low,
|
low,
|
||||||
|
|
|
||||||
|
|
@ -159,8 +159,7 @@ namespace platf {
|
||||||
}
|
}
|
||||||
|
|
||||||
bp::child
|
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) {
|
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) {
|
||||||
BOOST_LOG(warning) << "run_unprivileged() is not yet implemented for this platform. The new process will run with Sunshine's permissions."sv;
|
|
||||||
if (!group) {
|
if (!group) {
|
||||||
if (!file) {
|
if (!file) {
|
||||||
return bp::child(cmd, env, bp::start_dir(working_dir), bp::std_out > bp::null, bp::std_err > bp::null, ec);
|
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
|
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) {
|
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) {
|
||||||
BOOST_LOG(warning) << "run_unprivileged() is not yet implemented for this platform. The new process will run with Sunshine's permissions."sv;
|
|
||||||
if (!group) {
|
if (!group) {
|
||||||
if (!file) {
|
if (!file) {
|
||||||
return bp::child(cmd, env, bp::start_dir(working_dir), bp::std_out > bp::null, bp::std_err > bp::null, ec);
|
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 <winuser.h>
|
||||||
#include <wlanapi.h>
|
#include <wlanapi.h>
|
||||||
#include <ws2tcpip.h>
|
#include <ws2tcpip.h>
|
||||||
|
#include <wtsapi32.h>
|
||||||
|
#include <sddl.h>
|
||||||
// clang-format on
|
// clang-format on
|
||||||
|
|
||||||
#include "src/main.h"
|
#include "src/main.h"
|
||||||
|
|
@ -200,103 +202,99 @@ namespace platf {
|
||||||
return std::string(buffer, bytes);
|
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
|
bool
|
||||||
is_token_same_user_as_process(HANDLE other_token) {
|
IsUserAdmin(HANDLE user_token) {
|
||||||
HANDLE process_token;
|
WINBOOL ret;
|
||||||
if (!OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &process_token)) {
|
SID_IDENTIFIER_AUTHORITY NtAuthority = SECURITY_NT_AUTHORITY;
|
||||||
auto winerr = GetLastError();
|
PSID AdministratorsGroup;
|
||||||
BOOST_LOG(error) << "Failed to open process token: "sv << winerr;
|
ret = AllocateAndInitializeSid(
|
||||||
return false;
|
&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);
|
||||||
}
|
}
|
||||||
|
else {
|
||||||
auto process_user = get_token_user(process_token);
|
BOOST_LOG(error) << "Unable to allocate SID to check administrative access: " << GetLastError();
|
||||||
CloseHandle(process_token);
|
|
||||||
if (!process_user) {
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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;
|
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
|
bool
|
||||||
merge_user_environment_block(bp::environment &env, HANDLE shell_token) {
|
merge_user_environment_block(bp::environment &env, HANDLE shell_token) {
|
||||||
// Get the target user's environment block
|
// Get the target user's environment block
|
||||||
|
|
@ -334,6 +332,42 @@ namespace platf {
|
||||||
return true;
|
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
|
// Note: This does NOT append a null terminator
|
||||||
void
|
void
|
||||||
append_string_to_environment_block(wchar_t *env_block, int &offset, const std::wstring &wstr) {
|
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);
|
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
|
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) {
|
create_boost_child_from_results(bool process_launched, const std::string &cmd, std::error_code &ec, PROCESS_INFORMATION &process_info, bp::group *group) {
|
||||||
HANDLE shell_token = duplicate_shell_token();
|
// Use RAII to ensure the process is closed when we're done with it, even if there was an error.
|
||||||
if (!shell_token) {
|
auto close_process_handles = util::fail_guard([process_launched, process_info]() {
|
||||||
// This can happen if the shell has crashed. Fail the launch rather than risking launching with
|
if (process_launched) {
|
||||||
// Sunshine's permissions unmodified.
|
CloseHandle(process_info.hThread);
|
||||||
ec = std::make_error_code(std::errc::no_such_process);
|
CloseHandle(process_info.hProcess);
|
||||||
return bp::child();
|
}
|
||||||
}
|
|
||||||
|
|
||||||
auto token_close = util::fail_guard([shell_token]() {
|
|
||||||
CloseHandle(shell_token);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Populate env with user-specific environment variables
|
if (ec) {
|
||||||
if (!merge_user_environment_block(env, shell_token)) {
|
// If there was an error, return an empty bp::child object
|
||||||
ec = std::make_error_code(std::errc::not_enough_memory);
|
|
||||||
return bp::child();
|
return bp::child();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Most Win32 APIs can't consume UTF-8 strings directly, so we must convert them into UTF-16
|
if (process_launched) {
|
||||||
std::wstring wcmd = utf8_to_wide_string(cmd);
|
// If the launch was successful, create a new bp::child object representing the new process
|
||||||
std::wstring env_block = create_environment_block(env);
|
auto child = bp::child((bp::pid_t) process_info.dwProcessId);
|
||||||
std::wstring start_dir = utf8_to_wide_string(working_dir.string());
|
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 = {};
|
STARTUPINFOEXW startup_info = {};
|
||||||
startup_info.StartupInfo.cb = sizeof(startup_info);
|
startup_info.StartupInfo.cb = sizeof(startup_info);
|
||||||
|
|
||||||
// Allocate a process attribute list with space for 1 element
|
// Allocate a process attribute list with space for 1 element
|
||||||
startup_info.lpAttributeList = allocate_proc_thread_attr_list(1);
|
startup_info.lpAttributeList = allocate_proc_thread_attr_list(1);
|
||||||
if (startup_info.lpAttributeList == NULL) {
|
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);
|
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 (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));
|
HANDLE log_file_handle = (HANDLE) _get_osfhandle(_fileno(file));
|
||||||
|
|
||||||
// Populate std handles if the caller gave us a log file to use
|
// Populate std handles if the caller gave us a log file to use
|
||||||
|
|
@ -454,74 +556,84 @@ namespace platf {
|
||||||
NULL);
|
NULL);
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we're running with the same user account as the shell, just use CreateProcess().
|
return startup_info;
|
||||||
// This will launch the child process elevated if Sunshine is elevated.
|
}
|
||||||
PROCESS_INFORMATION process_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;
|
BOOL ret;
|
||||||
if (!is_token_same_user_as_process(shell_token)) {
|
// Convert cmd, env, and working_dir to the appropriate character sets for Win32 APIs
|
||||||
// Impersonate the user when launching the process. This will ensure that appropriate access
|
std::wstring wcmd = utf8_to_wide_string(cmd);
|
||||||
// checks are done against the user token, not our SYSTEM token. It will also allow network
|
std::wstring env_block = create_environment_block(env);
|
||||||
// shares and mapped network drives to be used as launch targets, since those credentials
|
std::wstring start_dir = utf8_to_wide_string(working_dir.string());
|
||||||
// are stored per-user.
|
|
||||||
if (!ImpersonateLoggedOnUser(shell_token)) {
|
STARTUPINFOEXW startup_info = create_startup_info(file, ec);
|
||||||
auto winerror = GetLastError();
|
PROCESS_INFORMATION process_info;
|
||||||
BOOST_LOG(error) << "Failed to impersonate user: "sv << winerror;
|
|
||||||
ec = std::make_error_code(std::errc::permission_denied);
|
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();
|
return bp::child();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Launch the process with the duplicated shell token.
|
// Use RAII to ensure the shell token is closed when we're done with it
|
||||||
// Set CREATE_BREAKAWAY_FROM_JOB to avoid the child being killed if SunshineSvc.exe is terminated.
|
auto token_close = util::fail_guard([user_token]() {
|
||||||
// Set CREATE_NEW_CONSOLE to avoid writing stdout to Sunshine's log if 'file' is not specified.
|
CloseHandle(user_token);
|
||||||
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);
|
|
||||||
|
|
||||||
if (!ret) {
|
// Populate env with user-specific environment variables
|
||||||
auto error = GetLastError();
|
if (!merge_user_environment_block(env, user_token)) {
|
||||||
|
ec = std::make_error_code(std::errc::not_enough_memory);
|
||||||
if (error == 740) {
|
return bp::child();
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// End impersonation of the logged on user. If this fails (which is extremely unlikely),
|
// Open the process as the current user account, elevation is handled in the token itself.
|
||||||
// we will be running with an unknown user token. The only safe thing to do in that case
|
ec = impersonate_current_user(user_token, [&]() {
|
||||||
// is terminate ourselves.
|
ret = CreateProcessAsUserW(user_token,
|
||||||
if (!RevertToSelf()) {
|
NULL,
|
||||||
auto winerror = GetLastError();
|
(LPWSTR) wcmd.c_str(),
|
||||||
BOOST_LOG(fatal) << "Failed to revert to self after impersonation: "sv << winerror;
|
NULL,
|
||||||
std::abort();
|
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 {
|
else {
|
||||||
ret = CreateProcessW(NULL,
|
ret = CreateProcessW(NULL,
|
||||||
(LPWSTR) wcmd.c_str(),
|
(LPWSTR) wcmd.c_str(),
|
||||||
|
|
@ -535,30 +647,8 @@ namespace platf {
|
||||||
&process_info);
|
&process_info);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ret) {
|
// Use the results of the launch to create a bp::child object
|
||||||
// Since we are always spawning a process with a less privileged token than ourselves,
|
return create_boost_child_from_results(ret, cmd, ec, process_info, group);
|
||||||
// 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();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
|
|
@ -900,5 +990,4 @@ namespace platf {
|
||||||
|
|
||||||
return std::make_unique<qos_t>(flow_id);
|
return std::make_unique<qos_t>(flow_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace platf
|
} // namespace platf
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
#ifndef SUNSHINE_WINDOWS_MISC_H
|
#ifndef SUNSHINE_WINDOWS_MISC_H
|
||||||
#define SUNSHINE_WINDOWS_MISC_H
|
#define SUNSHINE_WINDOWS_MISC_H
|
||||||
|
|
||||||
#include <string_view>
|
#include <string_view>
|
||||||
#include <windows.h>
|
#include <windows.h>
|
||||||
#include <winnt.h>
|
#include <winnt.h>
|
||||||
|
|
|
||||||
|
|
@ -125,16 +125,21 @@ namespace proc {
|
||||||
});
|
});
|
||||||
|
|
||||||
for (; _app_prep_it != std::end(_app.prep_cmds); ++_app_prep_it) {
|
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() ?
|
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::filesystem::path(_app.working_dir);
|
||||||
BOOST_LOG(info) << "Executing Do Cmd: ["sv << cmd << ']';
|
BOOST_LOG(info) << "Executing Do Cmd: ["sv << cmd.do_cmd << ']';
|
||||||
auto child = platf::run_unprivileged(cmd, working_dir, _env, _pipe.get(), ec, nullptr);
|
auto child = platf::run_command(cmd.elevated, cmd.do_cmd, working_dir, _env, _pipe.get(), ec, nullptr);
|
||||||
|
|
||||||
if (ec) {
|
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;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -142,7 +147,7 @@ namespace proc {
|
||||||
auto ret = child.exit_code();
|
auto ret = child.exit_code();
|
||||||
|
|
||||||
if (ret != 0) {
|
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;
|
return -1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -152,7 +157,7 @@ namespace proc {
|
||||||
find_working_directory(cmd, _env) :
|
find_working_directory(cmd, _env) :
|
||||||
boost::filesystem::path(_app.working_dir);
|
boost::filesystem::path(_app.working_dir);
|
||||||
BOOST_LOG(info) << "Spawning ["sv << cmd << "] in ["sv << 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) {
|
if (ec) {
|
||||||
BOOST_LOG(warning) << "Couldn't spawn ["sv << cmd << "]: System: "sv << ec.message();
|
BOOST_LOG(warning) << "Couldn't spawn ["sv << cmd << "]: System: "sv << ec.message();
|
||||||
}
|
}
|
||||||
|
|
@ -170,7 +175,7 @@ namespace proc {
|
||||||
find_working_directory(_app.cmd, _env) :
|
find_working_directory(_app.cmd, _env) :
|
||||||
boost::filesystem::path(_app.working_dir);
|
boost::filesystem::path(_app.working_dir);
|
||||||
BOOST_LOG(info) << "Executing: ["sv << _app.cmd << "] in ["sv << 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) {
|
if (ec) {
|
||||||
BOOST_LOG(warning) << "Couldn't run ["sv << _app.cmd << "]: System: "sv << ec.message();
|
BOOST_LOG(warning) << "Couldn't run ["sv << _app.cmd << "]: System: "sv << ec.message();
|
||||||
return -1;
|
return -1;
|
||||||
|
|
@ -208,17 +213,17 @@ namespace proc {
|
||||||
_app_id = -1;
|
_app_id = -1;
|
||||||
|
|
||||||
for (; _app_prep_it != _app_prep_begin; --_app_prep_it) {
|
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;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
boost::filesystem::path working_dir = _app.working_dir.empty() ?
|
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::filesystem::path(_app.working_dir);
|
||||||
BOOST_LOG(info) << "Executing Undo Cmd: ["sv << cmd << ']';
|
BOOST_LOG(info) << "Executing Undo Cmd: ["sv << cmd.undo_cmd << ']';
|
||||||
auto child = platf::run_unprivileged(cmd, working_dir, _env, _pipe.get(), ec, nullptr);
|
auto child = platf::run_command(cmd.elevated, cmd.undo_cmd, working_dir, _env, _pipe.get(), ec, nullptr);
|
||||||
|
|
||||||
if (ec) {
|
if (ec) {
|
||||||
BOOST_LOG(warning) << "System: "sv << ec.message();
|
BOOST_LOG(warning) << "System: "sv << ec.message();
|
||||||
|
|
@ -481,6 +486,7 @@ namespace proc {
|
||||||
auto cmd = app_node.get_optional<std::string>("cmd"s);
|
auto cmd = app_node.get_optional<std::string>("cmd"s);
|
||||||
auto image_path = app_node.get_optional<std::string>("image-path"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 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;
|
std::vector<proc::cmd_t> prep_cmds;
|
||||||
if (!exclude_global_prep.value_or(false)) {
|
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 do_cmd = parse_env_val(this_env, prep_cmd.do_cmd);
|
||||||
auto undo_cmd = parse_env_val(this_env, prep_cmd.undo_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());
|
prep_cmds.reserve(prep_cmds.size() + prep_nodes.size());
|
||||||
for (auto &[_, prep_node] : prep_nodes) {
|
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 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(
|
||||||
prep_cmds.emplace_back(std::move(do_cmd), parse_env_val(this_env, *undo_cmd));
|
parse_env_val(this_env, do_cmd.value_or("")),
|
||||||
}
|
parse_env_val(this_env, undo_cmd.value_or("")),
|
||||||
else {
|
std::move(elevated.value_or(false)));
|
||||||
prep_cmds.emplace_back(std::move(do_cmd));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -536,6 +544,8 @@ namespace proc {
|
||||||
ctx.image_path = parse_env_val(this_env, *image_path);
|
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++);
|
auto possible_ids = calculate_app_id(name, ctx.image_path, i++);
|
||||||
if (ids.count(std::get<0>(possible_ids)) == 0) {
|
if (ids.count(std::get<0>(possible_ids)) == 0) {
|
||||||
// Avoid using index to generate id if possible
|
// Avoid using index to generate id if possible
|
||||||
|
|
|
||||||
|
|
@ -48,6 +48,7 @@ namespace proc {
|
||||||
std::string output;
|
std::string output;
|
||||||
std::string image_path;
|
std::string image_path;
|
||||||
std::string id;
|
std::string id;
|
||||||
|
bool elevated;
|
||||||
};
|
};
|
||||||
|
|
||||||
class proc_t {
|
class proc_t {
|
||||||
|
|
|
||||||
|
|
@ -62,7 +62,7 @@ namespace system_tray {
|
||||||
|
|
||||||
boost::process::environment _env = boost::this_process::environment();
|
boost::process::environment _env = boost::this_process::environment();
|
||||||
std::error_code ec;
|
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) {
|
if (ec) {
|
||||||
BOOST_LOG(warning) << "Couldn't open url ["sv << url << "]: System: "sv << ec.message();
|
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">
|
<tr v-for="(app,i) in apps" :key="i">
|
||||||
<td>{{app.name}}</td>
|
<td>{{app.name}}</td>
|
||||||
<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)">
|
<button class="btn btn-danger" @click="showDeleteForm(i)">
|
||||||
Delete
|
<i class="fas fa-trash"></i> Delete
|
||||||
</button>
|
</button>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
@ -56,29 +58,51 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!--prep-cmd-->
|
<!--prep-cmd-->
|
||||||
<div class="mb-3 d-flex flex-column">
|
<div class="mb-3">
|
||||||
<div class="mb-3">
|
<label for="excludeGlobalPrep" class="form-label"
|
||||||
<label for="excludeGlobalPrep" class="form-label">Global Prep Commands</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
|
||||||
</select>
|
id="excludeGlobalPrep"
|
||||||
<div class="form-text">
|
class="form-select"
|
||||||
Enable/Disable the execution of Global Prep Commands for this application.
|
v-model="editForm['exclude-global-prep-cmd']"
|
||||||
</div>
|
>
|
||||||
|
<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>
|
||||||
|
<div class="mb-3">
|
||||||
<label for="appName" class="form-label">Command Preparations</label>
|
<label for="appName" class="form-label">Command Preparations</label>
|
||||||
<div class="form-text">
|
<div class="form-text">
|
||||||
A list of commands to be run before/after this application. <br />
|
A list of commands to be run before/after this application.<br />
|
||||||
If any of the prep-commands fail, starting the application is aborted
|
If any of the prep-commands fail, starting the application is aborted.
|
||||||
</div>
|
</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>
|
<thead>
|
||||||
<th class="precmd-head">Do</th>
|
<tr>
|
||||||
<th class="precmd-head">Undo</th>
|
<th scope="col"><i class="fas fa-play"></i> Do Command</th>
|
||||||
<th style="width: 48px"></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>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr v-for="(c,i) in editForm['prep-cmd']">
|
<tr v-for="(c, i) in editForm['prep-cmd']">
|
||||||
<td>
|
<td>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
|
|
@ -93,24 +117,35 @@
|
||||||
v-model="c.undo"
|
v-model="c.undo"
|
||||||
/>
|
/>
|
||||||
</td>
|
</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>
|
<td>
|
||||||
<button
|
<button
|
||||||
class="btn btn-danger"
|
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>
|
</button>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
<button
|
|
||||||
class="mt-2 btn btn-success"
|
|
||||||
style="margin: 0 auto"
|
|
||||||
@click="addPrepCmd"
|
|
||||||
>
|
|
||||||
+ Add
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
<!--detatched-->
|
<!--detatched-->
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
|
|
@ -170,9 +205,28 @@
|
||||||
v-model="editForm['working-dir']"
|
v-model="editForm['working-dir']"
|
||||||
/>
|
/>
|
||||||
<div id="appWorkingDirHelp" class="form-text">
|
<div id="appWorkingDirHelp" class="form-text">
|
||||||
The working directory that should be passed to the process.
|
The working directory that should be passed to the process. For
|
||||||
For example, some applications use the working directory to search for configuration files.
|
example, some applications use the working directory to search for
|
||||||
If not set, Sunshine will default to the parent directory of the command
|
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>
|
||||||
</div>
|
</div>
|
||||||
<!-- Image path -->
|
<!-- Image path -->
|
||||||
|
|
@ -180,36 +234,60 @@
|
||||||
<label for="appImagePath" class="form-label">Image</label>
|
<label for="appImagePath" class="form-label">Image</label>
|
||||||
<div class="input-group dropup">
|
<div class="input-group dropup">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
class="form-control monospace"
|
class="form-control monospace"
|
||||||
id="appImagePath"
|
id="appImagePath"
|
||||||
aria-describedby="appImagePathHelp"
|
aria-describedby="appImagePathHelp"
|
||||||
v-model="editForm['image-path']"
|
v-model="editForm['image-path']"
|
||||||
/>
|
/>
|
||||||
<button class="btn btn-secondary dropdown-toggle" type="button" id="findCoverToggle" data-bs-toggle="dropdown"
|
<button
|
||||||
data-bs-auto-close="outside" aria-expanded="false" v-dropdown-show="showCoverFinder"
|
class="btn btn-secondary dropdown-toggle"
|
||||||
ref="coverFinderDropdown">
|
type="button"
|
||||||
|
id="findCoverToggle"
|
||||||
|
data-bs-toggle="dropdown"
|
||||||
|
data-bs-auto-close="outside"
|
||||||
|
aria-expanded="false"
|
||||||
|
v-dropdown-show="showCoverFinder"
|
||||||
|
ref="coverFinderDropdown"
|
||||||
|
>
|
||||||
Find Cover
|
Find Cover
|
||||||
</button>
|
</button>
|
||||||
<div class="dropdown-menu dropdown-menu-end w-50 cover-finder overflow-hidden"
|
<div
|
||||||
aria-labelledby="findCoverToggle">
|
class="dropdown-menu dropdown-menu-end w-50 cover-finder overflow-hidden"
|
||||||
|
aria-labelledby="findCoverToggle"
|
||||||
|
>
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h4 class="modal-title">Covers Found</h4>
|
<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>
|
||||||
<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 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="cover-container">
|
||||||
<div class="spinner-border" role="status">
|
<div class="spinner-border" role="status">
|
||||||
<span class="visually-hidden">Loading...</span>
|
<span class="visually-hidden">Loading...</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-for="(cover,i) in coverCandidates" :key="i" class="col-12 col-sm-6 col-lg-4 mb-3"
|
<div
|
||||||
@click="useCover(cover)">
|
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">
|
<div class="cover-container result">
|
||||||
<img class="rounded" :src="cover.url"/>
|
<img class="rounded" :src="cover.url" />
|
||||||
</div>
|
</div>
|
||||||
<label class="d-block text-nowrap text-center text-truncate">
|
<label class="d-block text-nowrap text-center text-truncate">
|
||||||
{{cover.name}}
|
{{cover.name}}
|
||||||
|
|
@ -220,8 +298,8 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="appImagePathHelp" class="form-text">
|
<div id="appImagePathHelp" class="form-text">
|
||||||
Application icon/picture/image path that will be sent to client. Image must be a PNG file.
|
Application icon/picture/image path that will be sent to client. Image
|
||||||
If not set, Sunshine will send default box image.
|
must be a PNG file. If not set, Sunshine will send default box image.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!--buttons-->
|
<!--buttons-->
|
||||||
|
|
@ -234,10 +312,11 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-2" v-else>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
Vue.directive('dropdown-show', {
|
Vue.directive('dropdown-show', {
|
||||||
bind: function (el, binding) {
|
bind: function (el, binding) {
|
||||||
|
|
@ -255,7 +334,8 @@
|
||||||
detachedCmd: "",
|
detachedCmd: "",
|
||||||
coverSearching: false,
|
coverSearching: false,
|
||||||
coverFinderBusy: false,
|
coverFinderBusy: false,
|
||||||
coverCandidates: []
|
coverCandidates: [],
|
||||||
|
platform: "",
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
created() {
|
created() {
|
||||||
|
|
@ -265,6 +345,10 @@
|
||||||
console.log(r);
|
console.log(r);
|
||||||
this.apps = r.apps;
|
this.apps = r.apps;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
fetch("/api/config")
|
||||||
|
.then(r => r.json())
|
||||||
|
.then(r => this.platform = r.platform);
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
newApp() {
|
newApp() {
|
||||||
|
|
@ -274,6 +358,7 @@
|
||||||
cmd: [],
|
cmd: [],
|
||||||
index: -1,
|
index: -1,
|
||||||
"exclude-global-prep-cmd": false,
|
"exclude-global-prep-cmd": false,
|
||||||
|
elevated: false,
|
||||||
"prep-cmd": [],
|
"prep-cmd": [],
|
||||||
detached: [],
|
detached: [],
|
||||||
"image-path": ""
|
"image-path": ""
|
||||||
|
|
@ -290,6 +375,9 @@
|
||||||
this.$set(this.editForm, "detached", []);
|
this.$set(this.editForm, "detached", []);
|
||||||
if (this.editForm["exclude-global-prep-cmd"] === undefined)
|
if (this.editForm["exclude-global-prep-cmd"] === undefined)
|
||||||
this.$set(this.editForm, "exclude-global-prep-cmd", false);
|
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;
|
this.showEditForm = true;
|
||||||
},
|
},
|
||||||
showDeleteForm(id) {
|
showDeleteForm(id) {
|
||||||
|
|
@ -303,10 +391,16 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
addPrepCmd() {
|
addPrepCmd() {
|
||||||
this.editForm["prep-cmd"].push({
|
let template = {
|
||||||
do: "",
|
do: "",
|
||||||
undo: "",
|
undo: ""
|
||||||
});
|
};
|
||||||
|
|
||||||
|
if(this.platform === 'windows'){
|
||||||
|
template = {...template, elevated: false};
|
||||||
|
}
|
||||||
|
|
||||||
|
this.editForm["prep-cmd"].push(template);
|
||||||
},
|
},
|
||||||
showCoverFinder($event) {
|
showCoverFinder($event) {
|
||||||
this.coverCandidates = [];
|
this.coverCandidates = [];
|
||||||
|
|
|
||||||
|
|
@ -222,47 +222,55 @@
|
||||||
<div class="mb-3 d-flex flex-column">
|
<div class="mb-3 d-flex flex-column">
|
||||||
<label class="form-label">Command Preparations</label>
|
<label class="form-label">Command Preparations</label>
|
||||||
<div class="form-text">
|
<div class="form-text">
|
||||||
A list of commands to be run before/after all applications. <br />
|
Configure a list of commands to be executed before or after running any application.
|
||||||
If any of the prep-commands fail, starting the application is aborted.
|
If any of the specified preparation commands fail, the application launch process will be aborted.
|
||||||
</div>
|
</div>
|
||||||
<table v-if="global_prep_cmd.length > 0">
|
<table class="table" v-if="global_prep_cmd.length > 0">
|
||||||
<thead>
|
<thead>
|
||||||
<th>Do</th>
|
<tr>
|
||||||
<th>Undo</th>
|
<th scope="col"><i class="fas fa-play"></i> Do Command</th>
|
||||||
<th style="width: 48px"></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>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr v-for="(c,i) in global_prep_cmd">
|
<tr v-for="(c, i) in global_prep_cmd">
|
||||||
<td>
|
<td>
|
||||||
<input
|
<input type="text" class="form-control monospace" v-model="c.do" />
|
||||||
type="text"
|
|
||||||
class="form-control monospace"
|
|
||||||
v-model="c.do"
|
|
||||||
/>
|
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<input
|
<input type="text" class="form-control monospace" v-model="c.undo" />
|
||||||
type="text"
|
</td>
|
||||||
class="form-control monospace"
|
<td v-if="platform === 'windows'">
|
||||||
v-model="c.undo"
|
<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>
|
||||||
<td>
|
<td>
|
||||||
<button
|
<button class="btn btn-danger" @click="$delete(global_prep_cmd, i)">
|
||||||
class="btn btn-danger"
|
<i class="fas fa-trash"></i>
|
||||||
@click="global_prep_cmd.splice(i,1)"
|
</button>
|
||||||
>
|
<button class="btn btn-success" @click="add_global_prep_cmd">
|
||||||
×
|
<i class="fas fa-plus"></i>
|
||||||
</button>
|
</button>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
<button
|
<button class="mt-2 btn btn-success" style="margin: 0 auto" @click="add_global_prep_cmd">
|
||||||
class="mt-2 btn btn-success"
|
|
||||||
style="margin: 0 auto"
|
|
||||||
@click="add_global_prep_cmd"
|
|
||||||
>
|
|
||||||
+ Add
|
+ Add
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -1198,10 +1206,15 @@
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
add_global_prep_cmd() {
|
add_global_prep_cmd() {
|
||||||
this.global_prep_cmd.push({
|
let template = {
|
||||||
do: "",
|
do: "",
|
||||||
undo: "",
|
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})
|
${PLATFORM_LIBRARIES})
|
||||||
target_compile_options(dxgi-info PRIVATE ${SUNSHINE_COMPILE_OPTIONS})
|
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)
|
add_executable(audio-info audio.cpp)
|
||||||
set_target_properties(audio-info PROPERTIES CXX_STANDARD 17)
|
set_target_properties(audio-info PROPERTIES CXX_STANDARD 17)
|
||||||
target_link_libraries(audio-info
|
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…
Add table
Add a link
Reference in a new issue