mirror of
https://github.com/thinkonmay/sunshine-sdk.git
synced 2026-08-15 14:21:12 +00:00
remove modules
This commit is contained in:
parent
5d79fdbdfa
commit
70a00f2a58
32
.gitmodules
vendored
32
.gitmodules
vendored
@ -1,27 +1,7 @@
|
||||
[submodule "packaging/linux/flatpak/deps/org.flatpak.Builder.BaseApp"]
|
||||
path = packaging/linux/flatpak/deps/org.flatpak.Builder.BaseApp
|
||||
url = https://github.com/flathub/org.flatpak.Builder.BaseApp
|
||||
branch = branch/23.08
|
||||
[submodule "packaging/linux/flatpak/deps/shared-modules"]
|
||||
path = packaging/linux/flatpak/deps/shared-modules
|
||||
url = https://github.com/flathub/shared-modules
|
||||
branch = master
|
||||
[submodule "third-party/build-deps"]
|
||||
path = third-party/build-deps
|
||||
url = https://github.com/LizardByte/build-deps.git
|
||||
branch = dist
|
||||
[submodule "third-party/googletest"]
|
||||
path = third-party/googletest
|
||||
url = https://github.com/google/googletest/
|
||||
branch = v1.14.x
|
||||
[submodule "third-party/moonlight-common-c"]
|
||||
path = third-party/moonlight-common-c
|
||||
url = https://github.com/moonlight-stream/moonlight-common-c.git
|
||||
branch = master
|
||||
[submodule "third-party/nanors"]
|
||||
path = third-party/nanors
|
||||
url = https://github.com/sleepybishop/nanors.git
|
||||
branch = master
|
||||
[submodule "third-party/nv-codec-headers"]
|
||||
path = third-party/nv-codec-headers
|
||||
url = https://github.com/FFmpeg/nv-codec-headers
|
||||
@ -30,18 +10,10 @@
|
||||
path = third-party/nvapi-open-source-sdk
|
||||
url = https://github.com/LizardByte/nvapi-open-source-sdk
|
||||
branch = sdk
|
||||
[submodule "third-party/Simple-Web-Server"]
|
||||
path = third-party/Simple-Web-Server
|
||||
url = https://gitlab.com/eidheim/Simple-Web-Server.git
|
||||
branch = master
|
||||
[submodule "third-party/TPCircularBuffer"]
|
||||
path = third-party/TPCircularBuffer
|
||||
url = https://github.com/michaeltyson/TPCircularBuffer
|
||||
branch = master
|
||||
[submodule "third-party/tray"]
|
||||
path = third-party/tray
|
||||
url = https://github.com/LizardByte/tray
|
||||
branch = master
|
||||
[submodule "third-party/ViGEmClient"]
|
||||
path = third-party/ViGEmClient
|
||||
url = https://github.com/LizardByte/Virtual-Gamepad-Emulation-Client.git
|
||||
@ -54,3 +26,7 @@
|
||||
path = third-party/wlr-protocols
|
||||
url = https://gitlab.freedesktop.org/wlroots/wlr-protocols
|
||||
branch = master
|
||||
[submodule "third-party/moonlight-common-c"]
|
||||
path = third-party/moonlight-common-c
|
||||
url = https://github.com/moonlight-stream/moonlight-common-c.git
|
||||
branch = master
|
||||
@ -49,22 +49,10 @@ configure_file("${CMAKE_SOURCE_DIR}/src/version.h.in" version.h @ONLY)
|
||||
include_directories("${CMAKE_CURRENT_BINARY_DIR}") # required for importing version.h
|
||||
|
||||
set(SUNSHINE_TARGET_FILES
|
||||
"${CMAKE_SOURCE_DIR}/third-party/nanors/rs.c"
|
||||
"${CMAKE_SOURCE_DIR}/third-party/nanors/rs.h"
|
||||
"${CMAKE_SOURCE_DIR}/third-party/moonlight-common-c/src/Input.h"
|
||||
"${CMAKE_SOURCE_DIR}/third-party/moonlight-common-c/src/Rtsp.h"
|
||||
"${CMAKE_SOURCE_DIR}/third-party/moonlight-common-c/src/RtspParser.c"
|
||||
"${CMAKE_SOURCE_DIR}/third-party/moonlight-common-c/src/Video.h"
|
||||
"${CMAKE_SOURCE_DIR}/third-party/tray/tray.h"
|
||||
"${CMAKE_SOURCE_DIR}/src/upnp.cpp"
|
||||
"${CMAKE_SOURCE_DIR}/src/upnp.h"
|
||||
"${CMAKE_SOURCE_DIR}/src/cbs.cpp"
|
||||
"${CMAKE_SOURCE_DIR}/src/utility.h"
|
||||
"${CMAKE_SOURCE_DIR}/src/uuid.h"
|
||||
"${CMAKE_SOURCE_DIR}/src/config.h"
|
||||
"${CMAKE_SOURCE_DIR}/src/config.cpp"
|
||||
"${CMAKE_SOURCE_DIR}/src/entry_handler.cpp"
|
||||
"${CMAKE_SOURCE_DIR}/src/entry_handler.h"
|
||||
"${CMAKE_SOURCE_DIR}/src/file_handler.cpp"
|
||||
"${CMAKE_SOURCE_DIR}/src/file_handler.h"
|
||||
"${CMAKE_SOURCE_DIR}/src/globals.cpp"
|
||||
@ -73,18 +61,6 @@ set(SUNSHINE_TARGET_FILES
|
||||
"${CMAKE_SOURCE_DIR}/src/logging.h"
|
||||
"${CMAKE_SOURCE_DIR}/src/main.cpp"
|
||||
"${CMAKE_SOURCE_DIR}/src/main.h"
|
||||
"${CMAKE_SOURCE_DIR}/src/crypto.cpp"
|
||||
"${CMAKE_SOURCE_DIR}/src/crypto.h"
|
||||
"${CMAKE_SOURCE_DIR}/src/nvhttp.cpp"
|
||||
"${CMAKE_SOURCE_DIR}/src/nvhttp.h"
|
||||
"${CMAKE_SOURCE_DIR}/src/httpcommon.cpp"
|
||||
"${CMAKE_SOURCE_DIR}/src/httpcommon.h"
|
||||
"${CMAKE_SOURCE_DIR}/src/confighttp.cpp"
|
||||
"${CMAKE_SOURCE_DIR}/src/confighttp.h"
|
||||
"${CMAKE_SOURCE_DIR}/src/rtsp.cpp"
|
||||
"${CMAKE_SOURCE_DIR}/src/rtsp.h"
|
||||
"${CMAKE_SOURCE_DIR}/src/stream.cpp"
|
||||
"${CMAKE_SOURCE_DIR}/src/stream.h"
|
||||
"${CMAKE_SOURCE_DIR}/src/video.cpp"
|
||||
"${CMAKE_SOURCE_DIR}/src/video.h"
|
||||
"${CMAKE_SOURCE_DIR}/src/video_colorspace.cpp"
|
||||
@ -94,13 +70,7 @@ set(SUNSHINE_TARGET_FILES
|
||||
"${CMAKE_SOURCE_DIR}/src/audio.cpp"
|
||||
"${CMAKE_SOURCE_DIR}/src/audio.h"
|
||||
"${CMAKE_SOURCE_DIR}/src/platform/common.h"
|
||||
"${CMAKE_SOURCE_DIR}/src/process.cpp"
|
||||
"${CMAKE_SOURCE_DIR}/src/process.h"
|
||||
"${CMAKE_SOURCE_DIR}/src/network.cpp"
|
||||
"${CMAKE_SOURCE_DIR}/src/network.h"
|
||||
"${CMAKE_SOURCE_DIR}/src/move_by_copy.h"
|
||||
"${CMAKE_SOURCE_DIR}/src/system_tray.cpp"
|
||||
"${CMAKE_SOURCE_DIR}/src/system_tray.h"
|
||||
"${CMAKE_SOURCE_DIR}/src/task_pool.h"
|
||||
"${CMAKE_SOURCE_DIR}/src/thread_pool.h"
|
||||
"${CMAKE_SOURCE_DIR}/src/thread_safe.h"
|
||||
@ -122,9 +92,6 @@ include_directories("${CMAKE_SOURCE_DIR}")
|
||||
include_directories(
|
||||
SYSTEM
|
||||
"${CMAKE_SOURCE_DIR}/third-party"
|
||||
"${CMAKE_SOURCE_DIR}/third-party/moonlight-common-c/enet/include"
|
||||
"${CMAKE_SOURCE_DIR}/third-party/nanors"
|
||||
"${CMAKE_SOURCE_DIR}/third-party/nanors/deps/obl"
|
||||
${FFMPEG_INCLUDE_DIRS}
|
||||
${PLATFORM_INCLUDE_DIRS}
|
||||
)
|
||||
@ -132,7 +99,6 @@ include_directories(
|
||||
list(APPEND SUNSHINE_EXTERNAL_LIBRARIES
|
||||
${MINIUPNP_LIBRARIES}
|
||||
${CMAKE_THREAD_LIBS_INIT}
|
||||
enet
|
||||
opus
|
||||
${FFMPEG_LIBRARIES}
|
||||
${Boost_LIBRARIES}
|
||||
|
||||
@ -222,7 +222,6 @@ if(${SUNSHINE_ENABLE_TRAY})
|
||||
include_directories(SYSTEM ${APPINDICATOR_INCLUDE_DIRS} ${LIBNOTIFY_INCLUDE_DIRS})
|
||||
link_directories(${APPINDICATOR_LIBRARY_DIRS} ${LIBNOTIFY_LIBRARY_DIRS})
|
||||
|
||||
list(APPEND PLATFORM_TARGET_FILES "${CMAKE_SOURCE_DIR}/third-party/tray/tray_linux.c")
|
||||
list(APPEND SUNSHINE_EXTERNAL_LIBRARIES ${APPINDICATOR_LIBRARIES} ${LIBNOTIFY_LIBRARIES})
|
||||
endif()
|
||||
else()
|
||||
@ -235,7 +234,6 @@ if (${SUNSHINE_TRAY} EQUAL 0 AND SUNSHINE_REQUIRE_TRAY)
|
||||
endif()
|
||||
|
||||
list(APPEND PLATFORM_TARGET_FILES
|
||||
"${CMAKE_SOURCE_DIR}/src/platform/linux/publish.cpp"
|
||||
"${CMAKE_SOURCE_DIR}/src/platform/linux/graphics.h"
|
||||
"${CMAKE_SOURCE_DIR}/src/platform/linux/graphics.cpp"
|
||||
"${CMAKE_SOURCE_DIR}/src/platform/linux/misc.h"
|
||||
|
||||
@ -36,7 +36,6 @@ set(PLATFORM_TARGET_FILES
|
||||
"${CMAKE_SOURCE_DIR}/src/platform/macos/misc.h"
|
||||
"${CMAKE_SOURCE_DIR}/src/platform/macos/nv12_zero_device.cpp"
|
||||
"${CMAKE_SOURCE_DIR}/src/platform/macos/nv12_zero_device.h"
|
||||
"${CMAKE_SOURCE_DIR}/src/platform/macos/publish.cpp"
|
||||
"${CMAKE_SOURCE_DIR}/third-party/TPCircularBuffer/TPCircularBuffer.c"
|
||||
"${CMAKE_SOURCE_DIR}/third-party/TPCircularBuffer/TPCircularBuffer.h"
|
||||
${APPLE_PLIST_FILE})
|
||||
@ -44,6 +43,4 @@ set(PLATFORM_TARGET_FILES
|
||||
if(SUNSHINE_ENABLE_TRAY)
|
||||
list(APPEND SUNSHINE_EXTERNAL_LIBRARIES
|
||||
${COCOA})
|
||||
list(APPEND PLATFORM_TARGET_FILES
|
||||
"${CMAKE_SOURCE_DIR}/third-party/tray/tray_darwin.m")
|
||||
endif()
|
||||
|
||||
@ -31,7 +31,6 @@ file(GLOB NVPREFS_FILES CONFIGURE_DEPENDS
|
||||
include_directories(SYSTEM "${CMAKE_SOURCE_DIR}/third-party/ViGEmClient/include")
|
||||
|
||||
set(PLATFORM_TARGET_FILES
|
||||
"${CMAKE_SOURCE_DIR}/src/platform/windows/publish.cpp"
|
||||
"${CMAKE_SOURCE_DIR}/src/platform/windows/misc.h"
|
||||
"${CMAKE_SOURCE_DIR}/src/platform/windows/misc.cpp"
|
||||
"${CMAKE_SOURCE_DIR}/src/platform/windows/input.cpp"
|
||||
@ -64,12 +63,12 @@ list(PREPEND PLATFORM_LIBRARIES
|
||||
userenv
|
||||
synchronization.lib
|
||||
avrt
|
||||
ole32
|
||||
oleaut32
|
||||
imm32
|
||||
winmm
|
||||
version
|
||||
iphlpapi
|
||||
shlwapi
|
||||
PkgConfig::NLOHMANN_JSON
|
||||
${CURL_STATIC_LIBRARIES})
|
||||
|
||||
if(SUNSHINE_ENABLE_TRAY)
|
||||
list(APPEND PLATFORM_TARGET_FILES
|
||||
"${CMAKE_SOURCE_DIR}/third-party/tray/tray_windows.c")
|
||||
endif()
|
||||
|
||||
@ -1,13 +1,6 @@
|
||||
# load common dependencies
|
||||
# this file will also load platform specific dependencies
|
||||
|
||||
# submodules
|
||||
# moonlight common library
|
||||
set(ENET_NO_INSTALL ON CACHE BOOL "Don't install any libraries build for enet")
|
||||
add_subdirectory("${CMAKE_SOURCE_DIR}/third-party/moonlight-common-c/enet")
|
||||
|
||||
# web server
|
||||
add_subdirectory("${CMAKE_SOURCE_DIR}/third-party/Simple-Web-Server")
|
||||
|
||||
# common dependencies
|
||||
find_package(OpenSSL REQUIRED)
|
||||
|
||||
@ -51,14 +51,6 @@ target_compile_options(sunshine PRIVATE $<$<COMPILE_LANGUAGE:CXX>:${SUNSHINE_COM
|
||||
|
||||
# custom compile flags, must be after adding tests
|
||||
|
||||
# src/upnp
|
||||
set_source_files_properties("${CMAKE_SOURCE_DIR}/src/upnp.cpp"
|
||||
PROPERTIES COMPILE_FLAGS -Wno-pedantic)
|
||||
|
||||
# third-party/nanors
|
||||
set_source_files_properties("${CMAKE_SOURCE_DIR}/third-party/nanors/rs.c"
|
||||
PROPERTIES COMPILE_FLAGS "-include deps/obl/autoshim.h -ftree-vectorize")
|
||||
|
||||
# third-party/ViGEmClient
|
||||
set(VIGEM_COMPILE_FLAGS "")
|
||||
string(APPEND VIGEM_COMPILE_FLAGS "-Wno-unknown-pragmas ")
|
||||
|
||||
861
src/config.cpp
861
src/config.cpp
@ -16,11 +16,8 @@
|
||||
#include <boost/property_tree/ptree.hpp>
|
||||
|
||||
#include "config.h"
|
||||
#include "entry_handler.h"
|
||||
#include "file_handler.h"
|
||||
#include "logging.h"
|
||||
#include "nvhttp.h"
|
||||
#include "rtsp.h"
|
||||
#include "utility.h"
|
||||
|
||||
#include "platform/common.h"
|
||||
@ -463,862 +460,4 @@ namespace config {
|
||||
platf::appdata().string() + "/sunshine.log", // log file
|
||||
{}, // prep commands
|
||||
};
|
||||
|
||||
bool
|
||||
endline(char ch) {
|
||||
return ch == '\r' || ch == '\n';
|
||||
}
|
||||
|
||||
bool
|
||||
space_tab(char ch) {
|
||||
return ch == ' ' || ch == '\t';
|
||||
}
|
||||
|
||||
bool
|
||||
whitespace(char ch) {
|
||||
return space_tab(ch) || endline(ch);
|
||||
}
|
||||
|
||||
std::string
|
||||
to_string(const char *begin, const char *end) {
|
||||
std::string result;
|
||||
|
||||
KITTY_WHILE_LOOP(auto pos = begin, pos != end, {
|
||||
auto comment = std::find(pos, end, '#');
|
||||
auto endl = std::find_if(comment, end, endline);
|
||||
|
||||
result.append(pos, comment);
|
||||
|
||||
pos = endl;
|
||||
})
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
template <class It>
|
||||
It
|
||||
skip_list(It skipper, It end) {
|
||||
int stack = 1;
|
||||
while (skipper != end && stack) {
|
||||
if (*skipper == '[') {
|
||||
++stack;
|
||||
}
|
||||
if (*skipper == ']') {
|
||||
--stack;
|
||||
}
|
||||
|
||||
++skipper;
|
||||
}
|
||||
|
||||
return skipper;
|
||||
}
|
||||
|
||||
std::pair<
|
||||
std::string_view::const_iterator,
|
||||
std::optional<std::pair<std::string, std::string>>>
|
||||
parse_option(std::string_view::const_iterator begin, std::string_view::const_iterator end) {
|
||||
begin = std::find_if_not(begin, end, whitespace);
|
||||
auto endl = std::find_if(begin, end, endline);
|
||||
auto endc = std::find(begin, endl, '#');
|
||||
endc = std::find_if(std::make_reverse_iterator(endc), std::make_reverse_iterator(begin), std::not_fn(whitespace)).base();
|
||||
|
||||
auto eq = std::find(begin, endc, '=');
|
||||
if (eq == endc || eq == begin) {
|
||||
return std::make_pair(endl, std::nullopt);
|
||||
}
|
||||
|
||||
auto end_name = std::find_if_not(std::make_reverse_iterator(eq), std::make_reverse_iterator(begin), space_tab).base();
|
||||
auto begin_val = std::find_if_not(eq + 1, endc, space_tab);
|
||||
|
||||
if (begin_val == endl) {
|
||||
return std::make_pair(endl, std::nullopt);
|
||||
}
|
||||
|
||||
// Lists might contain newlines
|
||||
if (*begin_val == '[') {
|
||||
endl = skip_list(begin_val + 1, end);
|
||||
if (endl == end) {
|
||||
std::cout << "Warning: Config option ["sv << to_string(begin, end_name) << "] Missing ']'"sv;
|
||||
|
||||
return std::make_pair(endl, std::nullopt);
|
||||
}
|
||||
}
|
||||
|
||||
return std::make_pair(
|
||||
endl,
|
||||
std::make_pair(to_string(begin, end_name), to_string(begin_val, endl)));
|
||||
}
|
||||
|
||||
std::unordered_map<std::string, std::string>
|
||||
parse_config(const std::string_view &file_content) {
|
||||
std::unordered_map<std::string, std::string> vars;
|
||||
|
||||
auto pos = std::begin(file_content);
|
||||
auto end = std::end(file_content);
|
||||
|
||||
while (pos < end) {
|
||||
// auto newline = std::find_if(pos, end, [](auto ch) { return ch == '\n' || ch == '\r'; });
|
||||
TUPLE_2D(endl, var, parse_option(pos, end));
|
||||
|
||||
pos = endl;
|
||||
if (pos != end) {
|
||||
pos += (*pos == '\r') ? 2 : 1;
|
||||
}
|
||||
|
||||
if (!var) {
|
||||
continue;
|
||||
}
|
||||
|
||||
vars.emplace(std::move(*var));
|
||||
}
|
||||
|
||||
return vars;
|
||||
}
|
||||
|
||||
void
|
||||
string_f(std::unordered_map<std::string, std::string> &vars, const std::string &name, std::string &input) {
|
||||
auto it = vars.find(name);
|
||||
if (it == std::end(vars)) {
|
||||
return;
|
||||
}
|
||||
|
||||
input = std::move(it->second);
|
||||
|
||||
vars.erase(it);
|
||||
}
|
||||
|
||||
template <typename T, typename F>
|
||||
void
|
||||
generic_f(std::unordered_map<std::string, std::string> &vars, const std::string &name, T &input, F &&f) {
|
||||
std::string tmp;
|
||||
string_f(vars, name, tmp);
|
||||
if (!tmp.empty()) {
|
||||
input = f(tmp);
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
string_restricted_f(std::unordered_map<std::string, std::string> &vars, const std::string &name, std::string &input, const std::vector<std::string_view> &allowed_vals) {
|
||||
std::string temp;
|
||||
string_f(vars, name, temp);
|
||||
|
||||
for (auto &allowed_val : allowed_vals) {
|
||||
if (temp == allowed_val) {
|
||||
input = std::move(temp);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
path_f(std::unordered_map<std::string, std::string> &vars, const std::string &name, fs::path &input) {
|
||||
// appdata needs to be retrieved once only
|
||||
static auto appdata = platf::appdata();
|
||||
|
||||
std::string temp;
|
||||
string_f(vars, name, temp);
|
||||
|
||||
if (!temp.empty()) {
|
||||
input = temp;
|
||||
}
|
||||
|
||||
if (input.is_relative()) {
|
||||
input = appdata / input;
|
||||
}
|
||||
|
||||
auto dir = input;
|
||||
dir.remove_filename();
|
||||
|
||||
// Ensure the directories exists
|
||||
if (!fs::exists(dir)) {
|
||||
fs::create_directories(dir);
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
path_f(std::unordered_map<std::string, std::string> &vars, const std::string &name, std::string &input) {
|
||||
fs::path temp = input;
|
||||
|
||||
path_f(vars, name, temp);
|
||||
|
||||
input = temp.string();
|
||||
}
|
||||
|
||||
void
|
||||
int_f(std::unordered_map<std::string, std::string> &vars, const std::string &name, int &input) {
|
||||
auto it = vars.find(name);
|
||||
|
||||
if (it == std::end(vars)) {
|
||||
return;
|
||||
}
|
||||
|
||||
std::string_view val = it->second;
|
||||
|
||||
// If value is something like: "756" instead of 756
|
||||
if (val.size() >= 2 && val[0] == '"') {
|
||||
val = val.substr(1, val.size() - 2);
|
||||
}
|
||||
|
||||
// If that integer is in hexadecimal
|
||||
if (val.size() >= 2 && val.substr(0, 2) == "0x"sv) {
|
||||
input = util::from_hex<int>(val.substr(2));
|
||||
}
|
||||
else {
|
||||
input = util::from_view(val);
|
||||
}
|
||||
|
||||
vars.erase(it);
|
||||
}
|
||||
|
||||
void
|
||||
int_f(std::unordered_map<std::string, std::string> &vars, const std::string &name, std::optional<int> &input) {
|
||||
auto it = vars.find(name);
|
||||
|
||||
if (it == std::end(vars)) {
|
||||
return;
|
||||
}
|
||||
|
||||
std::string_view val = it->second;
|
||||
|
||||
// If value is something like: "756" instead of 756
|
||||
if (val.size() >= 2 && val[0] == '"') {
|
||||
val = val.substr(1, val.size() - 2);
|
||||
}
|
||||
|
||||
// If that integer is in hexadecimal
|
||||
if (val.size() >= 2 && val.substr(0, 2) == "0x"sv) {
|
||||
input = util::from_hex<int>(val.substr(2));
|
||||
}
|
||||
else {
|
||||
input = util::from_view(val);
|
||||
}
|
||||
|
||||
vars.erase(it);
|
||||
}
|
||||
|
||||
template <class F>
|
||||
void
|
||||
int_f(std::unordered_map<std::string, std::string> &vars, const std::string &name, int &input, F &&f) {
|
||||
std::string tmp;
|
||||
string_f(vars, name, tmp);
|
||||
if (!tmp.empty()) {
|
||||
input = f(tmp);
|
||||
}
|
||||
}
|
||||
|
||||
template <class F>
|
||||
void
|
||||
int_f(std::unordered_map<std::string, std::string> &vars, const std::string &name, std::optional<int> &input, F &&f) {
|
||||
std::string tmp;
|
||||
string_f(vars, name, tmp);
|
||||
if (!tmp.empty()) {
|
||||
input = f(tmp);
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
int_between_f(std::unordered_map<std::string, std::string> &vars, const std::string &name, int &input, const std::pair<int, int> &range) {
|
||||
int temp = input;
|
||||
|
||||
int_f(vars, name, temp);
|
||||
|
||||
TUPLE_2D_REF(lower, upper, range);
|
||||
if (temp >= lower && temp <= upper) {
|
||||
input = temp;
|
||||
}
|
||||
}
|
||||
|
||||
bool
|
||||
to_bool(std::string &boolean) {
|
||||
std::for_each(std::begin(boolean), std::end(boolean), [](char ch) { return (char) std::tolower(ch); });
|
||||
|
||||
return boolean == "true"sv ||
|
||||
boolean == "yes"sv ||
|
||||
boolean == "enable"sv ||
|
||||
boolean == "enabled"sv ||
|
||||
boolean == "on"sv ||
|
||||
(std::find(std::begin(boolean), std::end(boolean), '1') != std::end(boolean));
|
||||
}
|
||||
|
||||
void
|
||||
bool_f(std::unordered_map<std::string, std::string> &vars, const std::string &name, bool &input) {
|
||||
std::string tmp;
|
||||
string_f(vars, name, tmp);
|
||||
|
||||
if (tmp.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
input = to_bool(tmp);
|
||||
}
|
||||
|
||||
void
|
||||
double_f(std::unordered_map<std::string, std::string> &vars, const std::string &name, double &input) {
|
||||
std::string tmp;
|
||||
string_f(vars, name, tmp);
|
||||
|
||||
if (tmp.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
char *c_str_p;
|
||||
auto val = std::strtod(tmp.c_str(), &c_str_p);
|
||||
|
||||
if (c_str_p == tmp.c_str()) {
|
||||
return;
|
||||
}
|
||||
|
||||
input = val;
|
||||
}
|
||||
|
||||
void
|
||||
double_between_f(std::unordered_map<std::string, std::string> &vars, const std::string &name, double &input, const std::pair<double, double> &range) {
|
||||
double temp = input;
|
||||
|
||||
double_f(vars, name, temp);
|
||||
|
||||
TUPLE_2D_REF(lower, upper, range);
|
||||
if (temp >= lower && temp <= upper) {
|
||||
input = temp;
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
list_string_f(std::unordered_map<std::string, std::string> &vars, const std::string &name, std::vector<std::string> &input) {
|
||||
std::string string;
|
||||
string_f(vars, name, string);
|
||||
|
||||
if (string.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
input.clear();
|
||||
|
||||
auto begin = std::cbegin(string);
|
||||
if (*begin == '[') {
|
||||
++begin;
|
||||
}
|
||||
|
||||
begin = std::find_if_not(begin, std::cend(string), whitespace);
|
||||
if (begin == std::cend(string)) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto pos = begin;
|
||||
while (pos < std::cend(string)) {
|
||||
if (*pos == '[') {
|
||||
pos = skip_list(pos + 1, std::cend(string)) + 1;
|
||||
}
|
||||
else if (*pos == ']') {
|
||||
break;
|
||||
}
|
||||
else if (*pos == ',') {
|
||||
input.emplace_back(begin, pos);
|
||||
pos = begin = std::find_if_not(pos + 1, std::cend(string), whitespace);
|
||||
}
|
||||
else {
|
||||
++pos;
|
||||
}
|
||||
}
|
||||
|
||||
if (pos != begin) {
|
||||
input.emplace_back(begin, pos);
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
list_prep_cmd_f(std::unordered_map<std::string, std::string> &vars, const std::string &name, std::vector<prep_cmd_t> &input) {
|
||||
std::string string;
|
||||
string_f(vars, name, string);
|
||||
|
||||
std::stringstream jsonStream;
|
||||
|
||||
// check if string is empty, i.e. when the value doesn't exist in the config file
|
||||
if (string.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// We need to add a wrapping object to make it valid JSON, otherwise ptree cannot parse it.
|
||||
jsonStream << "{\"prep_cmd\":" << string << "}";
|
||||
|
||||
boost::property_tree::ptree jsonTree;
|
||||
boost::property_tree::read_json(jsonStream, jsonTree);
|
||||
|
||||
for (auto &[_, prep_cmd] : jsonTree.get_child("prep_cmd"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(do_cmd.value_or(""), undo_cmd.value_or(""), elevated.value_or(false));
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
list_int_f(std::unordered_map<std::string, std::string> &vars, const std::string &name, std::vector<int> &input) {
|
||||
std::vector<std::string> list;
|
||||
list_string_f(vars, name, list);
|
||||
|
||||
// check if list is empty, i.e. when the value doesn't exist in the config file
|
||||
if (list.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// The framerate list must be cleared before adding values from the file configuration.
|
||||
// If the list is not cleared, then the specified parameters do not affect the behavior of the sunshine server.
|
||||
// That is, if you set only 30 fps in the configuration file, it will not work because by default, during initialization the list includes 10, 30, 60, 90 and 120 fps.
|
||||
input.clear();
|
||||
for (auto &el : list) {
|
||||
std::string_view val = el;
|
||||
|
||||
// If value is something like: "756" instead of 756
|
||||
if (val.size() >= 2 && val[0] == '"') {
|
||||
val = val.substr(1, val.size() - 2);
|
||||
}
|
||||
|
||||
int tmp;
|
||||
|
||||
// If the integer is a hexadecimal
|
||||
if (val.size() >= 2 && val.substr(0, 2) == "0x"sv) {
|
||||
tmp = util::from_hex<int>(val.substr(2));
|
||||
}
|
||||
else {
|
||||
tmp = util::from_view(val);
|
||||
}
|
||||
input.emplace_back(tmp);
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
map_int_int_f(std::unordered_map<std::string, std::string> &vars, const std::string &name, std::unordered_map<int, int> &input) {
|
||||
std::vector<int> list;
|
||||
list_int_f(vars, name, list);
|
||||
|
||||
// The list needs to be a multiple of 2
|
||||
if (list.size() % 2) {
|
||||
std::cout << "Warning: expected "sv << name << " to have a multiple of two elements --> not "sv << list.size() << std::endl;
|
||||
return;
|
||||
}
|
||||
|
||||
int x = 0;
|
||||
while (x < list.size()) {
|
||||
auto key = list[x++];
|
||||
auto val = list[x++];
|
||||
|
||||
input.emplace(key, val);
|
||||
}
|
||||
}
|
||||
|
||||
int
|
||||
apply_flags(const char *line) {
|
||||
int ret = 0;
|
||||
while (*line != '\0') {
|
||||
switch (*line) {
|
||||
case '0':
|
||||
config::sunshine.flags[config::flag::PIN_STDIN].flip();
|
||||
break;
|
||||
case '1':
|
||||
config::sunshine.flags[config::flag::FRESH_STATE].flip();
|
||||
break;
|
||||
case '2':
|
||||
config::sunshine.flags[config::flag::FORCE_VIDEO_HEADER_REPLACE].flip();
|
||||
break;
|
||||
case 'p':
|
||||
config::sunshine.flags[config::flag::UPNP].flip();
|
||||
break;
|
||||
default:
|
||||
std::cout << "Warning: Unrecognized flag: ["sv << *line << ']' << std::endl;
|
||||
ret = -1;
|
||||
}
|
||||
|
||||
++line;
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
void
|
||||
apply_config(std::unordered_map<std::string, std::string> &&vars) {
|
||||
if (!fs::exists(stream.file_apps.c_str())) {
|
||||
fs::copy_file(SUNSHINE_ASSETS_DIR "/apps.json", stream.file_apps);
|
||||
}
|
||||
|
||||
for (auto &[name, val] : vars) {
|
||||
std::cout << "["sv << name << "] -- ["sv << val << ']' << std::endl;
|
||||
}
|
||||
|
||||
int_f(vars, "qp", video.qp);
|
||||
int_f(vars, "min_threads", video.min_threads);
|
||||
int_between_f(vars, "hevc_mode", video.hevc_mode, { 0, 3 });
|
||||
int_between_f(vars, "av1_mode", video.av1_mode, { 0, 3 });
|
||||
string_f(vars, "sw_preset", video.sw.sw_preset);
|
||||
if (!video.sw.sw_preset.empty()) {
|
||||
video.sw.svtav1_preset = sw::svtav1_preset_from_view(video.sw.sw_preset);
|
||||
}
|
||||
string_f(vars, "sw_tune", video.sw.sw_tune);
|
||||
|
||||
int_between_f(vars, "nvenc_preset", video.nv.quality_preset, { 1, 7 });
|
||||
int_between_f(vars, "nvenc_vbv_increase", video.nv.vbv_percentage_increase, { 0, 400 });
|
||||
bool_f(vars, "nvenc_spatial_aq", video.nv.adaptive_quantization);
|
||||
generic_f(vars, "nvenc_twopass", video.nv.two_pass, nv::twopass_from_view);
|
||||
bool_f(vars, "nvenc_h264_cavlc", video.nv.h264_cavlc);
|
||||
bool_f(vars, "nvenc_realtime_hags", video.nv_realtime_hags);
|
||||
bool_f(vars, "nvenc_opengl_vulkan_on_dxgi", video.nv_opengl_vulkan_on_dxgi);
|
||||
bool_f(vars, "nvenc_latency_over_power", video.nv_sunshine_high_power_mode);
|
||||
|
||||
#ifndef __APPLE__
|
||||
video.nv_legacy.preset = video.nv.quality_preset + 11;
|
||||
video.nv_legacy.multipass = video.nv.two_pass == nvenc::nvenc_two_pass::quarter_resolution ? NV_ENC_TWO_PASS_QUARTER_RESOLUTION :
|
||||
video.nv.two_pass == nvenc::nvenc_two_pass::full_resolution ? NV_ENC_TWO_PASS_FULL_RESOLUTION :
|
||||
NV_ENC_MULTI_PASS_DISABLED;
|
||||
video.nv_legacy.h264_coder = video.nv.h264_cavlc ? NV_ENC_H264_ENTROPY_CODING_MODE_CAVLC : NV_ENC_H264_ENTROPY_CODING_MODE_CABAC;
|
||||
video.nv_legacy.aq = video.nv.adaptive_quantization;
|
||||
video.nv_legacy.vbv_percentage_increase = video.nv.vbv_percentage_increase;
|
||||
#endif
|
||||
|
||||
int_f(vars, "qsv_preset", video.qsv.qsv_preset, qsv::preset_from_view);
|
||||
int_f(vars, "qsv_coder", video.qsv.qsv_cavlc, qsv::coder_from_view);
|
||||
bool_f(vars, "qsv_slow_hevc", video.qsv.qsv_slow_hevc);
|
||||
|
||||
std::string quality;
|
||||
string_f(vars, "amd_quality", quality);
|
||||
if (!quality.empty()) {
|
||||
video.amd.amd_quality_h264 = amd::quality_from_view<amd::quality_h264_e>(quality);
|
||||
video.amd.amd_quality_hevc = amd::quality_from_view<amd::quality_hevc_e>(quality);
|
||||
video.amd.amd_quality_av1 = amd::quality_from_view<amd::quality_av1_e>(quality);
|
||||
}
|
||||
|
||||
std::string rc;
|
||||
string_f(vars, "amd_rc", rc);
|
||||
int_f(vars, "amd_coder", video.amd.amd_coder, amd::coder_from_view);
|
||||
if (!rc.empty()) {
|
||||
video.amd.amd_rc_h264 = amd::rc_from_view<amd::rc_h264_e>(rc);
|
||||
video.amd.amd_rc_hevc = amd::rc_from_view<amd::rc_hevc_e>(rc);
|
||||
video.amd.amd_rc_av1 = amd::rc_from_view<amd::rc_av1_e>(rc);
|
||||
}
|
||||
|
||||
std::string usage;
|
||||
string_f(vars, "amd_usage", usage);
|
||||
if (!usage.empty()) {
|
||||
video.amd.amd_usage_h264 = amd::usage_from_view<amd::usage_h264_e>(usage);
|
||||
video.amd.amd_usage_hevc = amd::usage_from_view<amd::usage_hevc_e>(usage);
|
||||
video.amd.amd_usage_av1 = amd::usage_from_view<amd::usage_av1_e>(usage);
|
||||
}
|
||||
|
||||
bool_f(vars, "amd_preanalysis", (bool &) video.amd.amd_preanalysis);
|
||||
bool_f(vars, "amd_vbaq", (bool &) video.amd.amd_vbaq);
|
||||
bool_f(vars, "amd_enforce_hrd", (bool &) video.amd.amd_enforce_hrd);
|
||||
|
||||
int_f(vars, "vt_coder", video.vt.vt_coder, vt::coder_from_view);
|
||||
int_f(vars, "vt_software", video.vt.vt_allow_sw, vt::allow_software_from_view);
|
||||
int_f(vars, "vt_software", video.vt.vt_require_sw, vt::force_software_from_view);
|
||||
int_f(vars, "vt_realtime", video.vt.vt_realtime, vt::rt_from_view);
|
||||
|
||||
string_f(vars, "capture", video.capture);
|
||||
string_f(vars, "encoder", video.encoder);
|
||||
string_f(vars, "adapter_name", video.adapter_name);
|
||||
string_f(vars, "output_name", video.output_name);
|
||||
|
||||
path_f(vars, "pkey", nvhttp.pkey);
|
||||
path_f(vars, "cert", nvhttp.cert);
|
||||
string_f(vars, "sunshine_name", nvhttp.sunshine_name);
|
||||
path_f(vars, "log_path", config::sunshine.log_file);
|
||||
path_f(vars, "file_state", nvhttp.file_state);
|
||||
|
||||
// Must be run after "file_state"
|
||||
config::sunshine.credentials_file = config::nvhttp.file_state;
|
||||
path_f(vars, "credentials_file", config::sunshine.credentials_file);
|
||||
|
||||
string_f(vars, "external_ip", nvhttp.external_ip);
|
||||
list_string_f(vars, "resolutions"s, nvhttp.resolutions);
|
||||
list_int_f(vars, "fps"s, nvhttp.fps);
|
||||
list_prep_cmd_f(vars, "global_prep_cmd", config::sunshine.prep_cmds);
|
||||
|
||||
string_f(vars, "audio_sink", audio.sink);
|
||||
string_f(vars, "virtual_sink", audio.virtual_sink);
|
||||
bool_f(vars, "install_steam_audio_drivers", audio.install_steam_drivers);
|
||||
|
||||
string_restricted_f(vars, "origin_web_ui_allowed", nvhttp.origin_web_ui_allowed, { "pc"sv, "lan"sv, "wan"sv });
|
||||
|
||||
int to = -1;
|
||||
int_between_f(vars, "ping_timeout", to, { -1, std::numeric_limits<int>::max() });
|
||||
if (to != -1) {
|
||||
stream.ping_timeout = std::chrono::milliseconds(to);
|
||||
}
|
||||
|
||||
int_between_f(vars, "channels", stream.channels, { 1, std::numeric_limits<int>::max() });
|
||||
|
||||
int_between_f(vars, "lan_encryption_mode", stream.lan_encryption_mode, { 0, 2 });
|
||||
int_between_f(vars, "wan_encryption_mode", stream.wan_encryption_mode, { 0, 2 });
|
||||
|
||||
path_f(vars, "file_apps", stream.file_apps);
|
||||
int_between_f(vars, "fec_percentage", stream.fec_percentage, { 1, 255 });
|
||||
|
||||
map_int_int_f(vars, "keybindings"s, input.keybindings);
|
||||
|
||||
// This config option will only be used by the UI
|
||||
// When editing in the config file itself, use "keybindings"
|
||||
bool map_rightalt_to_win = false;
|
||||
bool_f(vars, "key_rightalt_to_key_win", map_rightalt_to_win);
|
||||
|
||||
if (map_rightalt_to_win) {
|
||||
input.keybindings.emplace(0xA5, 0x5B);
|
||||
}
|
||||
|
||||
to = std::numeric_limits<int>::min();
|
||||
int_f(vars, "back_button_timeout", to);
|
||||
|
||||
if (to > std::numeric_limits<int>::min()) {
|
||||
input.back_button_timeout = std::chrono::milliseconds { to };
|
||||
}
|
||||
|
||||
double repeat_frequency { 0 };
|
||||
double_between_f(vars, "key_repeat_frequency", repeat_frequency, { 0, std::numeric_limits<double>::max() });
|
||||
|
||||
if (repeat_frequency > 0) {
|
||||
config::input.key_repeat_period = std::chrono::duration<double> { 1 / repeat_frequency };
|
||||
}
|
||||
|
||||
to = -1;
|
||||
int_f(vars, "key_repeat_delay", to);
|
||||
if (to >= 0) {
|
||||
input.key_repeat_delay = std::chrono::milliseconds { to };
|
||||
}
|
||||
|
||||
string_restricted_f(vars, "gamepad"s, input.gamepad, platf::supported_gamepads());
|
||||
bool_f(vars, "ds4_back_as_touchpad_click", input.ds4_back_as_touchpad_click);
|
||||
bool_f(vars, "motion_as_ds4", input.motion_as_ds4);
|
||||
bool_f(vars, "touchpad_as_ds4", input.touchpad_as_ds4);
|
||||
|
||||
bool_f(vars, "mouse", input.mouse);
|
||||
bool_f(vars, "keyboard", input.keyboard);
|
||||
bool_f(vars, "controller", input.controller);
|
||||
|
||||
bool_f(vars, "always_send_scancodes", input.always_send_scancodes);
|
||||
|
||||
bool_f(vars, "high_resolution_scrolling", input.high_resolution_scrolling);
|
||||
bool_f(vars, "native_pen_touch", input.native_pen_touch);
|
||||
|
||||
int port = sunshine.port;
|
||||
int_between_f(vars, "port"s, port, { 1024 + nvhttp::PORT_HTTPS, 65535 - rtsp_stream::RTSP_SETUP_PORT });
|
||||
sunshine.port = (std::uint16_t) port;
|
||||
|
||||
string_restricted_f(vars, "address_family", sunshine.address_family, { "ipv4"sv, "both"sv });
|
||||
|
||||
bool upnp = false;
|
||||
bool_f(vars, "upnp"s, upnp);
|
||||
|
||||
if (upnp) {
|
||||
config::sunshine.flags[config::flag::UPNP].flip();
|
||||
}
|
||||
|
||||
string_restricted_f(vars, "locale", config::sunshine.locale, {
|
||||
"de"sv, // German
|
||||
"en"sv, // English
|
||||
"en_GB"sv, // English (UK)
|
||||
"en_US"sv, // English (US)
|
||||
"es"sv, // Spanish
|
||||
"fr"sv, // French
|
||||
"it"sv, // Italian
|
||||
"ru"sv, // Russian
|
||||
"sv"sv, // Swedish
|
||||
"zh"sv, // Chinese
|
||||
});
|
||||
|
||||
std::string log_level_string;
|
||||
string_f(vars, "min_log_level", log_level_string);
|
||||
|
||||
if (!log_level_string.empty()) {
|
||||
if (log_level_string == "verbose"sv) {
|
||||
sunshine.min_log_level = 0;
|
||||
}
|
||||
else if (log_level_string == "debug"sv) {
|
||||
sunshine.min_log_level = 1;
|
||||
}
|
||||
else if (log_level_string == "info"sv) {
|
||||
sunshine.min_log_level = 2;
|
||||
}
|
||||
else if (log_level_string == "warning"sv) {
|
||||
sunshine.min_log_level = 3;
|
||||
}
|
||||
else if (log_level_string == "error"sv) {
|
||||
sunshine.min_log_level = 4;
|
||||
}
|
||||
else if (log_level_string == "fatal"sv) {
|
||||
sunshine.min_log_level = 5;
|
||||
}
|
||||
else if (log_level_string == "none"sv) {
|
||||
sunshine.min_log_level = 6;
|
||||
}
|
||||
else {
|
||||
// accept digit directly
|
||||
auto val = log_level_string[0];
|
||||
if (val >= '0' && val < '7') {
|
||||
sunshine.min_log_level = val - '0';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
auto it = vars.find("flags"s);
|
||||
if (it != std::end(vars)) {
|
||||
apply_flags(it->second.c_str());
|
||||
|
||||
vars.erase(it);
|
||||
}
|
||||
|
||||
if (sunshine.min_log_level <= 3) {
|
||||
for (auto &[var, _] : vars) {
|
||||
std::cout << "Warning: Unrecognized configurable option ["sv << var << ']' << std::endl;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int
|
||||
parse(int argc, char *argv[]) {
|
||||
std::unordered_map<std::string, std::string> cmd_vars;
|
||||
#ifdef _WIN32
|
||||
bool shortcut_launch = false;
|
||||
bool service_admin_launch = false;
|
||||
#endif
|
||||
|
||||
for (auto x = 1; x < argc; ++x) {
|
||||
auto line = argv[x];
|
||||
|
||||
if (line == "--help"sv) {
|
||||
logging::print_help(*argv);
|
||||
return 1;
|
||||
}
|
||||
#ifdef _WIN32
|
||||
else if (line == "--shortcut"sv) {
|
||||
shortcut_launch = true;
|
||||
}
|
||||
else if (line == "--shortcut-admin"sv) {
|
||||
service_admin_launch = true;
|
||||
}
|
||||
#endif
|
||||
else if (*line == '-') {
|
||||
if (*(line + 1) == '-') {
|
||||
sunshine.cmd.name = line + 2;
|
||||
sunshine.cmd.argc = argc - x - 1;
|
||||
sunshine.cmd.argv = argv + x + 1;
|
||||
|
||||
break;
|
||||
}
|
||||
if (apply_flags(line + 1)) {
|
||||
logging::print_help(*argv);
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
else {
|
||||
auto line_end = line + strlen(line);
|
||||
|
||||
auto pos = std::find(line, line_end, '=');
|
||||
if (pos == line_end) {
|
||||
sunshine.config_file = line;
|
||||
}
|
||||
else {
|
||||
TUPLE_EL(var, 1, parse_option(line, line_end));
|
||||
if (!var) {
|
||||
logging::print_help(*argv);
|
||||
return -1;
|
||||
}
|
||||
|
||||
TUPLE_EL_REF(name, 0, *var);
|
||||
|
||||
auto it = cmd_vars.find(name);
|
||||
if (it != std::end(cmd_vars)) {
|
||||
cmd_vars.erase(it);
|
||||
}
|
||||
|
||||
cmd_vars.emplace(std::move(*var));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool config_loaded = false;
|
||||
try {
|
||||
// Create appdata folder if it does not exist
|
||||
if (!boost::filesystem::exists(platf::appdata().string())) {
|
||||
boost::filesystem::create_directories(platf::appdata().string());
|
||||
}
|
||||
|
||||
// Create empty config file if it does not exist
|
||||
if (!fs::exists(sunshine.config_file)) {
|
||||
std::ofstream { sunshine.config_file };
|
||||
}
|
||||
|
||||
// Read config file
|
||||
auto vars = parse_config(file_handler::read_file(sunshine.config_file.c_str()));
|
||||
|
||||
for (auto &[name, value] : cmd_vars) {
|
||||
vars.insert_or_assign(std::move(name), std::move(value));
|
||||
}
|
||||
|
||||
// Apply the config. Note: This will try to create any paths
|
||||
// referenced in the config, so we may receive exceptions if
|
||||
// the path is incorrect or inaccessible.
|
||||
apply_config(std::move(vars));
|
||||
config_loaded = true;
|
||||
}
|
||||
catch (const std::filesystem::filesystem_error &err) {
|
||||
BOOST_LOG(fatal) << "Failed to apply config: "sv << err.what();
|
||||
}
|
||||
catch (const boost::filesystem::filesystem_error &err) {
|
||||
BOOST_LOG(fatal) << "Failed to apply config: "sv << err.what();
|
||||
}
|
||||
|
||||
if (!config_loaded) {
|
||||
#ifdef _WIN32
|
||||
BOOST_LOG(fatal) << "To relaunch Sunshine successfully, use the shortcut in the Start Menu. Do not run Sunshine.exe manually."sv;
|
||||
std::this_thread::sleep_for(10s);
|
||||
#endif
|
||||
return -1;
|
||||
}
|
||||
|
||||
#ifdef _WIN32
|
||||
// We have to wait until the config is loaded to handle these launches,
|
||||
// because we need to have the correct base port loaded in our config.
|
||||
if (service_admin_launch) {
|
||||
// This is a relaunch as admin to start the service
|
||||
service_ctrl::start_service();
|
||||
|
||||
// Always return 1 to ensure Sunshine doesn't start normally
|
||||
return 1;
|
||||
}
|
||||
else if (shortcut_launch) {
|
||||
if (!service_ctrl::is_service_running()) {
|
||||
// If the service isn't running, relaunch ourselves as admin to start it
|
||||
WCHAR executable[MAX_PATH];
|
||||
GetModuleFileNameW(NULL, executable, ARRAYSIZE(executable));
|
||||
|
||||
SHELLEXECUTEINFOW shell_exec_info {};
|
||||
shell_exec_info.cbSize = sizeof(shell_exec_info);
|
||||
shell_exec_info.fMask = SEE_MASK_NOASYNC | SEE_MASK_NO_CONSOLE | SEE_MASK_NOCLOSEPROCESS;
|
||||
shell_exec_info.lpVerb = L"runas";
|
||||
shell_exec_info.lpFile = executable;
|
||||
shell_exec_info.lpParameters = L"--shortcut-admin";
|
||||
shell_exec_info.nShow = SW_NORMAL;
|
||||
if (!ShellExecuteExW(&shell_exec_info)) {
|
||||
auto winerr = GetLastError();
|
||||
std::cout << "Error: ShellExecuteEx() failed:"sv << winerr << std::endl;
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Wait for the elevated process to finish starting the service
|
||||
WaitForSingleObject(shell_exec_info.hProcess, INFINITE);
|
||||
CloseHandle(shell_exec_info.hProcess);
|
||||
|
||||
// Wait for the UI to be ready for connections
|
||||
service_ctrl::wait_for_ui_ready();
|
||||
}
|
||||
|
||||
// Launch the web UI
|
||||
launch_ui();
|
||||
|
||||
// Always return 1 to ensure Sunshine doesn't start normally
|
||||
return 1;
|
||||
}
|
||||
#endif
|
||||
|
||||
return 0;
|
||||
}
|
||||
} // namespace config
|
||||
|
||||
@ -1,804 +0,0 @@
|
||||
/**
|
||||
* @file src/confighttp.cpp
|
||||
* @brief todo
|
||||
*
|
||||
* @todo Authentication, better handling of routes common to nvhttp, cleanup
|
||||
*/
|
||||
|
||||
#define BOOST_BIND_GLOBAL_PLACEHOLDERS
|
||||
|
||||
#include "process.h"
|
||||
|
||||
#include <filesystem>
|
||||
#include <set>
|
||||
|
||||
#include <boost/property_tree/json_parser.hpp>
|
||||
#include <boost/property_tree/ptree.hpp>
|
||||
#include <boost/property_tree/xml_parser.hpp>
|
||||
|
||||
#include <boost/algorithm/string.hpp>
|
||||
|
||||
#include <boost/asio/ssl/context.hpp>
|
||||
|
||||
#include <boost/filesystem.hpp>
|
||||
|
||||
#include <Simple-Web-Server/crypto.hpp>
|
||||
#include <Simple-Web-Server/server_https.hpp>
|
||||
#include <boost/asio/ssl/context_base.hpp>
|
||||
|
||||
#include "config.h"
|
||||
#include "confighttp.h"
|
||||
#include "crypto.h"
|
||||
#include "file_handler.h"
|
||||
#include "globals.h"
|
||||
#include "httpcommon.h"
|
||||
#include "logging.h"
|
||||
#include "network.h"
|
||||
#include "nvhttp.h"
|
||||
#include "platform/common.h"
|
||||
#include "rtsp.h"
|
||||
#include "utility.h"
|
||||
#include "uuid.h"
|
||||
#include "version.h"
|
||||
|
||||
using namespace std::literals;
|
||||
|
||||
namespace confighttp {
|
||||
namespace fs = std::filesystem;
|
||||
namespace pt = boost::property_tree;
|
||||
|
||||
using https_server_t = SimpleWeb::Server<SimpleWeb::HTTPS>;
|
||||
|
||||
using args_t = SimpleWeb::CaseInsensitiveMultimap;
|
||||
using resp_https_t = std::shared_ptr<typename SimpleWeb::ServerBase<SimpleWeb::HTTPS>::Response>;
|
||||
using req_https_t = std::shared_ptr<typename SimpleWeb::ServerBase<SimpleWeb::HTTPS>::Request>;
|
||||
|
||||
enum class op_e {
|
||||
ADD,
|
||||
REMOVE
|
||||
};
|
||||
|
||||
void
|
||||
print_req(const req_https_t &request) {
|
||||
BOOST_LOG(debug) << "METHOD :: "sv << request->method;
|
||||
BOOST_LOG(debug) << "DESTINATION :: "sv << request->path;
|
||||
|
||||
for (auto &[name, val] : request->header) {
|
||||
BOOST_LOG(debug) << name << " -- " << (name == "Authorization" ? "CREDENTIALS REDACTED" : val);
|
||||
}
|
||||
|
||||
BOOST_LOG(debug) << " [--] "sv;
|
||||
|
||||
for (auto &[name, val] : request->parse_query_string()) {
|
||||
BOOST_LOG(debug) << name << " -- " << val;
|
||||
}
|
||||
|
||||
BOOST_LOG(debug) << " [--] "sv;
|
||||
}
|
||||
|
||||
void
|
||||
send_unauthorized(resp_https_t response, req_https_t request) {
|
||||
auto address = net::addr_to_normalized_string(request->remote_endpoint().address());
|
||||
BOOST_LOG(info) << "Web UI: ["sv << address << "] -- not authorized"sv;
|
||||
const SimpleWeb::CaseInsensitiveMultimap headers {
|
||||
{ "WWW-Authenticate", R"(Basic realm="Sunshine Gamestream Host", charset="UTF-8")" }
|
||||
};
|
||||
response->write(SimpleWeb::StatusCode::client_error_unauthorized, headers);
|
||||
}
|
||||
|
||||
void
|
||||
send_redirect(resp_https_t response, req_https_t request, const char *path) {
|
||||
auto address = net::addr_to_normalized_string(request->remote_endpoint().address());
|
||||
BOOST_LOG(info) << "Web UI: ["sv << address << "] -- not authorized"sv;
|
||||
const SimpleWeb::CaseInsensitiveMultimap headers {
|
||||
{ "Location", path }
|
||||
};
|
||||
response->write(SimpleWeb::StatusCode::redirection_temporary_redirect, headers);
|
||||
}
|
||||
|
||||
bool
|
||||
authenticate(resp_https_t response, req_https_t request) {
|
||||
auto address = net::addr_to_normalized_string(request->remote_endpoint().address());
|
||||
auto ip_type = net::from_address(address);
|
||||
|
||||
if (ip_type > http::origin_web_ui_allowed) {
|
||||
BOOST_LOG(info) << "Web UI: ["sv << address << "] -- denied"sv;
|
||||
response->write(SimpleWeb::StatusCode::client_error_forbidden);
|
||||
return false;
|
||||
}
|
||||
|
||||
// If credentials are shown, redirect the user to a /welcome page
|
||||
if (config::sunshine.username.empty()) {
|
||||
send_redirect(response, request, "/welcome");
|
||||
return false;
|
||||
}
|
||||
|
||||
auto fg = util::fail_guard([&]() {
|
||||
send_unauthorized(response, request);
|
||||
});
|
||||
|
||||
auto auth = request->header.find("authorization");
|
||||
if (auth == request->header.end()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
auto &rawAuth = auth->second;
|
||||
auto authData = SimpleWeb::Crypto::Base64::decode(rawAuth.substr("Basic "sv.length()));
|
||||
|
||||
int index = authData.find(':');
|
||||
if (index >= authData.size() - 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
auto username = authData.substr(0, index);
|
||||
auto password = authData.substr(index + 1);
|
||||
auto hash = util::hex(crypto::hash(password + config::sunshine.salt)).to_string();
|
||||
|
||||
if (!boost::iequals(username, config::sunshine.username) || hash != config::sunshine.password) {
|
||||
return false;
|
||||
}
|
||||
|
||||
fg.disable();
|
||||
return true;
|
||||
}
|
||||
|
||||
void
|
||||
not_found(resp_https_t response, req_https_t request) {
|
||||
pt::ptree tree;
|
||||
tree.put("root.<xmlattr>.status_code", 404);
|
||||
|
||||
std::ostringstream data;
|
||||
|
||||
pt::write_xml(data, tree);
|
||||
response->write(data.str());
|
||||
|
||||
*response << "HTTP/1.1 404 NOT FOUND\r\n"
|
||||
<< data.str();
|
||||
}
|
||||
|
||||
// todo - combine these functions into a single function that accepts the page, i.e "index", "pin", "apps"
|
||||
void
|
||||
getIndexPage(resp_https_t response, req_https_t request) {
|
||||
if (!authenticate(response, request)) return;
|
||||
|
||||
print_req(request);
|
||||
|
||||
std::string content = file_handler::read_file(WEB_DIR "index.html");
|
||||
SimpleWeb::CaseInsensitiveMultimap headers;
|
||||
headers.emplace("Content-Type", "text/html; charset=utf-8");
|
||||
response->write(content, headers);
|
||||
}
|
||||
|
||||
void
|
||||
getPinPage(resp_https_t response, req_https_t request) {
|
||||
if (!authenticate(response, request)) return;
|
||||
|
||||
print_req(request);
|
||||
|
||||
std::string content = file_handler::read_file(WEB_DIR "pin.html");
|
||||
SimpleWeb::CaseInsensitiveMultimap headers;
|
||||
headers.emplace("Content-Type", "text/html; charset=utf-8");
|
||||
response->write(content, headers);
|
||||
}
|
||||
|
||||
void
|
||||
getAppsPage(resp_https_t response, req_https_t request) {
|
||||
if (!authenticate(response, request)) return;
|
||||
|
||||
print_req(request);
|
||||
|
||||
std::string content = file_handler::read_file(WEB_DIR "apps.html");
|
||||
SimpleWeb::CaseInsensitiveMultimap headers;
|
||||
headers.emplace("Content-Type", "text/html; charset=utf-8");
|
||||
headers.emplace("Access-Control-Allow-Origin", "https://images.igdb.com/");
|
||||
response->write(content, headers);
|
||||
}
|
||||
|
||||
void
|
||||
getClientsPage(resp_https_t response, req_https_t request) {
|
||||
if (!authenticate(response, request)) return;
|
||||
|
||||
print_req(request);
|
||||
|
||||
std::string content = file_handler::read_file(WEB_DIR "clients.html");
|
||||
SimpleWeb::CaseInsensitiveMultimap headers;
|
||||
headers.emplace("Content-Type", "text/html; charset=utf-8");
|
||||
response->write(content, headers);
|
||||
}
|
||||
|
||||
void
|
||||
getConfigPage(resp_https_t response, req_https_t request) {
|
||||
if (!authenticate(response, request)) return;
|
||||
|
||||
print_req(request);
|
||||
|
||||
std::string content = file_handler::read_file(WEB_DIR "config.html");
|
||||
SimpleWeb::CaseInsensitiveMultimap headers;
|
||||
headers.emplace("Content-Type", "text/html; charset=utf-8");
|
||||
response->write(content, headers);
|
||||
}
|
||||
|
||||
void
|
||||
getPasswordPage(resp_https_t response, req_https_t request) {
|
||||
if (!authenticate(response, request)) return;
|
||||
|
||||
print_req(request);
|
||||
|
||||
std::string content = file_handler::read_file(WEB_DIR "password.html");
|
||||
SimpleWeb::CaseInsensitiveMultimap headers;
|
||||
headers.emplace("Content-Type", "text/html; charset=utf-8");
|
||||
response->write(content, headers);
|
||||
}
|
||||
|
||||
void
|
||||
getWelcomePage(resp_https_t response, req_https_t request) {
|
||||
print_req(request);
|
||||
if (!config::sunshine.username.empty()) {
|
||||
send_redirect(response, request, "/");
|
||||
return;
|
||||
}
|
||||
std::string content = file_handler::read_file(WEB_DIR "welcome.html");
|
||||
SimpleWeb::CaseInsensitiveMultimap headers;
|
||||
headers.emplace("Content-Type", "text/html; charset=utf-8");
|
||||
response->write(content, headers);
|
||||
}
|
||||
|
||||
void
|
||||
getTroubleshootingPage(resp_https_t response, req_https_t request) {
|
||||
if (!authenticate(response, request)) return;
|
||||
|
||||
print_req(request);
|
||||
|
||||
std::string content = file_handler::read_file(WEB_DIR "troubleshooting.html");
|
||||
SimpleWeb::CaseInsensitiveMultimap headers;
|
||||
headers.emplace("Content-Type", "text/html; charset=utf-8");
|
||||
response->write(content, headers);
|
||||
}
|
||||
|
||||
void
|
||||
getFaviconImage(resp_https_t response, req_https_t request) {
|
||||
// todo - combine function with getSunshineLogoImage and possibly getNodeModules
|
||||
// todo - use mime_types map
|
||||
print_req(request);
|
||||
|
||||
std::ifstream in(WEB_DIR "images/sunshine.ico", std::ios::binary);
|
||||
SimpleWeb::CaseInsensitiveMultimap headers;
|
||||
headers.emplace("Content-Type", "image/x-icon");
|
||||
response->write(SimpleWeb::StatusCode::success_ok, in, headers);
|
||||
}
|
||||
|
||||
void
|
||||
getSunshineLogoImage(resp_https_t response, req_https_t request) {
|
||||
// todo - combine function with getFaviconImage and possibly getNodeModules
|
||||
// todo - use mime_types map
|
||||
print_req(request);
|
||||
|
||||
std::ifstream in(WEB_DIR "images/logo-sunshine-45.png", std::ios::binary);
|
||||
SimpleWeb::CaseInsensitiveMultimap headers;
|
||||
headers.emplace("Content-Type", "image/png");
|
||||
response->write(SimpleWeb::StatusCode::success_ok, in, headers);
|
||||
}
|
||||
|
||||
bool
|
||||
isChildPath(fs::path const &base, fs::path const &query) {
|
||||
auto relPath = fs::relative(base, query);
|
||||
return *(relPath.begin()) != fs::path("..");
|
||||
}
|
||||
|
||||
void
|
||||
getNodeModules(resp_https_t response, req_https_t request) {
|
||||
print_req(request);
|
||||
fs::path webDirPath(WEB_DIR);
|
||||
fs::path nodeModulesPath(webDirPath / "assets");
|
||||
|
||||
// .relative_path is needed to shed any leading slash that might exist in the request path
|
||||
auto filePath = fs::weakly_canonical(webDirPath / fs::path(request->path).relative_path());
|
||||
|
||||
// Don't do anything if file does not exist or is outside the assets directory
|
||||
if (!isChildPath(filePath, nodeModulesPath)) {
|
||||
BOOST_LOG(warning) << "Someone requested a path " << filePath << " that is outside the assets folder";
|
||||
response->write(SimpleWeb::StatusCode::client_error_bad_request, "Bad Request");
|
||||
}
|
||||
else if (!fs::exists(filePath)) {
|
||||
response->write(SimpleWeb::StatusCode::client_error_not_found);
|
||||
}
|
||||
else {
|
||||
auto relPath = fs::relative(filePath, webDirPath);
|
||||
// get the mime type from the file extension mime_types map
|
||||
// remove the leading period from the extension
|
||||
auto mimeType = mime_types.find(relPath.extension().string().substr(1));
|
||||
// check if the extension is in the map at the x position
|
||||
if (mimeType != mime_types.end()) {
|
||||
// if it is, set the content type to the mime type
|
||||
SimpleWeb::CaseInsensitiveMultimap headers;
|
||||
headers.emplace("Content-Type", mimeType->second);
|
||||
std::ifstream in(filePath.string(), std::ios::binary);
|
||||
response->write(SimpleWeb::StatusCode::success_ok, in, headers);
|
||||
}
|
||||
// do not return any file if the type is not in the map
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
getApps(resp_https_t response, req_https_t request) {
|
||||
if (!authenticate(response, request)) return;
|
||||
|
||||
print_req(request);
|
||||
|
||||
std::string content = file_handler::read_file(config::stream.file_apps.c_str());
|
||||
SimpleWeb::CaseInsensitiveMultimap headers;
|
||||
headers.emplace("Content-Type", "application/json");
|
||||
response->write(content, headers);
|
||||
}
|
||||
|
||||
void
|
||||
getLogs(resp_https_t response, req_https_t request) {
|
||||
if (!authenticate(response, request)) return;
|
||||
|
||||
print_req(request);
|
||||
|
||||
std::string content = file_handler::read_file(config::sunshine.log_file.c_str());
|
||||
SimpleWeb::CaseInsensitiveMultimap headers;
|
||||
headers.emplace("Content-Type", "text/plain");
|
||||
response->write(SimpleWeb::StatusCode::success_ok, content, headers);
|
||||
}
|
||||
|
||||
void
|
||||
saveApp(resp_https_t response, req_https_t request) {
|
||||
if (!authenticate(response, request)) return;
|
||||
|
||||
print_req(request);
|
||||
|
||||
std::stringstream ss;
|
||||
ss << request->content.rdbuf();
|
||||
|
||||
pt::ptree outputTree;
|
||||
auto g = util::fail_guard([&]() {
|
||||
std::ostringstream data;
|
||||
|
||||
pt::write_json(data, outputTree);
|
||||
response->write(data.str());
|
||||
});
|
||||
|
||||
pt::ptree inputTree, fileTree;
|
||||
|
||||
BOOST_LOG(info) << config::stream.file_apps;
|
||||
try {
|
||||
// TODO: Input Validation
|
||||
pt::read_json(ss, inputTree);
|
||||
pt::read_json(config::stream.file_apps, fileTree);
|
||||
|
||||
if (inputTree.get_child("prep-cmd").empty()) {
|
||||
inputTree.erase("prep-cmd");
|
||||
}
|
||||
|
||||
if (inputTree.get_child("detached").empty()) {
|
||||
inputTree.erase("detached");
|
||||
}
|
||||
|
||||
auto &apps_node = fileTree.get_child("apps"s);
|
||||
int index = inputTree.get<int>("index");
|
||||
|
||||
inputTree.erase("index");
|
||||
|
||||
if (index == -1) {
|
||||
apps_node.push_back(std::make_pair("", inputTree));
|
||||
}
|
||||
else {
|
||||
// Unfortunately Boost PT does not allow to directly edit the array, copy should do the trick
|
||||
pt::ptree newApps;
|
||||
int i = 0;
|
||||
for (const auto &kv : apps_node) {
|
||||
if (i == index) {
|
||||
newApps.push_back(std::make_pair("", inputTree));
|
||||
}
|
||||
else {
|
||||
newApps.push_back(std::make_pair("", kv.second));
|
||||
}
|
||||
i++;
|
||||
}
|
||||
fileTree.erase("apps");
|
||||
fileTree.push_back(std::make_pair("apps", newApps));
|
||||
}
|
||||
pt::write_json(config::stream.file_apps, fileTree);
|
||||
}
|
||||
catch (std::exception &e) {
|
||||
BOOST_LOG(warning) << "SaveApp: "sv << e.what();
|
||||
|
||||
outputTree.put("status", "false");
|
||||
outputTree.put("error", "Invalid Input JSON");
|
||||
return;
|
||||
}
|
||||
|
||||
outputTree.put("status", "true");
|
||||
proc::refresh(config::stream.file_apps);
|
||||
}
|
||||
|
||||
void
|
||||
deleteApp(resp_https_t response, req_https_t request) {
|
||||
if (!authenticate(response, request)) return;
|
||||
|
||||
print_req(request);
|
||||
|
||||
pt::ptree outputTree;
|
||||
auto g = util::fail_guard([&]() {
|
||||
std::ostringstream data;
|
||||
|
||||
pt::write_json(data, outputTree);
|
||||
response->write(data.str());
|
||||
});
|
||||
pt::ptree fileTree;
|
||||
try {
|
||||
pt::read_json(config::stream.file_apps, fileTree);
|
||||
auto &apps_node = fileTree.get_child("apps"s);
|
||||
int index = stoi(request->path_match[1]);
|
||||
|
||||
if (index < 0) {
|
||||
outputTree.put("status", "false");
|
||||
outputTree.put("error", "Invalid Index");
|
||||
return;
|
||||
}
|
||||
else {
|
||||
// Unfortunately Boost PT does not allow to directly edit the array, copy should do the trick
|
||||
pt::ptree newApps;
|
||||
int i = 0;
|
||||
for (const auto &kv : apps_node) {
|
||||
if (i++ != index) {
|
||||
newApps.push_back(std::make_pair("", kv.second));
|
||||
}
|
||||
}
|
||||
fileTree.erase("apps");
|
||||
fileTree.push_back(std::make_pair("apps", newApps));
|
||||
}
|
||||
pt::write_json(config::stream.file_apps, fileTree);
|
||||
}
|
||||
catch (std::exception &e) {
|
||||
BOOST_LOG(warning) << "DeleteApp: "sv << e.what();
|
||||
outputTree.put("status", "false");
|
||||
outputTree.put("error", "Invalid File JSON");
|
||||
return;
|
||||
}
|
||||
|
||||
outputTree.put("status", "true");
|
||||
proc::refresh(config::stream.file_apps);
|
||||
}
|
||||
|
||||
void
|
||||
uploadCover(resp_https_t response, req_https_t request) {
|
||||
if (!authenticate(response, request)) return;
|
||||
|
||||
std::stringstream ss;
|
||||
std::stringstream configStream;
|
||||
ss << request->content.rdbuf();
|
||||
pt::ptree outputTree;
|
||||
auto g = util::fail_guard([&]() {
|
||||
std::ostringstream data;
|
||||
|
||||
SimpleWeb::StatusCode code = SimpleWeb::StatusCode::success_ok;
|
||||
if (outputTree.get_child_optional("error").has_value()) {
|
||||
code = SimpleWeb::StatusCode::client_error_bad_request;
|
||||
}
|
||||
|
||||
pt::write_json(data, outputTree);
|
||||
response->write(code, data.str());
|
||||
});
|
||||
pt::ptree inputTree;
|
||||
try {
|
||||
pt::read_json(ss, inputTree);
|
||||
}
|
||||
catch (std::exception &e) {
|
||||
BOOST_LOG(warning) << "UploadCover: "sv << e.what();
|
||||
outputTree.put("status", "false");
|
||||
outputTree.put("error", e.what());
|
||||
return;
|
||||
}
|
||||
|
||||
auto key = inputTree.get("key", "");
|
||||
if (key.empty()) {
|
||||
outputTree.put("error", "Cover key is required");
|
||||
return;
|
||||
}
|
||||
auto url = inputTree.get("url", "");
|
||||
|
||||
const std::string coverdir = platf::appdata().string() + "/covers/";
|
||||
if (!boost::filesystem::exists(coverdir)) {
|
||||
boost::filesystem::create_directories(coverdir);
|
||||
}
|
||||
|
||||
std::basic_string path = coverdir + http::url_escape(key) + ".png";
|
||||
if (!url.empty()) {
|
||||
if (http::url_get_host(url) != "images.igdb.com") {
|
||||
outputTree.put("error", "Only images.igdb.com is allowed");
|
||||
return;
|
||||
}
|
||||
if (!http::download_file(url, path)) {
|
||||
outputTree.put("error", "Failed to download cover");
|
||||
return;
|
||||
}
|
||||
}
|
||||
else {
|
||||
auto data = SimpleWeb::Crypto::Base64::decode(inputTree.get<std::string>("data"));
|
||||
|
||||
std::ofstream imgfile(path);
|
||||
imgfile.write(data.data(), (int) data.size());
|
||||
}
|
||||
outputTree.put("path", path);
|
||||
}
|
||||
|
||||
void
|
||||
getConfig(resp_https_t response, req_https_t request) {
|
||||
if (!authenticate(response, request)) return;
|
||||
|
||||
print_req(request);
|
||||
|
||||
pt::ptree outputTree;
|
||||
auto g = util::fail_guard([&]() {
|
||||
std::ostringstream data;
|
||||
|
||||
pt::write_json(data, outputTree);
|
||||
response->write(data.str());
|
||||
});
|
||||
|
||||
outputTree.put("status", "true");
|
||||
outputTree.put("platform", SUNSHINE_PLATFORM);
|
||||
outputTree.put("version", PROJECT_VER);
|
||||
|
||||
auto vars = config::parse_config(file_handler::read_file(config::sunshine.config_file.c_str()));
|
||||
|
||||
for (auto &[name, value] : vars) {
|
||||
outputTree.put(std::move(name), std::move(value));
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
getLocale(resp_https_t response, req_https_t request) {
|
||||
// we need to return the locale whether authenticated or not
|
||||
|
||||
print_req(request);
|
||||
|
||||
pt::ptree outputTree;
|
||||
auto g = util::fail_guard([&]() {
|
||||
std::ostringstream data;
|
||||
|
||||
pt::write_json(data, outputTree);
|
||||
response->write(data.str());
|
||||
});
|
||||
|
||||
outputTree.put("status", "true");
|
||||
outputTree.put("locale", config::sunshine.locale);
|
||||
}
|
||||
|
||||
void
|
||||
saveConfig(resp_https_t response, req_https_t request) {
|
||||
if (!authenticate(response, request)) return;
|
||||
|
||||
print_req(request);
|
||||
|
||||
std::stringstream ss;
|
||||
std::stringstream configStream;
|
||||
ss << request->content.rdbuf();
|
||||
pt::ptree outputTree;
|
||||
auto g = util::fail_guard([&]() {
|
||||
std::ostringstream data;
|
||||
|
||||
pt::write_json(data, outputTree);
|
||||
response->write(data.str());
|
||||
});
|
||||
pt::ptree inputTree;
|
||||
try {
|
||||
// TODO: Input Validation
|
||||
pt::read_json(ss, inputTree);
|
||||
for (const auto &kv : inputTree) {
|
||||
std::string value = inputTree.get<std::string>(kv.first);
|
||||
if (value.length() == 0 || value.compare("null") == 0) continue;
|
||||
|
||||
configStream << kv.first << " = " << value << std::endl;
|
||||
}
|
||||
file_handler::write_file(config::sunshine.config_file.c_str(), configStream.str());
|
||||
}
|
||||
catch (std::exception &e) {
|
||||
BOOST_LOG(warning) << "SaveConfig: "sv << e.what();
|
||||
outputTree.put("status", "false");
|
||||
outputTree.put("error", e.what());
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
restart(resp_https_t response, req_https_t request) {
|
||||
if (!authenticate(response, request)) return;
|
||||
|
||||
print_req(request);
|
||||
|
||||
// We may not return from this call
|
||||
platf::restart();
|
||||
}
|
||||
|
||||
void
|
||||
savePassword(resp_https_t response, req_https_t request) {
|
||||
if (!config::sunshine.username.empty() && !authenticate(response, request)) return;
|
||||
|
||||
print_req(request);
|
||||
|
||||
std::stringstream ss;
|
||||
std::stringstream configStream;
|
||||
ss << request->content.rdbuf();
|
||||
|
||||
pt::ptree inputTree, outputTree;
|
||||
|
||||
auto g = util::fail_guard([&]() {
|
||||
std::ostringstream data;
|
||||
pt::write_json(data, outputTree);
|
||||
response->write(data.str());
|
||||
});
|
||||
|
||||
try {
|
||||
// TODO: Input Validation
|
||||
pt::read_json(ss, inputTree);
|
||||
auto username = inputTree.count("currentUsername") > 0 ? inputTree.get<std::string>("currentUsername") : "";
|
||||
auto newUsername = inputTree.get<std::string>("newUsername");
|
||||
auto password = inputTree.count("currentPassword") > 0 ? inputTree.get<std::string>("currentPassword") : "";
|
||||
auto newPassword = inputTree.count("newPassword") > 0 ? inputTree.get<std::string>("newPassword") : "";
|
||||
auto confirmPassword = inputTree.count("confirmNewPassword") > 0 ? inputTree.get<std::string>("confirmNewPassword") : "";
|
||||
if (newUsername.length() == 0) newUsername = username;
|
||||
if (newUsername.length() == 0) {
|
||||
outputTree.put("status", false);
|
||||
outputTree.put("error", "Invalid Username");
|
||||
}
|
||||
else {
|
||||
auto hash = util::hex(crypto::hash(password + config::sunshine.salt)).to_string();
|
||||
if (config::sunshine.username.empty() || (boost::iequals(username, config::sunshine.username) && hash == config::sunshine.password)) {
|
||||
if (newPassword.empty() || newPassword != confirmPassword) {
|
||||
outputTree.put("status", false);
|
||||
outputTree.put("error", "Password Mismatch");
|
||||
}
|
||||
else {
|
||||
http::save_user_creds(config::sunshine.credentials_file, newUsername, newPassword);
|
||||
http::reload_user_creds(config::sunshine.credentials_file);
|
||||
outputTree.put("status", true);
|
||||
}
|
||||
}
|
||||
else {
|
||||
outputTree.put("status", false);
|
||||
outputTree.put("error", "Invalid Current Credentials");
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (std::exception &e) {
|
||||
BOOST_LOG(warning) << "SavePassword: "sv << e.what();
|
||||
outputTree.put("status", false);
|
||||
outputTree.put("error", e.what());
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
savePin(resp_https_t response, req_https_t request) {
|
||||
if (!authenticate(response, request)) return;
|
||||
|
||||
print_req(request);
|
||||
|
||||
std::stringstream ss;
|
||||
ss << request->content.rdbuf();
|
||||
|
||||
pt::ptree inputTree, outputTree;
|
||||
|
||||
auto g = util::fail_guard([&]() {
|
||||
std::ostringstream data;
|
||||
pt::write_json(data, outputTree);
|
||||
response->write(data.str());
|
||||
});
|
||||
|
||||
try {
|
||||
// TODO: Input Validation
|
||||
pt::read_json(ss, inputTree);
|
||||
std::string pin = inputTree.get<std::string>("pin");
|
||||
outputTree.put("status", nvhttp::pin(pin));
|
||||
}
|
||||
catch (std::exception &e) {
|
||||
BOOST_LOG(warning) << "SavePin: "sv << e.what();
|
||||
outputTree.put("status", false);
|
||||
outputTree.put("error", e.what());
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
unpairAll(resp_https_t response, req_https_t request) {
|
||||
if (!authenticate(response, request)) return;
|
||||
|
||||
print_req(request);
|
||||
|
||||
pt::ptree outputTree;
|
||||
|
||||
auto g = util::fail_guard([&]() {
|
||||
std::ostringstream data;
|
||||
pt::write_json(data, outputTree);
|
||||
response->write(data.str());
|
||||
});
|
||||
nvhttp::erase_all_clients();
|
||||
outputTree.put("status", true);
|
||||
}
|
||||
|
||||
void
|
||||
closeApp(resp_https_t response, req_https_t request) {
|
||||
if (!authenticate(response, request)) return;
|
||||
|
||||
print_req(request);
|
||||
|
||||
pt::ptree outputTree;
|
||||
|
||||
auto g = util::fail_guard([&]() {
|
||||
std::ostringstream data;
|
||||
pt::write_json(data, outputTree);
|
||||
response->write(data.str());
|
||||
});
|
||||
|
||||
proc::proc.terminate();
|
||||
outputTree.put("status", true);
|
||||
}
|
||||
|
||||
void
|
||||
start() {
|
||||
auto shutdown_event = mail::man->event<bool>(mail::shutdown);
|
||||
|
||||
auto port_https = net::map_port(PORT_HTTPS);
|
||||
auto address_family = net::af_from_enum_string(config::sunshine.address_family);
|
||||
|
||||
https_server_t server { config::nvhttp.cert, config::nvhttp.pkey };
|
||||
server.default_resource["GET"] = not_found;
|
||||
server.resource["^/$"]["GET"] = getIndexPage;
|
||||
server.resource["^/pin/?$"]["GET"] = getPinPage;
|
||||
server.resource["^/apps/?$"]["GET"] = getAppsPage;
|
||||
server.resource["^/clients/?$"]["GET"] = getClientsPage;
|
||||
server.resource["^/config/?$"]["GET"] = getConfigPage;
|
||||
server.resource["^/password/?$"]["GET"] = getPasswordPage;
|
||||
server.resource["^/welcome/?$"]["GET"] = getWelcomePage;
|
||||
server.resource["^/troubleshooting/?$"]["GET"] = getTroubleshootingPage;
|
||||
server.resource["^/api/pin$"]["POST"] = savePin;
|
||||
server.resource["^/api/apps$"]["GET"] = getApps;
|
||||
server.resource["^/api/logs$"]["GET"] = getLogs;
|
||||
server.resource["^/api/apps$"]["POST"] = saveApp;
|
||||
server.resource["^/api/config$"]["GET"] = getConfig;
|
||||
server.resource["^/api/config$"]["POST"] = saveConfig;
|
||||
server.resource["^/api/configLocale$"]["GET"] = getLocale;
|
||||
server.resource["^/api/restart$"]["POST"] = restart;
|
||||
server.resource["^/api/password$"]["POST"] = savePassword;
|
||||
server.resource["^/api/apps/([0-9]+)$"]["DELETE"] = deleteApp;
|
||||
server.resource["^/api/clients/unpair$"]["POST"] = unpairAll;
|
||||
server.resource["^/api/apps/close$"]["POST"] = closeApp;
|
||||
server.resource["^/api/covers/upload$"]["POST"] = uploadCover;
|
||||
server.resource["^/images/sunshine.ico$"]["GET"] = getFaviconImage;
|
||||
server.resource["^/images/logo-sunshine-45.png$"]["GET"] = getSunshineLogoImage;
|
||||
server.resource["^/assets\\/.+$"]["GET"] = getNodeModules;
|
||||
server.config.reuse_address = true;
|
||||
server.config.address = net::af_to_any_address_string(address_family);
|
||||
server.config.port = port_https;
|
||||
|
||||
auto accept_and_run = [&](auto *server) {
|
||||
try {
|
||||
server->start([](unsigned short port) {
|
||||
BOOST_LOG(info) << "Configuration UI available at [https://localhost:"sv << port << "]";
|
||||
});
|
||||
}
|
||||
catch (boost::system::system_error &err) {
|
||||
// It's possible the exception gets thrown after calling server->stop() from a different thread
|
||||
if (shutdown_event->peek()) {
|
||||
return;
|
||||
}
|
||||
|
||||
BOOST_LOG(fatal) << "Couldn't start Configuration HTTPS server on port ["sv << port_https << "]: "sv << err.what();
|
||||
shutdown_event->raise(true);
|
||||
return;
|
||||
}
|
||||
};
|
||||
std::thread tcp { accept_and_run, &server };
|
||||
|
||||
// Wait for any event
|
||||
shutdown_event->view();
|
||||
|
||||
server.stop();
|
||||
|
||||
tcp.join();
|
||||
}
|
||||
} // namespace confighttp
|
||||
@ -1,37 +0,0 @@
|
||||
/**
|
||||
* @file src/confighttp.h
|
||||
* @brief todo
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include <functional>
|
||||
#include <string>
|
||||
|
||||
#include "thread_safe.h"
|
||||
|
||||
#define WEB_DIR SUNSHINE_ASSETS_DIR "/web/"
|
||||
|
||||
namespace confighttp {
|
||||
constexpr auto PORT_HTTPS = 1;
|
||||
void
|
||||
start();
|
||||
} // namespace confighttp
|
||||
|
||||
// mime types map
|
||||
const std::map<std::string, std::string> mime_types = {
|
||||
{ "css", "text/css" },
|
||||
{ "gif", "image/gif" },
|
||||
{ "htm", "text/html" },
|
||||
{ "html", "text/html" },
|
||||
{ "ico", "image/x-icon" },
|
||||
{ "jpeg", "image/jpeg" },
|
||||
{ "jpg", "image/jpeg" },
|
||||
{ "js", "application/javascript" },
|
||||
{ "json", "application/json" },
|
||||
{ "png", "image/png" },
|
||||
{ "svg", "image/svg+xml" },
|
||||
{ "ttf", "font/ttf" },
|
||||
{ "txt", "text/plain" },
|
||||
{ "woff2", "font/woff2" },
|
||||
{ "xml", "text/xml" },
|
||||
};
|
||||
510
src/crypto.cpp
510
src/crypto.cpp
@ -1,510 +0,0 @@
|
||||
/**
|
||||
* @file src/crypto.cpp
|
||||
* @brief todo
|
||||
*/
|
||||
#include "crypto.h"
|
||||
#include <openssl/pem.h>
|
||||
|
||||
namespace crypto {
|
||||
using asn1_string_t = util::safe_ptr<ASN1_STRING, ASN1_STRING_free>;
|
||||
|
||||
cert_chain_t::cert_chain_t():
|
||||
_certs {}, _cert_ctx { X509_STORE_CTX_new() } {}
|
||||
void
|
||||
cert_chain_t::add(x509_t &&cert) {
|
||||
x509_store_t x509_store { X509_STORE_new() };
|
||||
|
||||
X509_STORE_add_cert(x509_store.get(), cert.get());
|
||||
_certs.emplace_back(std::make_pair(std::move(cert), std::move(x509_store)));
|
||||
}
|
||||
|
||||
static int
|
||||
openssl_verify_cb(int ok, X509_STORE_CTX *ctx) {
|
||||
int err_code = X509_STORE_CTX_get_error(ctx);
|
||||
|
||||
switch (err_code) {
|
||||
// Expired or not-yet-valid certificates are fine. Sometimes Moonlight is running on embedded devices
|
||||
// that don't have accurate clocks (or haven't yet synchronized by the time Moonlight first runs).
|
||||
// This behavior also matches what GeForce Experience does.
|
||||
// FIXME: Checking for X509_V_ERR_UNABLE_TO_GET_ISSUER_CERT_LOCALLY is a temporary workaround to get moonlight-embedded to work on the raspberry pi
|
||||
case X509_V_ERR_UNABLE_TO_GET_ISSUER_CERT_LOCALLY:
|
||||
case X509_V_ERR_CERT_NOT_YET_VALID:
|
||||
case X509_V_ERR_CERT_HAS_EXPIRED:
|
||||
return 1;
|
||||
|
||||
default:
|
||||
return ok;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* When certificates from two or more instances of Moonlight have been added to x509_store_t,
|
||||
* only one of them will be verified by X509_verify_cert, resulting in only a single instance of
|
||||
* Moonlight to be able to use Sunshine
|
||||
*
|
||||
* To circumvent this, x509_store_t instance will be created for each instance of the certificates.
|
||||
*/
|
||||
const char *
|
||||
cert_chain_t::verify(x509_t::element_type *cert) {
|
||||
int err_code = 0;
|
||||
for (auto &[_, x509_store] : _certs) {
|
||||
auto fg = util::fail_guard([this]() {
|
||||
X509_STORE_CTX_cleanup(_cert_ctx.get());
|
||||
});
|
||||
|
||||
X509_STORE_CTX_init(_cert_ctx.get(), x509_store.get(), cert, nullptr);
|
||||
X509_STORE_CTX_set_verify_cb(_cert_ctx.get(), openssl_verify_cb);
|
||||
|
||||
// We don't care to validate the entire chain for the purposes of client auth.
|
||||
// Some versions of clients forked from Moonlight Embedded produce client certs
|
||||
// that OpenSSL doesn't detect as self-signed due to some X509v3 extensions.
|
||||
X509_STORE_CTX_set_flags(_cert_ctx.get(), X509_V_FLAG_PARTIAL_CHAIN);
|
||||
|
||||
auto err = X509_verify_cert(_cert_ctx.get());
|
||||
|
||||
if (err == 1) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
err_code = X509_STORE_CTX_get_error(_cert_ctx.get());
|
||||
|
||||
if (err_code != X509_V_ERR_DEPTH_ZERO_SELF_SIGNED_CERT && err_code != X509_V_ERR_INVALID_CA) {
|
||||
return X509_verify_cert_error_string(err_code);
|
||||
}
|
||||
}
|
||||
|
||||
return X509_verify_cert_error_string(err_code);
|
||||
}
|
||||
|
||||
namespace cipher {
|
||||
|
||||
static int
|
||||
init_decrypt_gcm(cipher_ctx_t &ctx, aes_t *key, aes_t *iv, bool padding) {
|
||||
ctx.reset(EVP_CIPHER_CTX_new());
|
||||
|
||||
if (!ctx) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (EVP_DecryptInit_ex(ctx.get(), EVP_aes_128_gcm(), nullptr, nullptr, nullptr) != 1) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (EVP_CIPHER_CTX_ctrl(ctx.get(), EVP_CTRL_GCM_SET_IVLEN, iv->size(), nullptr) != 1) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (EVP_DecryptInit_ex(ctx.get(), nullptr, nullptr, key->data(), iv->data()) != 1) {
|
||||
return -1;
|
||||
}
|
||||
EVP_CIPHER_CTX_set_padding(ctx.get(), padding);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int
|
||||
init_encrypt_gcm(cipher_ctx_t &ctx, aes_t *key, aes_t *iv, bool padding) {
|
||||
ctx.reset(EVP_CIPHER_CTX_new());
|
||||
|
||||
// Gen 7 servers use 128-bit AES ECB
|
||||
if (EVP_EncryptInit_ex(ctx.get(), EVP_aes_128_gcm(), nullptr, nullptr, nullptr) != 1) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (EVP_CIPHER_CTX_ctrl(ctx.get(), EVP_CTRL_GCM_SET_IVLEN, iv->size(), nullptr) != 1) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (EVP_EncryptInit_ex(ctx.get(), nullptr, nullptr, key->data(), iv->data()) != 1) {
|
||||
return -1;
|
||||
}
|
||||
EVP_CIPHER_CTX_set_padding(ctx.get(), padding);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int
|
||||
init_encrypt_cbc(cipher_ctx_t &ctx, aes_t *key, aes_t *iv, bool padding) {
|
||||
ctx.reset(EVP_CIPHER_CTX_new());
|
||||
|
||||
// Gen 7 servers use 128-bit AES ECB
|
||||
if (EVP_EncryptInit_ex(ctx.get(), EVP_aes_128_cbc(), nullptr, key->data(), iv->data()) != 1) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
EVP_CIPHER_CTX_set_padding(ctx.get(), padding);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int
|
||||
gcm_t::decrypt(const std::string_view &tagged_cipher, std::vector<std::uint8_t> &plaintext, aes_t *iv) {
|
||||
if (!decrypt_ctx && init_decrypt_gcm(decrypt_ctx, &key, iv, padding)) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Calling with cipher == nullptr results in a parameter change
|
||||
// without requiring a reallocation of the internal cipher ctx.
|
||||
if (EVP_DecryptInit_ex(decrypt_ctx.get(), nullptr, nullptr, nullptr, iv->data()) != 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
auto cipher = tagged_cipher.substr(tag_size);
|
||||
auto tag = tagged_cipher.substr(0, tag_size);
|
||||
|
||||
plaintext.resize(round_to_pkcs7_padded(cipher.size()));
|
||||
|
||||
int update_outlen, final_outlen;
|
||||
|
||||
if (EVP_DecryptUpdate(decrypt_ctx.get(), plaintext.data(), &update_outlen, (const std::uint8_t *) cipher.data(), cipher.size()) != 1) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (EVP_CIPHER_CTX_ctrl(decrypt_ctx.get(), EVP_CTRL_GCM_SET_TAG, tag.size(), const_cast<char *>(tag.data())) != 1) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (EVP_DecryptFinal_ex(decrypt_ctx.get(), plaintext.data() + update_outlen, &final_outlen) != 1) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
plaintext.resize(update_outlen + final_outlen);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int
|
||||
gcm_t::encrypt(const std::string_view &plaintext, std::uint8_t *tagged_cipher, aes_t *iv) {
|
||||
if (!encrypt_ctx && init_encrypt_gcm(encrypt_ctx, &key, iv, padding)) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Calling with cipher == nullptr results in a parameter change
|
||||
// without requiring a reallocation of the internal cipher ctx.
|
||||
if (EVP_EncryptInit_ex(encrypt_ctx.get(), nullptr, nullptr, nullptr, iv->data()) != 1) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
auto tag = tagged_cipher;
|
||||
auto cipher = tag + tag_size;
|
||||
|
||||
int update_outlen, final_outlen;
|
||||
|
||||
// Encrypt into the caller's buffer
|
||||
if (EVP_EncryptUpdate(encrypt_ctx.get(), cipher, &update_outlen, (const std::uint8_t *) plaintext.data(), plaintext.size()) != 1) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
// GCM encryption won't ever fill ciphertext here but we have to call it anyway
|
||||
if (EVP_EncryptFinal_ex(encrypt_ctx.get(), cipher + update_outlen, &final_outlen) != 1) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (EVP_CIPHER_CTX_ctrl(encrypt_ctx.get(), EVP_CTRL_GCM_GET_TAG, tag_size, tag) != 1) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
return update_outlen + final_outlen;
|
||||
}
|
||||
|
||||
int
|
||||
ecb_t::decrypt(const std::string_view &cipher, std::vector<std::uint8_t> &plaintext) {
|
||||
auto fg = util::fail_guard([this]() {
|
||||
EVP_CIPHER_CTX_reset(decrypt_ctx.get());
|
||||
});
|
||||
|
||||
// Gen 7 servers use 128-bit AES ECB
|
||||
if (EVP_DecryptInit_ex(decrypt_ctx.get(), EVP_aes_128_ecb(), nullptr, key.data(), nullptr) != 1) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
EVP_CIPHER_CTX_set_padding(decrypt_ctx.get(), padding);
|
||||
plaintext.resize(round_to_pkcs7_padded(cipher.size()));
|
||||
|
||||
int update_outlen, final_outlen;
|
||||
|
||||
if (EVP_DecryptUpdate(decrypt_ctx.get(), plaintext.data(), &update_outlen, (const std::uint8_t *) cipher.data(), cipher.size()) != 1) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (EVP_DecryptFinal_ex(decrypt_ctx.get(), plaintext.data() + update_outlen, &final_outlen) != 1) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
plaintext.resize(update_outlen + final_outlen);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int
|
||||
ecb_t::encrypt(const std::string_view &plaintext, std::vector<std::uint8_t> &cipher) {
|
||||
auto fg = util::fail_guard([this]() {
|
||||
EVP_CIPHER_CTX_reset(encrypt_ctx.get());
|
||||
});
|
||||
|
||||
// Gen 7 servers use 128-bit AES ECB
|
||||
if (EVP_EncryptInit_ex(encrypt_ctx.get(), EVP_aes_128_ecb(), nullptr, key.data(), nullptr) != 1) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
EVP_CIPHER_CTX_set_padding(encrypt_ctx.get(), padding);
|
||||
cipher.resize(round_to_pkcs7_padded(plaintext.size()));
|
||||
|
||||
int update_outlen, final_outlen;
|
||||
|
||||
// Encrypt into the caller's buffer
|
||||
if (EVP_EncryptUpdate(encrypt_ctx.get(), cipher.data(), &update_outlen, (const std::uint8_t *) plaintext.data(), plaintext.size()) != 1) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (EVP_EncryptFinal_ex(encrypt_ctx.get(), cipher.data() + update_outlen, &final_outlen) != 1) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
cipher.resize(update_outlen + final_outlen);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int
|
||||
cbc_t::encrypt(const std::string_view &plaintext, std::uint8_t *cipher, aes_t *iv) {
|
||||
if (!encrypt_ctx && init_encrypt_cbc(encrypt_ctx, &key, iv, padding)) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Calling with cipher == nullptr results in a parameter change
|
||||
// without requiring a reallocation of the internal cipher ctx.
|
||||
if (EVP_EncryptInit_ex(encrypt_ctx.get(), nullptr, nullptr, nullptr, iv->data()) != 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
int update_outlen, final_outlen;
|
||||
|
||||
// Encrypt into the caller's buffer
|
||||
if (EVP_EncryptUpdate(encrypt_ctx.get(), cipher, &update_outlen, (const std::uint8_t *) plaintext.data(), plaintext.size()) != 1) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (EVP_EncryptFinal_ex(encrypt_ctx.get(), cipher + update_outlen, &final_outlen) != 1) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
return update_outlen + final_outlen;
|
||||
}
|
||||
|
||||
ecb_t::ecb_t(const aes_t &key, bool padding):
|
||||
cipher_t { EVP_CIPHER_CTX_new(), EVP_CIPHER_CTX_new(), key, padding } {}
|
||||
|
||||
cbc_t::cbc_t(const aes_t &key, bool padding):
|
||||
cipher_t { nullptr, nullptr, key, padding } {}
|
||||
|
||||
gcm_t::gcm_t(const crypto::aes_t &key, bool padding):
|
||||
cipher_t { nullptr, nullptr, key, padding } {}
|
||||
|
||||
} // namespace cipher
|
||||
|
||||
aes_t
|
||||
gen_aes_key(const std::array<uint8_t, 16> &salt, const std::string_view &pin) {
|
||||
aes_t key(16);
|
||||
|
||||
std::string salt_pin;
|
||||
salt_pin.reserve(salt.size() + pin.size());
|
||||
|
||||
salt_pin.insert(std::end(salt_pin), std::begin(salt), std::end(salt));
|
||||
salt_pin.insert(std::end(salt_pin), std::begin(pin), std::end(pin));
|
||||
|
||||
auto hsh = hash(salt_pin);
|
||||
|
||||
std::copy(std::begin(hsh), std::begin(hsh) + key.size(), std::begin(key));
|
||||
|
||||
return key;
|
||||
}
|
||||
|
||||
sha256_t
|
||||
hash(const std::string_view &plaintext) {
|
||||
sha256_t hsh;
|
||||
EVP_Digest(plaintext.data(), plaintext.size(), hsh.data(), nullptr, EVP_sha256(), nullptr);
|
||||
return hsh;
|
||||
}
|
||||
|
||||
x509_t
|
||||
x509(const std::string_view &x) {
|
||||
bio_t io { BIO_new(BIO_s_mem()) };
|
||||
|
||||
BIO_write(io.get(), x.data(), x.size());
|
||||
|
||||
x509_t p;
|
||||
PEM_read_bio_X509(io.get(), &p, nullptr, nullptr);
|
||||
|
||||
return p;
|
||||
}
|
||||
|
||||
pkey_t
|
||||
pkey(const std::string_view &k) {
|
||||
bio_t io { BIO_new(BIO_s_mem()) };
|
||||
|
||||
BIO_write(io.get(), k.data(), k.size());
|
||||
|
||||
pkey_t p = nullptr;
|
||||
PEM_read_bio_PrivateKey(io.get(), &p, nullptr, nullptr);
|
||||
|
||||
return p;
|
||||
}
|
||||
|
||||
std::string
|
||||
pem(x509_t &x509) {
|
||||
bio_t bio { BIO_new(BIO_s_mem()) };
|
||||
|
||||
PEM_write_bio_X509(bio.get(), x509.get());
|
||||
BUF_MEM *mem_ptr;
|
||||
BIO_get_mem_ptr(bio.get(), &mem_ptr);
|
||||
|
||||
return { mem_ptr->data, mem_ptr->length };
|
||||
}
|
||||
|
||||
std::string
|
||||
pem(pkey_t &pkey) {
|
||||
bio_t bio { BIO_new(BIO_s_mem()) };
|
||||
|
||||
PEM_write_bio_PrivateKey(bio.get(), pkey.get(), nullptr, nullptr, 0, nullptr, nullptr);
|
||||
BUF_MEM *mem_ptr;
|
||||
BIO_get_mem_ptr(bio.get(), &mem_ptr);
|
||||
|
||||
return { mem_ptr->data, mem_ptr->length };
|
||||
}
|
||||
|
||||
std::string_view
|
||||
signature(const x509_t &x) {
|
||||
// X509_ALGOR *_ = nullptr;
|
||||
|
||||
const ASN1_BIT_STRING *asn1 = nullptr;
|
||||
X509_get0_signature(&asn1, nullptr, x.get());
|
||||
|
||||
return { (const char *) asn1->data, (std::size_t) asn1->length };
|
||||
}
|
||||
|
||||
std::string
|
||||
rand(std::size_t bytes) {
|
||||
std::string r;
|
||||
r.resize(bytes);
|
||||
|
||||
RAND_bytes((uint8_t *) r.data(), r.size());
|
||||
|
||||
return r;
|
||||
}
|
||||
|
||||
std::vector<uint8_t>
|
||||
sign(const pkey_t &pkey, const std::string_view &data, const EVP_MD *md) {
|
||||
md_ctx_t ctx { EVP_MD_CTX_create() };
|
||||
|
||||
if (EVP_DigestSignInit(ctx.get(), nullptr, md, nullptr, (EVP_PKEY *) pkey.get()) != 1) {
|
||||
return {};
|
||||
}
|
||||
|
||||
if (EVP_DigestSignUpdate(ctx.get(), data.data(), data.size()) != 1) {
|
||||
return {};
|
||||
}
|
||||
|
||||
std::size_t slen;
|
||||
if (EVP_DigestSignFinal(ctx.get(), nullptr, &slen) != 1) {
|
||||
return {};
|
||||
}
|
||||
|
||||
std::vector<uint8_t> digest(slen);
|
||||
if (EVP_DigestSignFinal(ctx.get(), digest.data(), &slen) != 1) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return digest;
|
||||
}
|
||||
|
||||
creds_t
|
||||
gen_creds(const std::string_view &cn, std::uint32_t key_bits) {
|
||||
x509_t x509 { X509_new() };
|
||||
pkey_ctx_t ctx { EVP_PKEY_CTX_new_id(EVP_PKEY_RSA, nullptr) };
|
||||
pkey_t pkey;
|
||||
|
||||
EVP_PKEY_keygen_init(ctx.get());
|
||||
EVP_PKEY_CTX_set_rsa_keygen_bits(ctx.get(), key_bits);
|
||||
EVP_PKEY_keygen(ctx.get(), &pkey);
|
||||
|
||||
X509_set_version(x509.get(), 2);
|
||||
|
||||
// Generate a real serial number to avoid SEC_ERROR_REUSED_ISSUER_AND_SERIAL with Firefox
|
||||
bignum_t serial { BN_new() };
|
||||
BN_rand(serial.get(), 159, BN_RAND_TOP_ANY, BN_RAND_BOTTOM_ANY); // 159 bits to fit in 20 bytes in DER format
|
||||
BN_set_negative(serial.get(), 0); // Serial numbers must be positive
|
||||
BN_to_ASN1_INTEGER(serial.get(), X509_get_serialNumber(x509.get()));
|
||||
|
||||
constexpr auto year = 60 * 60 * 24 * 365;
|
||||
#if OPENSSL_VERSION_NUMBER < 0x10100000L
|
||||
X509_gmtime_adj(X509_get_notBefore(x509.get()), 0);
|
||||
X509_gmtime_adj(X509_get_notAfter(x509.get()), 20 * year);
|
||||
#else
|
||||
asn1_string_t not_before { ASN1_STRING_dup(X509_get0_notBefore(x509.get())) };
|
||||
asn1_string_t not_after { ASN1_STRING_dup(X509_get0_notAfter(x509.get())) };
|
||||
|
||||
X509_gmtime_adj(not_before.get(), 0);
|
||||
X509_gmtime_adj(not_after.get(), 20 * year);
|
||||
|
||||
X509_set1_notBefore(x509.get(), not_before.get());
|
||||
X509_set1_notAfter(x509.get(), not_after.get());
|
||||
#endif
|
||||
|
||||
X509_set_pubkey(x509.get(), pkey.get());
|
||||
|
||||
auto name = X509_get_subject_name(x509.get());
|
||||
X509_NAME_add_entry_by_txt(name, "CN", MBSTRING_ASC,
|
||||
(const std::uint8_t *) cn.data(), cn.size(),
|
||||
-1, 0);
|
||||
|
||||
X509_set_issuer_name(x509.get(), name);
|
||||
X509_sign(x509.get(), pkey.get(), EVP_sha256());
|
||||
|
||||
return { pem(x509), pem(pkey) };
|
||||
}
|
||||
|
||||
std::vector<uint8_t>
|
||||
sign256(const pkey_t &pkey, const std::string_view &data) {
|
||||
return sign(pkey, data, EVP_sha256());
|
||||
}
|
||||
|
||||
bool
|
||||
verify(const x509_t &x509, const std::string_view &data, const std::string_view &signature, const EVP_MD *md) {
|
||||
auto pkey = X509_get0_pubkey(x509.get());
|
||||
|
||||
md_ctx_t ctx { EVP_MD_CTX_create() };
|
||||
|
||||
if (EVP_DigestVerifyInit(ctx.get(), nullptr, md, nullptr, pkey) != 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (EVP_DigestVerifyUpdate(ctx.get(), data.data(), data.size()) != 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (EVP_DigestVerifyFinal(ctx.get(), (const uint8_t *) signature.data(), signature.size()) != 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool
|
||||
verify256(const x509_t &x509, const std::string_view &data, const std::string_view &signature) {
|
||||
return verify(x509, data, signature, EVP_sha256());
|
||||
}
|
||||
|
||||
void
|
||||
md_ctx_destroy(EVP_MD_CTX *ctx) {
|
||||
EVP_MD_CTX_destroy(ctx);
|
||||
}
|
||||
|
||||
std::string
|
||||
rand_alphabet(std::size_t bytes, const std::string_view &alphabet) {
|
||||
auto value = rand(bytes);
|
||||
|
||||
for (std::size_t i = 0; i != value.size(); ++i) {
|
||||
value[i] = alphabet[value[i] % alphabet.length()];
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
} // namespace crypto
|
||||
157
src/crypto.h
157
src/crypto.h
@ -1,157 +0,0 @@
|
||||
/**
|
||||
* @file src/crypto.h
|
||||
* @brief todo
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include <array>
|
||||
#include <openssl/evp.h>
|
||||
#include <openssl/rand.h>
|
||||
#include <openssl/sha.h>
|
||||
#include <openssl/x509.h>
|
||||
|
||||
#include "utility.h"
|
||||
|
||||
namespace crypto {
|
||||
struct creds_t {
|
||||
std::string x509;
|
||||
std::string pkey;
|
||||
};
|
||||
|
||||
void
|
||||
md_ctx_destroy(EVP_MD_CTX *);
|
||||
|
||||
using sha256_t = std::array<std::uint8_t, SHA256_DIGEST_LENGTH>;
|
||||
|
||||
using aes_t = std::vector<std::uint8_t>;
|
||||
using x509_t = util::safe_ptr<X509, X509_free>;
|
||||
using x509_store_t = util::safe_ptr<X509_STORE, X509_STORE_free>;
|
||||
using x509_store_ctx_t = util::safe_ptr<X509_STORE_CTX, X509_STORE_CTX_free>;
|
||||
using cipher_ctx_t = util::safe_ptr<EVP_CIPHER_CTX, EVP_CIPHER_CTX_free>;
|
||||
using md_ctx_t = util::safe_ptr<EVP_MD_CTX, md_ctx_destroy>;
|
||||
using bio_t = util::safe_ptr<BIO, BIO_free_all>;
|
||||
using pkey_t = util::safe_ptr<EVP_PKEY, EVP_PKEY_free>;
|
||||
using pkey_ctx_t = util::safe_ptr<EVP_PKEY_CTX, EVP_PKEY_CTX_free>;
|
||||
using bignum_t = util::safe_ptr<BIGNUM, BN_free>;
|
||||
|
||||
sha256_t
|
||||
hash(const std::string_view &plaintext);
|
||||
|
||||
aes_t
|
||||
gen_aes_key(const std::array<uint8_t, 16> &salt, const std::string_view &pin);
|
||||
|
||||
x509_t
|
||||
x509(const std::string_view &x);
|
||||
pkey_t
|
||||
pkey(const std::string_view &k);
|
||||
std::string
|
||||
pem(x509_t &x509);
|
||||
std::string
|
||||
pem(pkey_t &pkey);
|
||||
|
||||
std::vector<uint8_t>
|
||||
sign256(const pkey_t &pkey, const std::string_view &data);
|
||||
bool
|
||||
verify256(const x509_t &x509, const std::string_view &data, const std::string_view &signature);
|
||||
|
||||
creds_t
|
||||
gen_creds(const std::string_view &cn, std::uint32_t key_bits);
|
||||
|
||||
std::string_view
|
||||
signature(const x509_t &x);
|
||||
|
||||
std::string
|
||||
rand(std::size_t bytes);
|
||||
std::string
|
||||
rand_alphabet(std::size_t bytes,
|
||||
const std::string_view &alphabet = std::string_view { "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!%&()=-" });
|
||||
|
||||
class cert_chain_t {
|
||||
public:
|
||||
KITTY_DECL_CONSTR(cert_chain_t)
|
||||
|
||||
void
|
||||
add(x509_t &&cert);
|
||||
|
||||
const char *
|
||||
verify(x509_t::element_type *cert);
|
||||
|
||||
private:
|
||||
std::vector<std::pair<x509_t, x509_store_t>> _certs;
|
||||
x509_store_ctx_t _cert_ctx;
|
||||
};
|
||||
|
||||
namespace cipher {
|
||||
constexpr std::size_t tag_size = 16;
|
||||
constexpr std::size_t
|
||||
round_to_pkcs7_padded(std::size_t size) {
|
||||
return ((size + 15) / 16) * 16;
|
||||
}
|
||||
|
||||
class cipher_t {
|
||||
public:
|
||||
cipher_ctx_t decrypt_ctx;
|
||||
cipher_ctx_t encrypt_ctx;
|
||||
|
||||
aes_t key;
|
||||
|
||||
bool padding;
|
||||
};
|
||||
|
||||
class ecb_t: public cipher_t {
|
||||
public:
|
||||
ecb_t() = default;
|
||||
ecb_t(ecb_t &&) noexcept = default;
|
||||
ecb_t &
|
||||
operator=(ecb_t &&) noexcept = default;
|
||||
|
||||
ecb_t(const aes_t &key, bool padding = true);
|
||||
|
||||
int
|
||||
encrypt(const std::string_view &plaintext, std::vector<std::uint8_t> &cipher);
|
||||
int
|
||||
decrypt(const std::string_view &cipher, std::vector<std::uint8_t> &plaintext);
|
||||
};
|
||||
|
||||
class gcm_t: public cipher_t {
|
||||
public:
|
||||
gcm_t() = default;
|
||||
gcm_t(gcm_t &&) noexcept = default;
|
||||
gcm_t &
|
||||
operator=(gcm_t &&) noexcept = default;
|
||||
|
||||
gcm_t(const crypto::aes_t &key, bool padding = true);
|
||||
|
||||
/**
|
||||
* length of cipher must be at least: round_to_pkcs7_padded(plaintext.size()) + crypto::cipher::tag_size
|
||||
*
|
||||
* return -1 on error
|
||||
* return bytes written on success
|
||||
*/
|
||||
int
|
||||
encrypt(const std::string_view &plaintext, std::uint8_t *tagged_cipher, aes_t *iv);
|
||||
|
||||
int
|
||||
decrypt(const std::string_view &cipher, std::vector<std::uint8_t> &plaintext, aes_t *iv);
|
||||
};
|
||||
|
||||
class cbc_t: public cipher_t {
|
||||
public:
|
||||
cbc_t() = default;
|
||||
cbc_t(cbc_t &&) noexcept = default;
|
||||
cbc_t &
|
||||
operator=(cbc_t &&) noexcept = default;
|
||||
|
||||
cbc_t(const crypto::aes_t &key, bool padding = true);
|
||||
|
||||
/**
|
||||
* length of cipher must be at least: round_to_pkcs7_padded(plaintext.size())
|
||||
*
|
||||
* return -1 on error
|
||||
* return bytes written on success
|
||||
*/
|
||||
int
|
||||
encrypt(const std::string_view &plaintext, std::uint8_t *cipher, aes_t *iv);
|
||||
};
|
||||
} // namespace cipher
|
||||
} // namespace crypto
|
||||
@ -1,396 +0,0 @@
|
||||
/**
|
||||
* @file entry_handler.cpp
|
||||
* @brief Entry point related functions.
|
||||
*/
|
||||
|
||||
// standard includes
|
||||
#include <csignal>
|
||||
#include <iostream>
|
||||
#include <thread>
|
||||
|
||||
// local includes
|
||||
#include "config.h"
|
||||
#include "confighttp.h"
|
||||
#include "entry_handler.h"
|
||||
#include "globals.h"
|
||||
#include "httpcommon.h"
|
||||
#include "logging.h"
|
||||
#include "network.h"
|
||||
#include "platform/common.h"
|
||||
#include "version.h"
|
||||
|
||||
extern "C" {
|
||||
#ifdef _WIN32
|
||||
#include <iphlpapi.h>
|
||||
#endif
|
||||
}
|
||||
|
||||
using namespace std::literals;
|
||||
|
||||
/**
|
||||
* @brief Launch the Web UI.
|
||||
*
|
||||
* EXAMPLES:
|
||||
* ```cpp
|
||||
* launch_ui();
|
||||
* ```
|
||||
*/
|
||||
void
|
||||
launch_ui() {
|
||||
std::string url = "https://localhost:" + std::to_string(net::map_port(confighttp::PORT_HTTPS));
|
||||
platf::open_url(url);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Launch the Web UI at a specific endpoint.
|
||||
*
|
||||
* EXAMPLES:
|
||||
* ```cpp
|
||||
* launch_ui_with_path("/pin");
|
||||
* ```
|
||||
*/
|
||||
void
|
||||
launch_ui_with_path(std::string path) {
|
||||
std::string url = "https://localhost:" + std::to_string(net::map_port(confighttp::PORT_HTTPS)) + path;
|
||||
platf::open_url(url);
|
||||
}
|
||||
|
||||
namespace args {
|
||||
/**
|
||||
* @brief Reset the user credentials.
|
||||
*
|
||||
* @param name The name of the program.
|
||||
* @param argc The number of arguments.
|
||||
* @param argv The arguments.
|
||||
*
|
||||
* EXAMPLES:
|
||||
* ```cpp
|
||||
* creds("sunshine", 2, {"new_username", "new_password"});
|
||||
* ```
|
||||
*/
|
||||
int
|
||||
creds(const char *name, int argc, char *argv[]) {
|
||||
if (argc < 2 || argv[0] == "help"sv || argv[1] == "help"sv) {
|
||||
help(name, argc, argv);
|
||||
}
|
||||
|
||||
http::save_user_creds(config::sunshine.credentials_file, argv[0], argv[1]);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Print help to stdout, then exit.
|
||||
* @param name The name of the program.
|
||||
* @param argc The number of arguments. (Unused)
|
||||
* @param argv The arguments. (Unused)
|
||||
*
|
||||
* EXAMPLES:
|
||||
* ```cpp
|
||||
* help("sunshine", 0, nullptr);
|
||||
* ```
|
||||
*/
|
||||
int
|
||||
help(const char *name, int argc, char *argv[]) {
|
||||
logging::print_help(name);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Print the version to stdout, then exit.
|
||||
* @param name The name of the program. (Unused)
|
||||
* @param argc The number of arguments. (Unused)
|
||||
* @param argv The arguments. (Unused)
|
||||
*
|
||||
* EXAMPLES:
|
||||
* ```cpp
|
||||
* version("sunshine", 0, nullptr);
|
||||
* ```
|
||||
*/
|
||||
int
|
||||
version(const char *name, int argc, char *argv[]) {
|
||||
// version was already logged at startup
|
||||
return 0;
|
||||
}
|
||||
|
||||
#ifdef _WIN32
|
||||
/**
|
||||
* @brief Restore global NVIDIA control panel settings.
|
||||
*
|
||||
* If Sunshine was improperly terminated, this function restores
|
||||
* the global NVIDIA control panel settings to the undo file left
|
||||
* by Sunshine. This function is typically called by the uninstaller.
|
||||
*
|
||||
* @param name The name of the program. (Unused)
|
||||
* @param argc The number of arguments. (Unused)
|
||||
* @param argv The arguments. (Unused)
|
||||
*
|
||||
* EXAMPLES:
|
||||
* ```cpp
|
||||
* restore_nvprefs_undo("sunshine", 0, nullptr);
|
||||
* ```
|
||||
*/
|
||||
int
|
||||
restore_nvprefs_undo(const char *name, int argc, char *argv[]) {
|
||||
if (nvprefs_instance.load()) {
|
||||
nvprefs_instance.restore_from_and_delete_undo_file_if_exists();
|
||||
nvprefs_instance.unload();
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
#endif
|
||||
} // namespace args
|
||||
|
||||
namespace lifetime {
|
||||
char **argv;
|
||||
std::atomic_int desired_exit_code;
|
||||
|
||||
/**
|
||||
* @brief Terminates Sunshine gracefully with the provided exit code.
|
||||
* @param exit_code The exit code to return from main().
|
||||
* @param async Specifies whether our termination will be non-blocking.
|
||||
*/
|
||||
void
|
||||
exit_sunshine(int exit_code, bool async) {
|
||||
// Store the exit code of the first exit_sunshine() call
|
||||
int zero = 0;
|
||||
desired_exit_code.compare_exchange_strong(zero, exit_code);
|
||||
|
||||
// Raise SIGINT to start termination
|
||||
std::raise(SIGINT);
|
||||
|
||||
// Termination will happen asynchronously, but the caller may
|
||||
// have wanted synchronous behavior.
|
||||
while (!async) {
|
||||
std::this_thread::sleep_for(1s);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Breaks into the debugger or terminates Sunshine if no debugger is attached.
|
||||
*/
|
||||
void
|
||||
debug_trap() {
|
||||
#ifdef _WIN32
|
||||
DebugBreak();
|
||||
#else
|
||||
std::raise(SIGTRAP);
|
||||
#endif
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Gets the argv array passed to main().
|
||||
*/
|
||||
char **
|
||||
get_argv() {
|
||||
return argv;
|
||||
}
|
||||
} // namespace lifetime
|
||||
|
||||
#ifdef _WIN32
|
||||
/**
|
||||
* @brief Check if NVIDIA's GameStream software is running.
|
||||
* @return `true` if GameStream is enabled, `false` otherwise.
|
||||
*/
|
||||
bool
|
||||
is_gamestream_enabled() {
|
||||
DWORD enabled;
|
||||
DWORD size = sizeof(enabled);
|
||||
return RegGetValueW(
|
||||
HKEY_LOCAL_MACHINE,
|
||||
L"SOFTWARE\\NVIDIA Corporation\\NvStream",
|
||||
L"EnableStreaming",
|
||||
RRF_RT_REG_DWORD,
|
||||
nullptr,
|
||||
&enabled,
|
||||
&size) == ERROR_SUCCESS &&
|
||||
enabled != 0;
|
||||
}
|
||||
|
||||
namespace service_ctrl {
|
||||
class service_controller {
|
||||
public:
|
||||
/**
|
||||
* @brief Constructor for service_controller class.
|
||||
* @param service_desired_access SERVICE_* desired access flags.
|
||||
*/
|
||||
service_controller(DWORD service_desired_access) {
|
||||
scm_handle = OpenSCManagerA(nullptr, nullptr, SC_MANAGER_CONNECT);
|
||||
if (!scm_handle) {
|
||||
auto winerr = GetLastError();
|
||||
BOOST_LOG(error) << "OpenSCManager() failed: "sv << winerr;
|
||||
return;
|
||||
}
|
||||
|
||||
service_handle = OpenServiceA(scm_handle, "SunshineService", service_desired_access);
|
||||
if (!service_handle) {
|
||||
auto winerr = GetLastError();
|
||||
BOOST_LOG(error) << "OpenService() failed: "sv << winerr;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
~service_controller() {
|
||||
if (service_handle) {
|
||||
CloseServiceHandle(service_handle);
|
||||
}
|
||||
|
||||
if (scm_handle) {
|
||||
CloseServiceHandle(scm_handle);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Asynchronously starts the Sunshine service.
|
||||
*/
|
||||
bool
|
||||
start_service() {
|
||||
if (!service_handle) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!StartServiceA(service_handle, 0, nullptr)) {
|
||||
auto winerr = GetLastError();
|
||||
if (winerr != ERROR_SERVICE_ALREADY_RUNNING) {
|
||||
BOOST_LOG(error) << "StartService() failed: "sv << winerr;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Query the service status.
|
||||
* @param status The SERVICE_STATUS struct to populate.
|
||||
*/
|
||||
bool
|
||||
query_service_status(SERVICE_STATUS &status) {
|
||||
if (!service_handle) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!QueryServiceStatus(service_handle, &status)) {
|
||||
auto winerr = GetLastError();
|
||||
BOOST_LOG(error) << "QueryServiceStatus() failed: "sv << winerr;
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private:
|
||||
SC_HANDLE scm_handle = NULL;
|
||||
SC_HANDLE service_handle = NULL;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Check if the service is running.
|
||||
*
|
||||
* EXAMPLES:
|
||||
* ```cpp
|
||||
* is_service_running();
|
||||
* ```
|
||||
*/
|
||||
bool
|
||||
is_service_running() {
|
||||
service_controller sc { SERVICE_QUERY_STATUS };
|
||||
|
||||
SERVICE_STATUS status;
|
||||
if (!sc.query_service_status(status)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return status.dwCurrentState == SERVICE_RUNNING;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Start the service and wait for startup to complete.
|
||||
*
|
||||
* EXAMPLES:
|
||||
* ```cpp
|
||||
* start_service();
|
||||
* ```
|
||||
*/
|
||||
bool
|
||||
start_service() {
|
||||
service_controller sc { SERVICE_QUERY_STATUS | SERVICE_START };
|
||||
|
||||
std::cout << "Starting Sunshine..."sv;
|
||||
|
||||
// This operation is asynchronous, so we must wait for it to complete
|
||||
if (!sc.start_service()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
SERVICE_STATUS status;
|
||||
do {
|
||||
Sleep(1000);
|
||||
std::cout << '.';
|
||||
} while (sc.query_service_status(status) && status.dwCurrentState == SERVICE_START_PENDING);
|
||||
|
||||
if (status.dwCurrentState != SERVICE_RUNNING) {
|
||||
BOOST_LOG(error) << SERVICE_NAME " failed to start: "sv << status.dwWin32ExitCode;
|
||||
return false;
|
||||
}
|
||||
|
||||
std::cout << std::endl;
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Wait for the UI to be ready after Sunshine startup.
|
||||
*
|
||||
* EXAMPLES:
|
||||
* ```cpp
|
||||
* wait_for_ui_ready();
|
||||
* ```
|
||||
*/
|
||||
bool
|
||||
wait_for_ui_ready() {
|
||||
std::cout << "Waiting for Web UI to be ready...";
|
||||
|
||||
// Wait up to 30 seconds for the web UI to start
|
||||
for (int i = 0; i < 30; i++) {
|
||||
PMIB_TCPTABLE tcp_table = nullptr;
|
||||
ULONG table_size = 0;
|
||||
ULONG err;
|
||||
|
||||
auto fg = util::fail_guard([&tcp_table]() {
|
||||
free(tcp_table);
|
||||
});
|
||||
|
||||
do {
|
||||
// Query all open TCP sockets to look for our web UI port
|
||||
err = GetTcpTable(tcp_table, &table_size, false);
|
||||
if (err == ERROR_INSUFFICIENT_BUFFER) {
|
||||
free(tcp_table);
|
||||
tcp_table = (PMIB_TCPTABLE) malloc(table_size);
|
||||
}
|
||||
} while (err == ERROR_INSUFFICIENT_BUFFER);
|
||||
|
||||
if (err != NO_ERROR) {
|
||||
BOOST_LOG(error) << "Failed to query TCP table: "sv << err;
|
||||
return false;
|
||||
}
|
||||
|
||||
uint16_t port_nbo = htons(net::map_port(confighttp::PORT_HTTPS));
|
||||
for (DWORD i = 0; i < tcp_table->dwNumEntries; i++) {
|
||||
auto &entry = tcp_table->table[i];
|
||||
|
||||
// Look for our port in the listening state
|
||||
if (entry.dwLocalPort == port_nbo && entry.dwState == MIB_TCP_STATE_LISTEN) {
|
||||
std::cout << std::endl;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
Sleep(1000);
|
||||
std::cout << '.';
|
||||
}
|
||||
|
||||
std::cout << "timed out"sv << std::endl;
|
||||
return false;
|
||||
}
|
||||
} // namespace service_ctrl
|
||||
#endif
|
||||
@ -1,62 +0,0 @@
|
||||
/**
|
||||
* @file entry_handler.h
|
||||
* @brief Header file for entry point functions.
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
// standard includes
|
||||
#include <atomic>
|
||||
#include <string_view>
|
||||
|
||||
// local includes
|
||||
#include "thread_pool.h"
|
||||
#include "thread_safe.h"
|
||||
|
||||
// functions
|
||||
void
|
||||
launch_ui();
|
||||
void
|
||||
launch_ui_with_path(std::string path);
|
||||
|
||||
#ifdef _WIN32
|
||||
// windows only functions
|
||||
bool
|
||||
is_gamestream_enabled();
|
||||
#endif
|
||||
|
||||
namespace args {
|
||||
int
|
||||
creds(const char *name, int argc, char *argv[]);
|
||||
int
|
||||
help(const char *name, int argc, char *argv[]);
|
||||
int
|
||||
version(const char *name, int argc, char *argv[]);
|
||||
#ifdef _WIN32
|
||||
int
|
||||
restore_nvprefs_undo(const char *name, int argc, char *argv[]);
|
||||
#endif
|
||||
} // namespace args
|
||||
|
||||
namespace lifetime {
|
||||
extern char **argv;
|
||||
extern std::atomic_int desired_exit_code;
|
||||
void
|
||||
exit_sunshine(int exit_code, bool async);
|
||||
void
|
||||
debug_trap();
|
||||
char **
|
||||
get_argv();
|
||||
} // namespace lifetime
|
||||
|
||||
#ifdef _WIN32
|
||||
namespace service_ctrl {
|
||||
bool
|
||||
is_service_running();
|
||||
|
||||
bool
|
||||
start_service();
|
||||
|
||||
bool
|
||||
wait_for_ui_ready();
|
||||
} // namespace service_ctrl
|
||||
#endif
|
||||
@ -4,8 +4,8 @@
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "entry_handler.h"
|
||||
#include "thread_pool.h"
|
||||
#include "thread_safe.h"
|
||||
|
||||
extern thread_pool_util::ThreadPool task_pool;
|
||||
extern bool display_cursor;
|
||||
|
||||
@ -1,248 +0,0 @@
|
||||
/**
|
||||
* @file src/httpcommon.cpp
|
||||
* @brief todo
|
||||
*/
|
||||
#define BOOST_BIND_GLOBAL_PLACEHOLDERS
|
||||
|
||||
#include "process.h"
|
||||
|
||||
#include <filesystem>
|
||||
|
||||
#include <boost/property_tree/json_parser.hpp>
|
||||
#include <boost/property_tree/ptree.hpp>
|
||||
#include <boost/property_tree/xml_parser.hpp>
|
||||
|
||||
#include <boost/asio/ssl/context.hpp>
|
||||
|
||||
#include <Simple-Web-Server/server_http.hpp>
|
||||
#include <Simple-Web-Server/server_https.hpp>
|
||||
#include <boost/asio/ssl/context_base.hpp>
|
||||
#include <curl/curl.h>
|
||||
|
||||
#include "config.h"
|
||||
#include "crypto.h"
|
||||
#include "file_handler.h"
|
||||
#include "httpcommon.h"
|
||||
#include "logging.h"
|
||||
#include "network.h"
|
||||
#include "nvhttp.h"
|
||||
#include "platform/common.h"
|
||||
#include "rtsp.h"
|
||||
#include "utility.h"
|
||||
#include "uuid.h"
|
||||
|
||||
namespace http {
|
||||
using namespace std::literals;
|
||||
namespace fs = std::filesystem;
|
||||
namespace pt = boost::property_tree;
|
||||
|
||||
int
|
||||
reload_user_creds(const std::string &file);
|
||||
bool
|
||||
user_creds_exist(const std::string &file);
|
||||
|
||||
std::string unique_id;
|
||||
net::net_e origin_web_ui_allowed;
|
||||
|
||||
int
|
||||
init() {
|
||||
bool clean_slate = config::sunshine.flags[config::flag::FRESH_STATE];
|
||||
origin_web_ui_allowed = net::from_enum_string(config::nvhttp.origin_web_ui_allowed);
|
||||
|
||||
if (clean_slate) {
|
||||
unique_id = uuid_util::uuid_t::generate().string();
|
||||
auto dir = std::filesystem::temp_directory_path() / "Sunshine"sv;
|
||||
config::nvhttp.cert = (dir / ("cert-"s + unique_id)).string();
|
||||
config::nvhttp.pkey = (dir / ("pkey-"s + unique_id)).string();
|
||||
}
|
||||
|
||||
if (!fs::exists(config::nvhttp.pkey) || !fs::exists(config::nvhttp.cert)) {
|
||||
if (create_creds(config::nvhttp.pkey, config::nvhttp.cert)) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
if (user_creds_exist(config::sunshine.credentials_file)) {
|
||||
if (reload_user_creds(config::sunshine.credentials_file)) return -1;
|
||||
}
|
||||
else {
|
||||
BOOST_LOG(info) << "Open the Web UI to set your new username and password and getting started";
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int
|
||||
save_user_creds(const std::string &file, const std::string &username, const std::string &password, bool run_our_mouth) {
|
||||
pt::ptree outputTree;
|
||||
|
||||
if (fs::exists(file)) {
|
||||
try {
|
||||
pt::read_json(file, outputTree);
|
||||
}
|
||||
catch (std::exception &e) {
|
||||
BOOST_LOG(error) << "Couldn't read user credentials: "sv << e.what();
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
auto salt = crypto::rand_alphabet(16);
|
||||
outputTree.put("username", username);
|
||||
outputTree.put("salt", salt);
|
||||
outputTree.put("password", util::hex(crypto::hash(password + salt)).to_string());
|
||||
try {
|
||||
pt::write_json(file, outputTree);
|
||||
}
|
||||
catch (std::exception &e) {
|
||||
BOOST_LOG(error) << "error writing to the credentials file, perhaps try this again as an administrator? Details: "sv << e.what();
|
||||
return -1;
|
||||
}
|
||||
|
||||
BOOST_LOG(info) << "New credentials have been created"sv;
|
||||
return 0;
|
||||
}
|
||||
|
||||
bool
|
||||
user_creds_exist(const std::string &file) {
|
||||
if (!fs::exists(file)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
pt::ptree inputTree;
|
||||
try {
|
||||
pt::read_json(file, inputTree);
|
||||
return inputTree.find("username") != inputTree.not_found() &&
|
||||
inputTree.find("password") != inputTree.not_found() &&
|
||||
inputTree.find("salt") != inputTree.not_found();
|
||||
}
|
||||
catch (std::exception &e) {
|
||||
BOOST_LOG(error) << "validating user credentials: "sv << e.what();
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
int
|
||||
reload_user_creds(const std::string &file) {
|
||||
pt::ptree inputTree;
|
||||
try {
|
||||
pt::read_json(file, inputTree);
|
||||
config::sunshine.username = inputTree.get<std::string>("username");
|
||||
config::sunshine.password = inputTree.get<std::string>("password");
|
||||
config::sunshine.salt = inputTree.get<std::string>("salt");
|
||||
}
|
||||
catch (std::exception &e) {
|
||||
BOOST_LOG(error) << "loading user credentials: "sv << e.what();
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int
|
||||
create_creds(const std::string &pkey, const std::string &cert) {
|
||||
fs::path pkey_path = pkey;
|
||||
fs::path cert_path = cert;
|
||||
|
||||
auto creds = crypto::gen_creds("Sunshine Gamestream Host"sv, 2048);
|
||||
|
||||
auto pkey_dir = pkey_path;
|
||||
auto cert_dir = cert_path;
|
||||
pkey_dir.remove_filename();
|
||||
cert_dir.remove_filename();
|
||||
|
||||
std::error_code err_code {};
|
||||
fs::create_directories(pkey_dir, err_code);
|
||||
if (err_code) {
|
||||
BOOST_LOG(error) << "Couldn't create directory ["sv << pkey_dir << "] :"sv << err_code.message();
|
||||
return -1;
|
||||
}
|
||||
|
||||
fs::create_directories(cert_dir, err_code);
|
||||
if (err_code) {
|
||||
BOOST_LOG(error) << "Couldn't create directory ["sv << cert_dir << "] :"sv << err_code.message();
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (file_handler::write_file(pkey.c_str(), creds.pkey)) {
|
||||
BOOST_LOG(error) << "Couldn't open ["sv << config::nvhttp.pkey << ']';
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (file_handler::write_file(cert.c_str(), creds.x509)) {
|
||||
BOOST_LOG(error) << "Couldn't open ["sv << config::nvhttp.cert << ']';
|
||||
return -1;
|
||||
}
|
||||
|
||||
fs::permissions(pkey_path,
|
||||
fs::perms::owner_read | fs::perms::owner_write,
|
||||
fs::perm_options::replace, err_code);
|
||||
|
||||
if (err_code) {
|
||||
BOOST_LOG(error) << "Couldn't change permissions of ["sv << config::nvhttp.pkey << "] :"sv << err_code.message();
|
||||
return -1;
|
||||
}
|
||||
|
||||
fs::permissions(cert_path,
|
||||
fs::perms::owner_read | fs::perms::group_read | fs::perms::others_read | fs::perms::owner_write,
|
||||
fs::perm_options::replace, err_code);
|
||||
|
||||
if (err_code) {
|
||||
BOOST_LOG(error) << "Couldn't change permissions of ["sv << config::nvhttp.cert << "] :"sv << err_code.message();
|
||||
return -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
bool
|
||||
download_file(const std::string &url, const std::string &file) {
|
||||
CURL *curl = curl_easy_init();
|
||||
if (!curl) {
|
||||
BOOST_LOG(error) << "Couldn't create CURL instance";
|
||||
return false;
|
||||
}
|
||||
FILE *fp = fopen(file.c_str(), "wb");
|
||||
if (!fp) {
|
||||
BOOST_LOG(error) << "Couldn't open ["sv << file << ']';
|
||||
curl_easy_cleanup(curl);
|
||||
return false;
|
||||
}
|
||||
curl_easy_setopt(curl, CURLOPT_URL, url.c_str());
|
||||
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, fwrite);
|
||||
curl_easy_setopt(curl, CURLOPT_WRITEDATA, fp);
|
||||
#ifdef _WIN32
|
||||
curl_easy_setopt(curl, CURLOPT_SSL_OPTIONS, CURLSSLOPT_NATIVE_CA);
|
||||
#endif
|
||||
CURLcode result = curl_easy_perform(curl);
|
||||
if (result != CURLE_OK) {
|
||||
BOOST_LOG(error) << "Couldn't download ["sv << url << ", code:" << result << ']';
|
||||
}
|
||||
curl_easy_cleanup(curl);
|
||||
fclose(fp);
|
||||
return result == CURLE_OK;
|
||||
}
|
||||
|
||||
std::string
|
||||
url_escape(const std::string &url) {
|
||||
CURL *curl = curl_easy_init();
|
||||
char *string = curl_easy_escape(curl, url.c_str(), url.length());
|
||||
std::string result(string);
|
||||
curl_free(string);
|
||||
curl_easy_cleanup(curl);
|
||||
return result;
|
||||
}
|
||||
|
||||
std::string
|
||||
url_get_host(const std::string &url) {
|
||||
CURLU *curlu = curl_url();
|
||||
curl_url_set(curlu, CURLUPART_URL, url.c_str(), url.length());
|
||||
char *host;
|
||||
if (curl_url_get(curlu, CURLUPART_HOST, &host, 0) != CURLUE_OK) {
|
||||
curl_url_cleanup(curlu);
|
||||
return "";
|
||||
}
|
||||
std::string result(host);
|
||||
curl_free(host);
|
||||
curl_url_cleanup(curlu);
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace http
|
||||
@ -1,35 +0,0 @@
|
||||
/**
|
||||
* @file src/httpcommon.h
|
||||
* @brief todo
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "network.h"
|
||||
#include "thread_safe.h"
|
||||
|
||||
namespace http {
|
||||
|
||||
int
|
||||
init();
|
||||
int
|
||||
create_creds(const std::string &pkey, const std::string &cert);
|
||||
int
|
||||
save_user_creds(
|
||||
const std::string &file,
|
||||
const std::string &username,
|
||||
const std::string &password,
|
||||
bool run_our_mouth = false);
|
||||
|
||||
int
|
||||
reload_user_creds(const std::string &file);
|
||||
bool
|
||||
download_file(const std::string &url, const std::string &file);
|
||||
std::string
|
||||
url_escape(const std::string &url);
|
||||
std::string
|
||||
url_get_host(const std::string &url);
|
||||
|
||||
extern std::string unique_id;
|
||||
extern net::net_e origin_web_ui_allowed;
|
||||
|
||||
} // namespace http
|
||||
30
src/main.cpp
30
src/main.cpp
@ -7,21 +7,17 @@
|
||||
#include <csignal>
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
#include <Windows.h>
|
||||
|
||||
// local includes
|
||||
#include "globals.h"
|
||||
#include "logging.h"
|
||||
#include "main.h"
|
||||
#include "nvhttp.h"
|
||||
#include "process.h"
|
||||
#include "system_tray.h"
|
||||
#include "upnp.h"
|
||||
#include "version.h"
|
||||
#include "video.h"
|
||||
#include "config.h"
|
||||
|
||||
extern "C" {
|
||||
#include <rs.h>
|
||||
}
|
||||
|
||||
using namespace std::literals;
|
||||
|
||||
@ -52,7 +48,6 @@ on_signal(int sig, FN &&fn) {
|
||||
*/
|
||||
int
|
||||
main(int argc, char *argv[]) {
|
||||
lifetime::argv = argv;
|
||||
|
||||
task_pool_util::TaskPool::task_id_t force_shutdown = nullptr;
|
||||
|
||||
@ -113,7 +108,6 @@ main(int argc, char *argv[]) {
|
||||
auto task = []() {
|
||||
BOOST_LOG(fatal) << "10 seconds passed, yet Sunshine's still running: Forcing shutdown"sv;
|
||||
logging::log_flush();
|
||||
lifetime::debug_trap();
|
||||
};
|
||||
force_shutdown = task_pool.pushDelayed(task, 10s).task_id;
|
||||
|
||||
@ -126,15 +120,12 @@ main(int argc, char *argv[]) {
|
||||
auto task = []() {
|
||||
BOOST_LOG(fatal) << "10 seconds passed, yet Sunshine's still running: Forcing shutdown"sv;
|
||||
logging::log_flush();
|
||||
lifetime::debug_trap();
|
||||
};
|
||||
force_shutdown = task_pool.pushDelayed(task, 10s).task_id;
|
||||
|
||||
shutdown_event->raise(true);
|
||||
});
|
||||
|
||||
proc::refresh(config::stream.file_apps);
|
||||
|
||||
// If any of the following fail, we log an error and continue event though sunshine will not function correctly.
|
||||
// This allows access to the UI to fix configuration problems or view the logs.
|
||||
|
||||
@ -143,12 +134,6 @@ main(int argc, char *argv[]) {
|
||||
BOOST_LOG(error) << "Platform failed to initialize"sv;
|
||||
}
|
||||
|
||||
auto proc_deinit_guard = proc::init();
|
||||
if (!proc_deinit_guard) {
|
||||
BOOST_LOG(error) << "Proc failed to initialize"sv;
|
||||
}
|
||||
|
||||
reed_solomon_init();
|
||||
auto input_deinit_guard = input::init();
|
||||
if (video::probe_encoders()) {
|
||||
BOOST_LOG(error) << "Video failed to find working encoder"sv;
|
||||
@ -157,17 +142,10 @@ main(int argc, char *argv[]) {
|
||||
|
||||
// FIXME: Temporary workaround: Simple-Web_server needs to be updated or replaced
|
||||
if (shutdown_event->peek()) {
|
||||
return lifetime::desired_exit_code;
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
#ifdef _WIN32
|
||||
// If we're using the default port and GameStream is enabled, warn the user
|
||||
if (config::sunshine.port == 47989 && is_gamestream_enabled()) {
|
||||
BOOST_LOG(fatal) << "GameStream is still enabled in GeForce Experience! This *will* cause streaming problems with Sunshine!"sv;
|
||||
BOOST_LOG(fatal) << "Disable GameStream on the SHIELD tab in GeForce Experience or change the Port setting on the Advanced tab in the Sunshine Web UI."sv;
|
||||
}
|
||||
#endif
|
||||
|
||||
task_pool.stop();
|
||||
task_pool.join();
|
||||
@ -180,5 +158,5 @@ main(int argc, char *argv[]) {
|
||||
}
|
||||
#endif
|
||||
|
||||
return lifetime::desired_exit_code;
|
||||
return 0;
|
||||
}
|
||||
|
||||
251
src/network.cpp
251
src/network.cpp
@ -1,251 +0,0 @@
|
||||
/**
|
||||
* @file src/network.cpp
|
||||
* @brief todo
|
||||
*/
|
||||
#include "network.h"
|
||||
#include "config.h"
|
||||
#include "logging.h"
|
||||
#include "utility.h"
|
||||
#include <algorithm>
|
||||
|
||||
using namespace std::literals;
|
||||
|
||||
namespace ip = boost::asio::ip;
|
||||
|
||||
namespace net {
|
||||
std::vector<ip::network_v4> pc_ips_v4 {
|
||||
ip::make_network_v4("127.0.0.0/8"sv),
|
||||
};
|
||||
std::vector<ip::network_v4> lan_ips_v4 {
|
||||
ip::make_network_v4("192.168.0.0/16"sv),
|
||||
ip::make_network_v4("172.16.0.0/12"sv),
|
||||
ip::make_network_v4("10.0.0.0/8"sv),
|
||||
ip::make_network_v4("100.64.0.0/10"sv),
|
||||
ip::make_network_v4("169.254.0.0/16"sv),
|
||||
};
|
||||
|
||||
std::vector<ip::network_v6> pc_ips_v6 {
|
||||
ip::make_network_v6("::1/128"sv),
|
||||
};
|
||||
std::vector<ip::network_v6> lan_ips_v6 {
|
||||
ip::make_network_v6("fc00::/7"sv),
|
||||
ip::make_network_v6("fe80::/64"sv),
|
||||
};
|
||||
|
||||
net_e
|
||||
from_enum_string(const std::string_view &view) {
|
||||
if (view == "wan") {
|
||||
return WAN;
|
||||
}
|
||||
if (view == "lan") {
|
||||
return LAN;
|
||||
}
|
||||
|
||||
return PC;
|
||||
}
|
||||
|
||||
net_e
|
||||
from_address(const std::string_view &view) {
|
||||
auto addr = normalize_address(ip::make_address(view));
|
||||
|
||||
if (addr.is_v6()) {
|
||||
for (auto &range : pc_ips_v6) {
|
||||
if (range.hosts().find(addr.to_v6()) != range.hosts().end()) {
|
||||
return PC;
|
||||
}
|
||||
}
|
||||
|
||||
for (auto &range : lan_ips_v6) {
|
||||
if (range.hosts().find(addr.to_v6()) != range.hosts().end()) {
|
||||
return LAN;
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
for (auto &range : pc_ips_v4) {
|
||||
if (range.hosts().find(addr.to_v4()) != range.hosts().end()) {
|
||||
return PC;
|
||||
}
|
||||
}
|
||||
|
||||
for (auto &range : lan_ips_v4) {
|
||||
if (range.hosts().find(addr.to_v4()) != range.hosts().end()) {
|
||||
return LAN;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return WAN;
|
||||
}
|
||||
|
||||
std::string_view
|
||||
to_enum_string(net_e net) {
|
||||
switch (net) {
|
||||
case PC:
|
||||
return "pc"sv;
|
||||
case LAN:
|
||||
return "lan"sv;
|
||||
case WAN:
|
||||
return "wan"sv;
|
||||
}
|
||||
|
||||
// avoid warning
|
||||
return "wan"sv;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Returns the `af_e` enum value for the `address_family` config option value.
|
||||
* @param view The config option value.
|
||||
* @return The `af_e` enum value.
|
||||
*/
|
||||
af_e
|
||||
af_from_enum_string(const std::string_view &view) {
|
||||
if (view == "ipv4") {
|
||||
return IPV4;
|
||||
}
|
||||
if (view == "both") {
|
||||
return BOTH;
|
||||
}
|
||||
|
||||
// avoid warning
|
||||
return BOTH;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Returns the wildcard binding address for a given address family.
|
||||
* @param af Address family.
|
||||
* @return Normalized address.
|
||||
*/
|
||||
std::string_view
|
||||
af_to_any_address_string(af_e af) {
|
||||
switch (af) {
|
||||
case IPV4:
|
||||
return "0.0.0.0"sv;
|
||||
case BOTH:
|
||||
return "::"sv;
|
||||
}
|
||||
|
||||
// avoid warning
|
||||
return "::"sv;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Converts an address to a normalized form.
|
||||
* @details Normalization converts IPv4-mapped IPv6 addresses into IPv4 addresses.
|
||||
* @param address The address to normalize.
|
||||
* @return Normalized address.
|
||||
*/
|
||||
boost::asio::ip::address
|
||||
normalize_address(boost::asio::ip::address address) {
|
||||
// Convert IPv6-mapped IPv4 addresses into regular IPv4 addresses
|
||||
if (address.is_v6()) {
|
||||
auto v6 = address.to_v6();
|
||||
if (v6.is_v4_mapped()) {
|
||||
return boost::asio::ip::make_address_v4(boost::asio::ip::v4_mapped, v6);
|
||||
}
|
||||
}
|
||||
|
||||
return address;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Returns the given address in normalized string form.
|
||||
* @details Normalization converts IPv4-mapped IPv6 addresses into IPv4 addresses.
|
||||
* @param address The address to normalize.
|
||||
* @return Normalized address in string form.
|
||||
*/
|
||||
std::string
|
||||
addr_to_normalized_string(boost::asio::ip::address address) {
|
||||
return normalize_address(address).to_string();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Returns the given address in a normalized form for in the host portion of a URL.
|
||||
* @details Normalization converts IPv4-mapped IPv6 addresses into IPv4 addresses.
|
||||
* @param address The address to normalize and escape.
|
||||
* @return Normalized address in URL-escaped string.
|
||||
*/
|
||||
std::string
|
||||
addr_to_url_escaped_string(boost::asio::ip::address address) {
|
||||
address = normalize_address(address);
|
||||
if (address.is_v6()) {
|
||||
return "["s + address.to_string() + ']';
|
||||
}
|
||||
else {
|
||||
return address.to_string();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Returns the encryption mode for the given remote endpoint address.
|
||||
* @param address The address used to look up the desired encryption mode.
|
||||
* @return The WAN or LAN encryption mode, based on the provided address.
|
||||
*/
|
||||
int
|
||||
encryption_mode_for_address(boost::asio::ip::address address) {
|
||||
auto nettype = net::from_address(address.to_string());
|
||||
if (nettype == net::net_e::PC || nettype == net::net_e::LAN) {
|
||||
return config::stream.lan_encryption_mode;
|
||||
}
|
||||
else {
|
||||
return config::stream.wan_encryption_mode;
|
||||
}
|
||||
}
|
||||
|
||||
host_t
|
||||
host_create(af_e af, ENetAddress &addr, std::size_t peers, std::uint16_t port) {
|
||||
static std::once_flag enet_init_flag;
|
||||
std::call_once(enet_init_flag, []() {
|
||||
enet_initialize();
|
||||
});
|
||||
|
||||
auto any_addr = net::af_to_any_address_string(af);
|
||||
enet_address_set_host(&addr, any_addr.data());
|
||||
enet_address_set_port(&addr, port);
|
||||
|
||||
auto host = host_t { enet_host_create(af == IPV4 ? AF_INET : AF_INET6, &addr, peers, 0, 0, 0) };
|
||||
|
||||
// Enable opportunistic QoS tagging (automatically disables if the network appears to drop tagged packets)
|
||||
enet_socket_set_option(host->socket, ENET_SOCKOPT_QOS, 1);
|
||||
|
||||
return host;
|
||||
}
|
||||
|
||||
void
|
||||
free_host(ENetHost *host) {
|
||||
std::for_each(host->peers, host->peers + host->peerCount, [](ENetPeer &peer_ref) {
|
||||
ENetPeer *peer = &peer_ref;
|
||||
|
||||
if (peer) {
|
||||
enet_peer_disconnect_now(peer, 0);
|
||||
}
|
||||
});
|
||||
|
||||
enet_host_destroy(host);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Map a specified port based on the base port.
|
||||
* @param port The port to map as a difference from the base port.
|
||||
* @return `std:uint16_t` : The mapped port number.
|
||||
*
|
||||
* EXAMPLES:
|
||||
* ```cpp
|
||||
* std::uint16_t mapped_port = net::map_port(1);
|
||||
* ```
|
||||
*/
|
||||
std::uint16_t
|
||||
map_port(int port) {
|
||||
// calculate the port from the config port
|
||||
auto mapped_port = (std::uint16_t)((int) config::sunshine.port + port);
|
||||
|
||||
// Ensure port is in the range of 1024-65535
|
||||
if (mapped_port < 1024 || mapped_port > 65535) {
|
||||
BOOST_LOG(warning) << "Port out of range: "sv << mapped_port;
|
||||
}
|
||||
|
||||
// TODO: Ensure port is not already in use by another application
|
||||
|
||||
return mapped_port;
|
||||
}
|
||||
} // namespace net
|
||||
@ -1,98 +0,0 @@
|
||||
/**
|
||||
* @file src/network.h
|
||||
* @brief todo
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include <tuple>
|
||||
|
||||
#include <boost/asio.hpp>
|
||||
|
||||
#include <enet/enet.h>
|
||||
|
||||
#include "utility.h"
|
||||
|
||||
namespace net {
|
||||
void
|
||||
free_host(ENetHost *host);
|
||||
|
||||
std::uint16_t
|
||||
map_port(int port);
|
||||
|
||||
using host_t = util::safe_ptr<ENetHost, free_host>;
|
||||
using peer_t = ENetPeer *;
|
||||
using packet_t = util::safe_ptr<ENetPacket, enet_packet_destroy>;
|
||||
|
||||
enum net_e : int {
|
||||
PC,
|
||||
LAN,
|
||||
WAN
|
||||
};
|
||||
|
||||
enum af_e : int {
|
||||
IPV4,
|
||||
BOTH
|
||||
};
|
||||
|
||||
net_e
|
||||
from_enum_string(const std::string_view &view);
|
||||
std::string_view
|
||||
to_enum_string(net_e net);
|
||||
|
||||
net_e
|
||||
from_address(const std::string_view &view);
|
||||
|
||||
host_t
|
||||
host_create(af_e af, ENetAddress &addr, std::size_t peers, std::uint16_t port);
|
||||
|
||||
/**
|
||||
* @brief Returns the `af_e` enum value for the `address_family` config option value.
|
||||
* @param view The config option value.
|
||||
* @return The `af_e` enum value.
|
||||
*/
|
||||
af_e
|
||||
af_from_enum_string(const std::string_view &view);
|
||||
|
||||
/**
|
||||
* @brief Returns the wildcard binding address for a given address family.
|
||||
* @param af Address family.
|
||||
* @return Normalized address.
|
||||
*/
|
||||
std::string_view
|
||||
af_to_any_address_string(af_e af);
|
||||
|
||||
/**
|
||||
* @brief Converts an address to a normalized form.
|
||||
* @details Normalization converts IPv4-mapped IPv6 addresses into IPv4 addresses.
|
||||
* @param address The address to normalize.
|
||||
* @return Normalized address.
|
||||
*/
|
||||
boost::asio::ip::address
|
||||
normalize_address(boost::asio::ip::address address);
|
||||
|
||||
/**
|
||||
* @brief Returns the given address in normalized string form.
|
||||
* @details Normalization converts IPv4-mapped IPv6 addresses into IPv4 addresses.
|
||||
* @param address The address to normalize.
|
||||
* @return Normalized address in string form.
|
||||
*/
|
||||
std::string
|
||||
addr_to_normalized_string(boost::asio::ip::address address);
|
||||
|
||||
/**
|
||||
* @brief Returns the given address in a normalized form for in the host portion of a URL.
|
||||
* @details Normalization converts IPv4-mapped IPv6 addresses into IPv4 addresses.
|
||||
* @param address The address to normalize and escape.
|
||||
* @return Normalized address in URL-escaped string.
|
||||
*/
|
||||
std::string
|
||||
addr_to_url_escaped_string(boost::asio::ip::address address);
|
||||
|
||||
/**
|
||||
* @brief Returns the encryption mode for the given remote endpoint address.
|
||||
* @param address The address used to look up the desired encryption mode.
|
||||
* @return The WAN or LAN encryption mode, based on the provided address.
|
||||
*/
|
||||
int
|
||||
encryption_mode_for_address(boost::asio::ip::address address);
|
||||
} // namespace net
|
||||
1153
src/nvhttp.cpp
1153
src/nvhttp.cpp
File diff suppressed because it is too large
Load Diff
49
src/nvhttp.h
49
src/nvhttp.h
@ -1,49 +0,0 @@
|
||||
/**
|
||||
* @file src/nvhttp.h
|
||||
* @brief todo
|
||||
*/
|
||||
|
||||
// macros
|
||||
#pragma once
|
||||
|
||||
// standard includes
|
||||
#include <string>
|
||||
|
||||
// local includes
|
||||
#include "thread_safe.h"
|
||||
|
||||
/**
|
||||
* @brief This namespace contains all the functions and variables related to the nvhttp (GameStream) server.
|
||||
*/
|
||||
namespace nvhttp {
|
||||
|
||||
/**
|
||||
* @brief The protocol version.
|
||||
* @details The version of the GameStream protocol we are mocking.
|
||||
* @note The negative 4th number indicates to Moonlight that this is Sunshine.
|
||||
*/
|
||||
constexpr auto VERSION = "7.1.431.-1";
|
||||
|
||||
/**
|
||||
* @brief The GFE version we are replicating.
|
||||
*/
|
||||
constexpr auto GFE_VERSION = "3.23.0.74";
|
||||
|
||||
/**
|
||||
* @brief The HTTP port, as a difference from the config port.
|
||||
*/
|
||||
constexpr auto PORT_HTTP = 0;
|
||||
|
||||
/**
|
||||
* @brief The HTTPS port, as a difference from the config port.
|
||||
*/
|
||||
constexpr auto PORT_HTTPS = -5;
|
||||
|
||||
// functions
|
||||
void
|
||||
start();
|
||||
bool
|
||||
pin(std::string pin);
|
||||
void
|
||||
erase_all_clients();
|
||||
} // namespace nvhttp
|
||||
@ -27,7 +27,6 @@
|
||||
#include "graphics.h"
|
||||
#include "misc.h"
|
||||
#include "src/config.h"
|
||||
#include "src/entry_handler.h"
|
||||
#include "src/logging.h"
|
||||
#include "src/platform/common.h"
|
||||
#include "vaapi.h"
|
||||
@ -311,19 +310,6 @@ namespace platf {
|
||||
for (int fd = STDERR_FILENO + 1; fd < openmax; fd++) {
|
||||
close(fd);
|
||||
}
|
||||
|
||||
// Re-exec ourselves with the same arguments
|
||||
if (execv(executable, lifetime::get_argv()) < 0) {
|
||||
BOOST_LOG(fatal) << "execv() failed: "sv << errno;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
restart() {
|
||||
// Gracefully clean up and restart ourselves instead of exiting
|
||||
atexit(restart_on_exit);
|
||||
lifetime::exit_sunshine(0, true);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -1,446 +0,0 @@
|
||||
/**
|
||||
* @file src/platform/linux/publish.cpp
|
||||
* @brief todo
|
||||
* @note Adapted from https://www.avahi.org/doxygen/html/client-publish-service_8c-example.html
|
||||
* @todo Use a common file for this and src/platform/macos/publish.cpp
|
||||
*/
|
||||
#include <thread>
|
||||
|
||||
#include "misc.h"
|
||||
#include "src/logging.h"
|
||||
#include "src/network.h"
|
||||
#include "src/nvhttp.h"
|
||||
#include "src/platform/common.h"
|
||||
#include "src/utility.h"
|
||||
|
||||
using namespace std::literals;
|
||||
|
||||
namespace avahi {
|
||||
|
||||
/**
|
||||
* @brief Error codes used by avahi.
|
||||
*/
|
||||
enum err_e {
|
||||
OK = 0, /**< OK */
|
||||
ERR_FAILURE = -1, /**< Generic error code */
|
||||
ERR_BAD_STATE = -2, /**< Object was in a bad state */
|
||||
ERR_INVALID_HOST_NAME = -3, /**< Invalid host name */
|
||||
ERR_INVALID_DOMAIN_NAME = -4, /**< Invalid domain name */
|
||||
ERR_NO_NETWORK = -5, /**< No suitable network protocol available */
|
||||
ERR_INVALID_TTL = -6, /**< Invalid DNS TTL */
|
||||
ERR_IS_PATTERN = -7, /**< RR key is pattern */
|
||||
ERR_COLLISION = -8, /**< Name collision */
|
||||
ERR_INVALID_RECORD = -9, /**< Invalid RR */
|
||||
|
||||
ERR_INVALID_SERVICE_NAME = -10, /**< Invalid service name */
|
||||
ERR_INVALID_SERVICE_TYPE = -11, /**< Invalid service type */
|
||||
ERR_INVALID_PORT = -12, /**< Invalid port number */
|
||||
ERR_INVALID_KEY = -13, /**< Invalid key */
|
||||
ERR_INVALID_ADDRESS = -14, /**< Invalid address */
|
||||
ERR_TIMEOUT = -15, /**< Timeout reached */
|
||||
ERR_TOO_MANY_CLIENTS = -16, /**< Too many clients */
|
||||
ERR_TOO_MANY_OBJECTS = -17, /**< Too many objects */
|
||||
ERR_TOO_MANY_ENTRIES = -18, /**< Too many entries */
|
||||
ERR_OS = -19, /**< OS error */
|
||||
|
||||
ERR_ACCESS_DENIED = -20, /**< Access denied */
|
||||
ERR_INVALID_OPERATION = -21, /**< Invalid operation */
|
||||
ERR_DBUS_ERROR = -22, /**< An unexpected D-Bus error occurred */
|
||||
ERR_DISCONNECTED = -23, /**< Daemon connection failed */
|
||||
ERR_NO_MEMORY = -24, /**< Memory exhausted */
|
||||
ERR_INVALID_OBJECT = -25, /**< The object passed to this function was invalid */
|
||||
ERR_NO_DAEMON = -26, /**< Daemon not running */
|
||||
ERR_INVALID_INTERFACE = -27, /**< Invalid interface */
|
||||
ERR_INVALID_PROTOCOL = -28, /**< Invalid protocol */
|
||||
ERR_INVALID_FLAGS = -29, /**< Invalid flags */
|
||||
|
||||
ERR_NOT_FOUND = -30, /**< Not found */
|
||||
ERR_INVALID_CONFIG = -31, /**< Configuration error */
|
||||
ERR_VERSION_MISMATCH = -32, /**< Version mismatch */
|
||||
ERR_INVALID_SERVICE_SUBTYPE = -33, /**< Invalid service subtype */
|
||||
ERR_INVALID_PACKET = -34, /**< Invalid packet */
|
||||
ERR_INVALID_DNS_ERROR = -35, /**< Invalid DNS return code */
|
||||
ERR_DNS_FORMERR = -36, /**< DNS Error: Form error */
|
||||
ERR_DNS_SERVFAIL = -37, /**< DNS Error: Server Failure */
|
||||
ERR_DNS_NXDOMAIN = -38, /**< DNS Error: No such domain */
|
||||
ERR_DNS_NOTIMP = -39, /**< DNS Error: Not implemented */
|
||||
|
||||
ERR_DNS_REFUSED = -40, /**< DNS Error: Operation refused */
|
||||
ERR_DNS_YXDOMAIN = -41,
|
||||
ERR_DNS_YXRRSET = -42,
|
||||
ERR_DNS_NXRRSET = -43,
|
||||
ERR_DNS_NOTAUTH = -44, /**< DNS Error: Not authorized */
|
||||
ERR_DNS_NOTZONE = -45,
|
||||
ERR_INVALID_RDATA = -46, /**< Invalid RDATA */
|
||||
ERR_INVALID_DNS_CLASS = -47, /**< Invalid DNS class */
|
||||
ERR_INVALID_DNS_TYPE = -48, /**< Invalid DNS type */
|
||||
ERR_NOT_SUPPORTED = -49, /**< Not supported */
|
||||
|
||||
ERR_NOT_PERMITTED = -50, /**< Operation not permitted */
|
||||
ERR_INVALID_ARGUMENT = -51, /**< Invalid argument */
|
||||
ERR_IS_EMPTY = -52, /**< Is empty */
|
||||
ERR_NO_CHANGE = -53, /**< The requested operation is invalid because it is redundant */
|
||||
|
||||
ERR_MAX = -54
|
||||
};
|
||||
|
||||
constexpr auto IF_UNSPEC = -1;
|
||||
enum proto {
|
||||
PROTO_INET = 0, /**< IPv4 */
|
||||
PROTO_INET6 = 1, /**< IPv6 */
|
||||
PROTO_UNSPEC = -1 /**< Unspecified/all protocol(s) */
|
||||
};
|
||||
|
||||
enum ServerState {
|
||||
SERVER_INVALID, /**< Invalid state (initial) */
|
||||
SERVER_REGISTERING, /**< Host RRs are being registered */
|
||||
SERVER_RUNNING, /**< All host RRs have been established */
|
||||
SERVER_COLLISION, /**< There is a collision with a host RR. All host RRs have been withdrawn, the user should set a new host name via avahi_server_set_host_name() */
|
||||
SERVER_FAILURE /**< Some fatal failure happened, the server is unable to proceed */
|
||||
};
|
||||
|
||||
enum ClientState {
|
||||
CLIENT_S_REGISTERING = SERVER_REGISTERING, /**< Server state: REGISTERING */
|
||||
CLIENT_S_RUNNING = SERVER_RUNNING, /**< Server state: RUNNING */
|
||||
CLIENT_S_COLLISION = SERVER_COLLISION, /**< Server state: COLLISION */
|
||||
CLIENT_FAILURE = 100, /**< Some kind of error happened on the client side */
|
||||
CLIENT_CONNECTING = 101 /**< We're still connecting. This state is only entered when AVAHI_CLIENT_NO_FAIL has been passed to avahi_client_new() and the daemon is not yet available. */
|
||||
};
|
||||
|
||||
enum EntryGroupState {
|
||||
ENTRY_GROUP_UNCOMMITED, /**< The group has not yet been committed, the user must still call avahi_entry_group_commit() */
|
||||
ENTRY_GROUP_REGISTERING, /**< The entries of the group are currently being registered */
|
||||
ENTRY_GROUP_ESTABLISHED, /**< The entries have successfully been established */
|
||||
ENTRY_GROUP_COLLISION, /**< A name collision for one of the entries in the group has been detected, the entries have been withdrawn */
|
||||
ENTRY_GROUP_FAILURE /**< Some kind of failure happened, the entries have been withdrawn */
|
||||
};
|
||||
|
||||
enum ClientFlags {
|
||||
CLIENT_IGNORE_USER_CONFIG = 1, /**< Don't read user configuration */
|
||||
CLIENT_NO_FAIL = 2 /**< Don't fail if the daemon is not available when avahi_client_new() is called, instead enter CLIENT_CONNECTING state and wait for the daemon to appear */
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Flags for publishing functions.
|
||||
*/
|
||||
enum PublishFlags {
|
||||
PUBLISH_UNIQUE = 1, /**< For raw records: The RRset is intended to be unique */
|
||||
PUBLISH_NO_PROBE = 2, /**< For raw records: Though the RRset is intended to be unique no probes shall be sent */
|
||||
PUBLISH_NO_ANNOUNCE = 4, /**< For raw records: Do not announce this RR to other hosts */
|
||||
PUBLISH_ALLOW_MULTIPLE = 8, /**< For raw records: Allow multiple local records of this type, even if they are intended to be unique */
|
||||
/** \cond fulldocs */
|
||||
PUBLISH_NO_REVERSE = 16, /**< For address records: don't create a reverse (PTR) entry */
|
||||
PUBLISH_NO_COOKIE = 32, /**< For service records: do not implicitly add the local service cookie to TXT data */
|
||||
/** \endcond */
|
||||
PUBLISH_UPDATE = 64, /**< Update existing records instead of adding new ones */
|
||||
/** \cond fulldocs */
|
||||
PUBLISH_USE_WIDE_AREA = 128, /**< Register the record using wide area DNS (i.e. unicast DNS update) */
|
||||
PUBLISH_USE_MULTICAST = 256 /**< Register the record using multicast DNS */
|
||||
/** \endcond */
|
||||
};
|
||||
|
||||
using IfIndex = int;
|
||||
using Protocol = int;
|
||||
|
||||
struct EntryGroup;
|
||||
struct Poll;
|
||||
struct SimplePoll;
|
||||
struct Client;
|
||||
|
||||
typedef void (*ClientCallback)(Client *, ClientState, void *userdata);
|
||||
typedef void (*EntryGroupCallback)(EntryGroup *g, EntryGroupState state, void *userdata);
|
||||
|
||||
typedef void (*free_fn)(void *);
|
||||
|
||||
typedef Client *(*client_new_fn)(const Poll *poll_api, ClientFlags flags, ClientCallback callback, void *userdata, int *error);
|
||||
typedef void (*client_free_fn)(Client *);
|
||||
typedef char *(*alternative_service_name_fn)(char *);
|
||||
|
||||
typedef Client *(*entry_group_get_client_fn)(EntryGroup *);
|
||||
|
||||
typedef EntryGroup *(*entry_group_new_fn)(Client *, EntryGroupCallback, void *userdata);
|
||||
typedef int (*entry_group_add_service_fn)(
|
||||
EntryGroup *group,
|
||||
IfIndex interface,
|
||||
Protocol protocol,
|
||||
PublishFlags flags,
|
||||
const char *name,
|
||||
const char *type,
|
||||
const char *domain,
|
||||
const char *host,
|
||||
uint16_t port,
|
||||
...);
|
||||
|
||||
typedef int (*entry_group_is_empty_fn)(EntryGroup *);
|
||||
typedef int (*entry_group_reset_fn)(EntryGroup *);
|
||||
typedef int (*entry_group_commit_fn)(EntryGroup *);
|
||||
|
||||
typedef char *(*strdup_fn)(const char *);
|
||||
typedef char *(*strerror_fn)(int);
|
||||
typedef int (*client_errno_fn)(Client *);
|
||||
|
||||
typedef Poll *(*simple_poll_get_fn)(SimplePoll *);
|
||||
typedef int (*simple_poll_loop_fn)(SimplePoll *);
|
||||
typedef void (*simple_poll_quit_fn)(SimplePoll *);
|
||||
typedef SimplePoll *(*simple_poll_new_fn)();
|
||||
typedef void (*simple_poll_free_fn)(SimplePoll *);
|
||||
|
||||
free_fn free;
|
||||
client_new_fn client_new;
|
||||
client_free_fn client_free;
|
||||
alternative_service_name_fn alternative_service_name;
|
||||
entry_group_get_client_fn entry_group_get_client;
|
||||
entry_group_new_fn entry_group_new;
|
||||
entry_group_add_service_fn entry_group_add_service;
|
||||
entry_group_is_empty_fn entry_group_is_empty;
|
||||
entry_group_reset_fn entry_group_reset;
|
||||
entry_group_commit_fn entry_group_commit;
|
||||
strdup_fn strdup;
|
||||
strerror_fn strerror;
|
||||
client_errno_fn client_errno;
|
||||
simple_poll_get_fn simple_poll_get;
|
||||
simple_poll_loop_fn simple_poll_loop;
|
||||
simple_poll_quit_fn simple_poll_quit;
|
||||
simple_poll_new_fn simple_poll_new;
|
||||
simple_poll_free_fn simple_poll_free;
|
||||
|
||||
int
|
||||
init_common() {
|
||||
static void *handle { nullptr };
|
||||
static bool funcs_loaded = false;
|
||||
|
||||
if (funcs_loaded) return 0;
|
||||
|
||||
if (!handle) {
|
||||
handle = dyn::handle({ "libavahi-common.so.3", "libavahi-common.so" });
|
||||
if (!handle) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<std::tuple<dyn::apiproc *, const char *>> funcs {
|
||||
{ (dyn::apiproc *) &alternative_service_name, "avahi_alternative_service_name" },
|
||||
{ (dyn::apiproc *) &free, "avahi_free" },
|
||||
{ (dyn::apiproc *) &strdup, "avahi_strdup" },
|
||||
{ (dyn::apiproc *) &strerror, "avahi_strerror" },
|
||||
{ (dyn::apiproc *) &simple_poll_get, "avahi_simple_poll_get" },
|
||||
{ (dyn::apiproc *) &simple_poll_loop, "avahi_simple_poll_loop" },
|
||||
{ (dyn::apiproc *) &simple_poll_quit, "avahi_simple_poll_quit" },
|
||||
{ (dyn::apiproc *) &simple_poll_new, "avahi_simple_poll_new" },
|
||||
{ (dyn::apiproc *) &simple_poll_free, "avahi_simple_poll_free" },
|
||||
};
|
||||
|
||||
if (dyn::load(handle, funcs)) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
funcs_loaded = true;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int
|
||||
init_client() {
|
||||
if (init_common()) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
static void *handle { nullptr };
|
||||
static bool funcs_loaded = false;
|
||||
|
||||
if (funcs_loaded) return 0;
|
||||
|
||||
if (!handle) {
|
||||
handle = dyn::handle({ "libavahi-client.so.3", "libavahi-client.so" });
|
||||
if (!handle) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<std::tuple<dyn::apiproc *, const char *>> funcs {
|
||||
{ (dyn::apiproc *) &client_new, "avahi_client_new" },
|
||||
{ (dyn::apiproc *) &client_free, "avahi_client_free" },
|
||||
{ (dyn::apiproc *) &entry_group_get_client, "avahi_entry_group_get_client" },
|
||||
{ (dyn::apiproc *) &entry_group_new, "avahi_entry_group_new" },
|
||||
{ (dyn::apiproc *) &entry_group_add_service, "avahi_entry_group_add_service" },
|
||||
{ (dyn::apiproc *) &entry_group_is_empty, "avahi_entry_group_is_empty" },
|
||||
{ (dyn::apiproc *) &entry_group_reset, "avahi_entry_group_reset" },
|
||||
{ (dyn::apiproc *) &entry_group_commit, "avahi_entry_group_commit" },
|
||||
{ (dyn::apiproc *) &client_errno, "avahi_client_errno" },
|
||||
};
|
||||
|
||||
if (dyn::load(handle, funcs)) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
funcs_loaded = true;
|
||||
return 0;
|
||||
}
|
||||
} // namespace avahi
|
||||
|
||||
namespace platf::publish {
|
||||
|
||||
template <class T>
|
||||
void
|
||||
free(T *p) {
|
||||
avahi::free(p);
|
||||
}
|
||||
|
||||
template <class T>
|
||||
using ptr_t = util::safe_ptr<T, free<T>>;
|
||||
using client_t = util::dyn_safe_ptr<avahi::Client, &avahi::client_free>;
|
||||
using poll_t = util::dyn_safe_ptr<avahi::SimplePoll, &avahi::simple_poll_free>;
|
||||
|
||||
avahi::EntryGroup *group = nullptr;
|
||||
|
||||
poll_t poll;
|
||||
client_t client;
|
||||
|
||||
ptr_t<char> name;
|
||||
|
||||
void
|
||||
create_services(avahi::Client *c);
|
||||
|
||||
void
|
||||
entry_group_callback(avahi::EntryGroup *g, avahi::EntryGroupState state, void *) {
|
||||
group = g;
|
||||
|
||||
switch (state) {
|
||||
case avahi::ENTRY_GROUP_ESTABLISHED:
|
||||
BOOST_LOG(info) << "Avahi service " << name.get() << " successfully established.";
|
||||
break;
|
||||
case avahi::ENTRY_GROUP_COLLISION:
|
||||
name.reset(avahi::alternative_service_name(name.get()));
|
||||
|
||||
BOOST_LOG(info) << "Avahi service name collision, renaming service to " << name.get();
|
||||
|
||||
create_services(avahi::entry_group_get_client(g));
|
||||
break;
|
||||
case avahi::ENTRY_GROUP_FAILURE:
|
||||
BOOST_LOG(error) << "Avahi entry group failure: " << avahi::strerror(avahi::client_errno(avahi::entry_group_get_client(g)));
|
||||
avahi::simple_poll_quit(poll.get());
|
||||
break;
|
||||
case avahi::ENTRY_GROUP_UNCOMMITED:
|
||||
case avahi::ENTRY_GROUP_REGISTERING:;
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
create_services(avahi::Client *c) {
|
||||
int ret;
|
||||
|
||||
auto fg = util::fail_guard([]() {
|
||||
avahi::simple_poll_quit(poll.get());
|
||||
});
|
||||
|
||||
if (!group) {
|
||||
if (!(group = avahi::entry_group_new(c, entry_group_callback, nullptr))) {
|
||||
BOOST_LOG(error) << "avahi::entry_group_new() failed: "sv << avahi::strerror(avahi::client_errno(c));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (avahi::entry_group_is_empty(group)) {
|
||||
BOOST_LOG(info) << "Adding avahi service "sv << name.get();
|
||||
|
||||
ret = avahi::entry_group_add_service(
|
||||
group,
|
||||
avahi::IF_UNSPEC, avahi::PROTO_UNSPEC,
|
||||
avahi::PublishFlags(0),
|
||||
name.get(),
|
||||
SERVICE_TYPE,
|
||||
nullptr, nullptr,
|
||||
net::map_port(nvhttp::PORT_HTTP),
|
||||
nullptr);
|
||||
|
||||
if (ret < 0) {
|
||||
if (ret == avahi::ERR_COLLISION) {
|
||||
// A service name collision with a local service happened. Let's pick a new name
|
||||
name.reset(avahi::alternative_service_name(name.get()));
|
||||
BOOST_LOG(info) << "Service name collision, renaming service to "sv << name.get();
|
||||
|
||||
avahi::entry_group_reset(group);
|
||||
|
||||
create_services(c);
|
||||
|
||||
fg.disable();
|
||||
return;
|
||||
}
|
||||
|
||||
BOOST_LOG(error) << "Failed to add "sv << SERVICE_TYPE << " service: "sv << avahi::strerror(ret);
|
||||
return;
|
||||
}
|
||||
|
||||
ret = avahi::entry_group_commit(group);
|
||||
if (ret < 0) {
|
||||
BOOST_LOG(error) << "Failed to commit entry group: "sv << avahi::strerror(ret);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
fg.disable();
|
||||
}
|
||||
|
||||
void
|
||||
client_callback(avahi::Client *c, avahi::ClientState state, void *) {
|
||||
switch (state) {
|
||||
case avahi::CLIENT_S_RUNNING:
|
||||
create_services(c);
|
||||
break;
|
||||
case avahi::CLIENT_FAILURE:
|
||||
BOOST_LOG(error) << "Client failure: "sv << avahi::strerror(avahi::client_errno(c));
|
||||
avahi::simple_poll_quit(poll.get());
|
||||
break;
|
||||
case avahi::CLIENT_S_COLLISION:
|
||||
case avahi::CLIENT_S_REGISTERING:
|
||||
if (group)
|
||||
avahi::entry_group_reset(group);
|
||||
break;
|
||||
case avahi::CLIENT_CONNECTING:;
|
||||
}
|
||||
}
|
||||
|
||||
class deinit_t: public ::platf::deinit_t {
|
||||
public:
|
||||
std::thread poll_thread;
|
||||
|
||||
deinit_t(std::thread poll_thread):
|
||||
poll_thread { std::move(poll_thread) } {}
|
||||
|
||||
~deinit_t() override {
|
||||
if (avahi::simple_poll_quit && poll) {
|
||||
avahi::simple_poll_quit(poll.get());
|
||||
}
|
||||
|
||||
if (poll_thread.joinable()) {
|
||||
poll_thread.join();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
[[nodiscard]] std::unique_ptr<::platf::deinit_t>
|
||||
start() {
|
||||
if (avahi::init_client()) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
int avhi_error;
|
||||
|
||||
poll.reset(avahi::simple_poll_new());
|
||||
if (!poll) {
|
||||
BOOST_LOG(error) << "Failed to create simple poll object."sv;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
name.reset(avahi::strdup(SERVICE_NAME));
|
||||
|
||||
client.reset(
|
||||
avahi::client_new(avahi::simple_poll_get(poll.get()), avahi::ClientFlags(0), client_callback, nullptr, &avhi_error));
|
||||
|
||||
if (!client) {
|
||||
BOOST_LOG(error) << "Failed to create client: "sv << avahi::strerror(avhi_error);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
return std::make_unique<deinit_t>(std::thread { avahi::simple_poll_loop, poll.get() });
|
||||
}
|
||||
} // namespace platf::publish
|
||||
@ -18,7 +18,6 @@
|
||||
#include <pwd.h>
|
||||
|
||||
#include "misc.h"
|
||||
#include "src/entry_handler.h"
|
||||
#include "src/logging.h"
|
||||
#include "src/platform/common.h"
|
||||
|
||||
@ -238,19 +237,10 @@ namespace platf {
|
||||
for (int fd = STDERR_FILENO + 1; fd < openmax; fd++) {
|
||||
close(fd);
|
||||
}
|
||||
|
||||
// Re-exec ourselves with the same arguments
|
||||
if (execv(executable, lifetime::get_argv()) < 0) {
|
||||
BOOST_LOG(fatal) << "execv() failed: "sv << errno;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
restart() {
|
||||
// Gracefully clean up and restart ourselves instead of exiting
|
||||
atexit(restart_on_exit);
|
||||
lifetime::exit_sunshine(0, true);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -1,446 +0,0 @@
|
||||
/**
|
||||
* @file src/platform/macos/publish.cpp
|
||||
* @brief todo
|
||||
* @note Adapted from https://www.avahi.org/doxygen/html/client-publish-service_8c-example.html
|
||||
* @todo Use a common file for this and src/platform/linux/publish.cpp
|
||||
*/
|
||||
#include <thread>
|
||||
|
||||
#include "misc.h"
|
||||
#include "src/logging.h"
|
||||
#include "src/network.h"
|
||||
#include "src/nvhttp.h"
|
||||
#include "src/platform/common.h"
|
||||
#include "src/utility.h"
|
||||
|
||||
using namespace std::literals;
|
||||
|
||||
namespace avahi {
|
||||
|
||||
/**
|
||||
* @brief Error codes used by avahi.
|
||||
*/
|
||||
enum err_e {
|
||||
OK = 0, /**< OK */
|
||||
ERR_FAILURE = -1, /**< Generic error code */
|
||||
ERR_BAD_STATE = -2, /**< Object was in a bad state */
|
||||
ERR_INVALID_HOST_NAME = -3, /**< Invalid host name */
|
||||
ERR_INVALID_DOMAIN_NAME = -4, /**< Invalid domain name */
|
||||
ERR_NO_NETWORK = -5, /**< No suitable network protocol available */
|
||||
ERR_INVALID_TTL = -6, /**< Invalid DNS TTL */
|
||||
ERR_IS_PATTERN = -7, /**< RR key is pattern */
|
||||
ERR_COLLISION = -8, /**< Name collision */
|
||||
ERR_INVALID_RECORD = -9, /**< Invalid RR */
|
||||
|
||||
ERR_INVALID_SERVICE_NAME = -10, /**< Invalid service name */
|
||||
ERR_INVALID_SERVICE_TYPE = -11, /**< Invalid service type */
|
||||
ERR_INVALID_PORT = -12, /**< Invalid port number */
|
||||
ERR_INVALID_KEY = -13, /**< Invalid key */
|
||||
ERR_INVALID_ADDRESS = -14, /**< Invalid address */
|
||||
ERR_TIMEOUT = -15, /**< Timeout reached */
|
||||
ERR_TOO_MANY_CLIENTS = -16, /**< Too many clients */
|
||||
ERR_TOO_MANY_OBJECTS = -17, /**< Too many objects */
|
||||
ERR_TOO_MANY_ENTRIES = -18, /**< Too many entries */
|
||||
ERR_OS = -19, /**< OS error */
|
||||
|
||||
ERR_ACCESS_DENIED = -20, /**< Access denied */
|
||||
ERR_INVALID_OPERATION = -21, /**< Invalid operation */
|
||||
ERR_DBUS_ERROR = -22, /**< An unexpected D-Bus error occurred */
|
||||
ERR_DISCONNECTED = -23, /**< Daemon connection failed */
|
||||
ERR_NO_MEMORY = -24, /**< Memory exhausted */
|
||||
ERR_INVALID_OBJECT = -25, /**< The object passed to this function was invalid */
|
||||
ERR_NO_DAEMON = -26, /**< Daemon not running */
|
||||
ERR_INVALID_INTERFACE = -27, /**< Invalid interface */
|
||||
ERR_INVALID_PROTOCOL = -28, /**< Invalid protocol */
|
||||
ERR_INVALID_FLAGS = -29, /**< Invalid flags */
|
||||
|
||||
ERR_NOT_FOUND = -30, /**< Not found */
|
||||
ERR_INVALID_CONFIG = -31, /**< Configuration error */
|
||||
ERR_VERSION_MISMATCH = -32, /**< Version mismatch */
|
||||
ERR_INVALID_SERVICE_SUBTYPE = -33, /**< Invalid service subtype */
|
||||
ERR_INVALID_PACKET = -34, /**< Invalid packet */
|
||||
ERR_INVALID_DNS_ERROR = -35, /**< Invalid DNS return code */
|
||||
ERR_DNS_FORMERR = -36, /**< DNS Error: Form error */
|
||||
ERR_DNS_SERVFAIL = -37, /**< DNS Error: Server Failure */
|
||||
ERR_DNS_NXDOMAIN = -38, /**< DNS Error: No such domain */
|
||||
ERR_DNS_NOTIMP = -39, /**< DNS Error: Not implemented */
|
||||
|
||||
ERR_DNS_REFUSED = -40, /**< DNS Error: Operation refused */
|
||||
ERR_DNS_YXDOMAIN = -41,
|
||||
ERR_DNS_YXRRSET = -42,
|
||||
ERR_DNS_NXRRSET = -43,
|
||||
ERR_DNS_NOTAUTH = -44, /**< DNS Error: Not authorized */
|
||||
ERR_DNS_NOTZONE = -45,
|
||||
ERR_INVALID_RDATA = -46, /**< Invalid RDATA */
|
||||
ERR_INVALID_DNS_CLASS = -47, /**< Invalid DNS class */
|
||||
ERR_INVALID_DNS_TYPE = -48, /**< Invalid DNS type */
|
||||
ERR_NOT_SUPPORTED = -49, /**< Not supported */
|
||||
|
||||
ERR_NOT_PERMITTED = -50, /**< Operation not permitted */
|
||||
ERR_INVALID_ARGUMENT = -51, /**< Invalid argument */
|
||||
ERR_IS_EMPTY = -52, /**< Is empty */
|
||||
ERR_NO_CHANGE = -53, /**< The requested operation is invalid because it is redundant */
|
||||
|
||||
ERR_MAX = -54
|
||||
};
|
||||
|
||||
constexpr auto IF_UNSPEC = -1;
|
||||
enum proto {
|
||||
PROTO_INET = 0, /**< IPv4 */
|
||||
PROTO_INET6 = 1, /**< IPv6 */
|
||||
PROTO_UNSPEC = -1 /**< Unspecified/all protocol(s) */
|
||||
};
|
||||
|
||||
enum ServerState {
|
||||
SERVER_INVALID, /**< Invalid state (initial) */
|
||||
SERVER_REGISTERING, /**< Host RRs are being registered */
|
||||
SERVER_RUNNING, /**< All host RRs have been established */
|
||||
SERVER_COLLISION, /**< There is a collision with a host RR. All host RRs have been withdrawn, the user should set a new host name via avahi_server_set_host_name() */
|
||||
SERVER_FAILURE /**< Some fatal failure happened, the server is unable to proceed */
|
||||
};
|
||||
|
||||
enum ClientState {
|
||||
CLIENT_S_REGISTERING = SERVER_REGISTERING, /**< Server state: REGISTERING */
|
||||
CLIENT_S_RUNNING = SERVER_RUNNING, /**< Server state: RUNNING */
|
||||
CLIENT_S_COLLISION = SERVER_COLLISION, /**< Server state: COLLISION */
|
||||
CLIENT_FAILURE = 100, /**< Some kind of error happened on the client side */
|
||||
CLIENT_CONNECTING = 101 /**< We're still connecting. This state is only entered when AVAHI_CLIENT_NO_FAIL has been passed to avahi_client_new() and the daemon is not yet available. */
|
||||
};
|
||||
|
||||
enum EntryGroupState {
|
||||
ENTRY_GROUP_UNCOMMITED, /**< The group has not yet been committed, the user must still call avahi_entry_group_commit() */
|
||||
ENTRY_GROUP_REGISTERING, /**< The entries of the group are currently being registered */
|
||||
ENTRY_GROUP_ESTABLISHED, /**< The entries have successfully been established */
|
||||
ENTRY_GROUP_COLLISION, /**< A name collision for one of the entries in the group has been detected, the entries have been withdrawn */
|
||||
ENTRY_GROUP_FAILURE /**< Some kind of failure happened, the entries have been withdrawn */
|
||||
};
|
||||
|
||||
enum ClientFlags {
|
||||
CLIENT_IGNORE_USER_CONFIG = 1, /**< Don't read user configuration */
|
||||
CLIENT_NO_FAIL = 2 /**< Don't fail if the daemon is not available when avahi_client_new() is called, instead enter CLIENT_CONNECTING state and wait for the daemon to appear */
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Flags for publishing functions.
|
||||
*/
|
||||
enum PublishFlags {
|
||||
PUBLISH_UNIQUE = 1, /**< For raw records: The RRset is intended to be unique */
|
||||
PUBLISH_NO_PROBE = 2, /**< For raw records: Though the RRset is intended to be unique no probes shall be sent */
|
||||
PUBLISH_NO_ANNOUNCE = 4, /**< For raw records: Do not announce this RR to other hosts */
|
||||
PUBLISH_ALLOW_MULTIPLE = 8, /**< For raw records: Allow multiple local records of this type, even if they are intended to be unique */
|
||||
/** \cond fulldocs */
|
||||
PUBLISH_NO_REVERSE = 16, /**< For address records: don't create a reverse (PTR) entry */
|
||||
PUBLISH_NO_COOKIE = 32, /**< For service records: do not implicitly add the local service cookie to TXT data */
|
||||
/** \endcond */
|
||||
PUBLISH_UPDATE = 64, /**< Update existing records instead of adding new ones */
|
||||
/** \cond fulldocs */
|
||||
PUBLISH_USE_WIDE_AREA = 128, /**< Register the record using wide area DNS (i.e. unicast DNS update) */
|
||||
PUBLISH_USE_MULTICAST = 256 /**< Register the record using multicast DNS */
|
||||
/** \endcond */
|
||||
};
|
||||
|
||||
using IfIndex = int;
|
||||
using Protocol = int;
|
||||
|
||||
struct EntryGroup;
|
||||
struct Poll;
|
||||
struct SimplePoll;
|
||||
struct Client;
|
||||
|
||||
typedef void (*ClientCallback)(Client *, ClientState, void *userdata);
|
||||
typedef void (*EntryGroupCallback)(EntryGroup *g, EntryGroupState state, void *userdata);
|
||||
|
||||
typedef void (*free_fn)(void *);
|
||||
|
||||
typedef Client *(*client_new_fn)(const Poll *poll_api, ClientFlags flags, ClientCallback callback, void *userdata, int *error);
|
||||
typedef void (*client_free_fn)(Client *);
|
||||
typedef char *(*alternative_service_name_fn)(char *);
|
||||
|
||||
typedef Client *(*entry_group_get_client_fn)(EntryGroup *);
|
||||
|
||||
typedef EntryGroup *(*entry_group_new_fn)(Client *, EntryGroupCallback, void *userdata);
|
||||
typedef int (*entry_group_add_service_fn)(
|
||||
EntryGroup *group,
|
||||
IfIndex interface,
|
||||
Protocol protocol,
|
||||
PublishFlags flags,
|
||||
const char *name,
|
||||
const char *type,
|
||||
const char *domain,
|
||||
const char *host,
|
||||
uint16_t port,
|
||||
...);
|
||||
|
||||
typedef int (*entry_group_is_empty_fn)(EntryGroup *);
|
||||
typedef int (*entry_group_reset_fn)(EntryGroup *);
|
||||
typedef int (*entry_group_commit_fn)(EntryGroup *);
|
||||
|
||||
typedef char *(*strdup_fn)(const char *);
|
||||
typedef char *(*strerror_fn)(int);
|
||||
typedef int (*client_errno_fn)(Client *);
|
||||
|
||||
typedef Poll *(*simple_poll_get_fn)(SimplePoll *);
|
||||
typedef int (*simple_poll_loop_fn)(SimplePoll *);
|
||||
typedef void (*simple_poll_quit_fn)(SimplePoll *);
|
||||
typedef SimplePoll *(*simple_poll_new_fn)();
|
||||
typedef void (*simple_poll_free_fn)(SimplePoll *);
|
||||
|
||||
free_fn free;
|
||||
client_new_fn client_new;
|
||||
client_free_fn client_free;
|
||||
alternative_service_name_fn alternative_service_name;
|
||||
entry_group_get_client_fn entry_group_get_client;
|
||||
entry_group_new_fn entry_group_new;
|
||||
entry_group_add_service_fn entry_group_add_service;
|
||||
entry_group_is_empty_fn entry_group_is_empty;
|
||||
entry_group_reset_fn entry_group_reset;
|
||||
entry_group_commit_fn entry_group_commit;
|
||||
strdup_fn strdup;
|
||||
strerror_fn strerror;
|
||||
client_errno_fn client_errno;
|
||||
simple_poll_get_fn simple_poll_get;
|
||||
simple_poll_loop_fn simple_poll_loop;
|
||||
simple_poll_quit_fn simple_poll_quit;
|
||||
simple_poll_new_fn simple_poll_new;
|
||||
simple_poll_free_fn simple_poll_free;
|
||||
|
||||
int
|
||||
init_common() {
|
||||
static void *handle { nullptr };
|
||||
static bool funcs_loaded = false;
|
||||
|
||||
if (funcs_loaded) return 0;
|
||||
|
||||
if (!handle) {
|
||||
handle = dyn::handle({ "libavahi-common.3.dylib", "libavahi-common.dylib" });
|
||||
if (!handle) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<std::tuple<dyn::apiproc *, const char *>> funcs {
|
||||
{ (dyn::apiproc *) &alternative_service_name, "avahi_alternative_service_name" },
|
||||
{ (dyn::apiproc *) &free, "avahi_free" },
|
||||
{ (dyn::apiproc *) &strdup, "avahi_strdup" },
|
||||
{ (dyn::apiproc *) &strerror, "avahi_strerror" },
|
||||
{ (dyn::apiproc *) &simple_poll_get, "avahi_simple_poll_get" },
|
||||
{ (dyn::apiproc *) &simple_poll_loop, "avahi_simple_poll_loop" },
|
||||
{ (dyn::apiproc *) &simple_poll_quit, "avahi_simple_poll_quit" },
|
||||
{ (dyn::apiproc *) &simple_poll_new, "avahi_simple_poll_new" },
|
||||
{ (dyn::apiproc *) &simple_poll_free, "avahi_simple_poll_free" },
|
||||
};
|
||||
|
||||
if (dyn::load(handle, funcs)) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
funcs_loaded = true;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int
|
||||
init_client() {
|
||||
if (init_common()) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
static void *handle { nullptr };
|
||||
static bool funcs_loaded = false;
|
||||
|
||||
if (funcs_loaded) return 0;
|
||||
|
||||
if (!handle) {
|
||||
handle = dyn::handle({ "libavahi-client.3.dylib", "libavahi-client.dylib" });
|
||||
if (!handle) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<std::tuple<dyn::apiproc *, const char *>> funcs {
|
||||
{ (dyn::apiproc *) &client_new, "avahi_client_new" },
|
||||
{ (dyn::apiproc *) &client_free, "avahi_client_free" },
|
||||
{ (dyn::apiproc *) &entry_group_get_client, "avahi_entry_group_get_client" },
|
||||
{ (dyn::apiproc *) &entry_group_new, "avahi_entry_group_new" },
|
||||
{ (dyn::apiproc *) &entry_group_add_service, "avahi_entry_group_add_service" },
|
||||
{ (dyn::apiproc *) &entry_group_is_empty, "avahi_entry_group_is_empty" },
|
||||
{ (dyn::apiproc *) &entry_group_reset, "avahi_entry_group_reset" },
|
||||
{ (dyn::apiproc *) &entry_group_commit, "avahi_entry_group_commit" },
|
||||
{ (dyn::apiproc *) &client_errno, "avahi_client_errno" },
|
||||
};
|
||||
|
||||
if (dyn::load(handle, funcs)) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
funcs_loaded = true;
|
||||
return 0;
|
||||
}
|
||||
} // namespace avahi
|
||||
|
||||
namespace platf::publish {
|
||||
|
||||
template <class T>
|
||||
void
|
||||
free(T *p) {
|
||||
avahi::free(p);
|
||||
}
|
||||
|
||||
template <class T>
|
||||
using ptr_t = util::safe_ptr<T, free<T>>;
|
||||
using client_t = util::dyn_safe_ptr<avahi::Client, &avahi::client_free>;
|
||||
using poll_t = util::dyn_safe_ptr<avahi::SimplePoll, &avahi::simple_poll_free>;
|
||||
|
||||
avahi::EntryGroup *group = nullptr;
|
||||
|
||||
poll_t poll;
|
||||
client_t client;
|
||||
|
||||
ptr_t<char> name;
|
||||
|
||||
void
|
||||
create_services(avahi::Client *c);
|
||||
|
||||
void
|
||||
entry_group_callback(avahi::EntryGroup *g, avahi::EntryGroupState state, void *) {
|
||||
group = g;
|
||||
|
||||
switch (state) {
|
||||
case avahi::ENTRY_GROUP_ESTABLISHED:
|
||||
BOOST_LOG(info) << "Avahi service " << name.get() << " successfully established.";
|
||||
break;
|
||||
case avahi::ENTRY_GROUP_COLLISION:
|
||||
name.reset(avahi::alternative_service_name(name.get()));
|
||||
|
||||
BOOST_LOG(info) << "Avahi service name collision, renaming service to " << name.get();
|
||||
|
||||
create_services(avahi::entry_group_get_client(g));
|
||||
break;
|
||||
case avahi::ENTRY_GROUP_FAILURE:
|
||||
BOOST_LOG(error) << "Avahi entry group failure: " << avahi::strerror(avahi::client_errno(avahi::entry_group_get_client(g)));
|
||||
avahi::simple_poll_quit(poll.get());
|
||||
break;
|
||||
case avahi::ENTRY_GROUP_UNCOMMITED:
|
||||
case avahi::ENTRY_GROUP_REGISTERING:;
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
create_services(avahi::Client *c) {
|
||||
int ret;
|
||||
|
||||
auto fg = util::fail_guard([]() {
|
||||
avahi::simple_poll_quit(poll.get());
|
||||
});
|
||||
|
||||
if (!group) {
|
||||
if (!(group = avahi::entry_group_new(c, entry_group_callback, nullptr))) {
|
||||
BOOST_LOG(error) << "avahi::entry_group_new() failed: "sv << avahi::strerror(avahi::client_errno(c));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (avahi::entry_group_is_empty(group)) {
|
||||
BOOST_LOG(info) << "Adding avahi service "sv << name.get();
|
||||
|
||||
ret = avahi::entry_group_add_service(
|
||||
group,
|
||||
avahi::IF_UNSPEC, avahi::PROTO_UNSPEC,
|
||||
avahi::PublishFlags(0),
|
||||
name.get(),
|
||||
SERVICE_TYPE,
|
||||
nullptr, nullptr,
|
||||
net::map_port(nvhttp::PORT_HTTP),
|
||||
nullptr);
|
||||
|
||||
if (ret < 0) {
|
||||
if (ret == avahi::ERR_COLLISION) {
|
||||
// A service name collision with a local service happened. Let's pick a new name
|
||||
name.reset(avahi::alternative_service_name(name.get()));
|
||||
BOOST_LOG(info) << "Service name collision, renaming service to "sv << name.get();
|
||||
|
||||
avahi::entry_group_reset(group);
|
||||
|
||||
create_services(c);
|
||||
|
||||
fg.disable();
|
||||
return;
|
||||
}
|
||||
|
||||
BOOST_LOG(error) << "Failed to add "sv << SERVICE_TYPE << " service: "sv << avahi::strerror(ret);
|
||||
return;
|
||||
}
|
||||
|
||||
ret = avahi::entry_group_commit(group);
|
||||
if (ret < 0) {
|
||||
BOOST_LOG(error) << "Failed to commit entry group: "sv << avahi::strerror(ret);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
fg.disable();
|
||||
}
|
||||
|
||||
void
|
||||
client_callback(avahi::Client *c, avahi::ClientState state, void *) {
|
||||
switch (state) {
|
||||
case avahi::CLIENT_S_RUNNING:
|
||||
create_services(c);
|
||||
break;
|
||||
case avahi::CLIENT_FAILURE:
|
||||
BOOST_LOG(error) << "Client failure: "sv << avahi::strerror(avahi::client_errno(c));
|
||||
avahi::simple_poll_quit(poll.get());
|
||||
break;
|
||||
case avahi::CLIENT_S_COLLISION:
|
||||
case avahi::CLIENT_S_REGISTERING:
|
||||
if (group)
|
||||
avahi::entry_group_reset(group);
|
||||
break;
|
||||
case avahi::CLIENT_CONNECTING:;
|
||||
}
|
||||
}
|
||||
|
||||
class deinit_t: public ::platf::deinit_t {
|
||||
public:
|
||||
std::thread poll_thread;
|
||||
|
||||
explicit deinit_t(std::thread poll_thread):
|
||||
poll_thread { std::move(poll_thread) } {}
|
||||
|
||||
~deinit_t() override {
|
||||
if (avahi::simple_poll_quit && poll) {
|
||||
avahi::simple_poll_quit(poll.get());
|
||||
}
|
||||
|
||||
if (poll_thread.joinable()) {
|
||||
poll_thread.join();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
[[nodiscard]] std::unique_ptr<::platf::deinit_t>
|
||||
start() {
|
||||
if (avahi::init_client()) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
int avhi_error;
|
||||
|
||||
poll.reset(avahi::simple_poll_new());
|
||||
if (!poll) {
|
||||
BOOST_LOG(error) << "Failed to create simple poll object."sv;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
name.reset(avahi::strdup(SERVICE_NAME));
|
||||
|
||||
client.reset(
|
||||
avahi::client_new(avahi::simple_poll_get(poll.get()), avahi::ClientFlags(0), client_callback, nullptr, &avhi_error));
|
||||
|
||||
if (!client) {
|
||||
BOOST_LOG(error) << "Failed to create client: "sv << avahi::strerror(avhi_error);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
return std::make_unique<deinit_t>(std::thread { avahi::simple_poll_loop, poll.get() });
|
||||
}
|
||||
} // namespace platf::publish
|
||||
@ -36,7 +36,6 @@
|
||||
|
||||
#include "misc.h"
|
||||
|
||||
#include "src/entry_handler.h"
|
||||
#include "src/globals.h"
|
||||
#include "src/logging.h"
|
||||
#include "src/platform/common.h"
|
||||
@ -1255,15 +1254,6 @@ namespace platf {
|
||||
|
||||
void
|
||||
restart() {
|
||||
// If we're running standalone, we have to respawn ourselves via CreateProcess().
|
||||
// If we're running from the service, we should just exit and let it respawn us.
|
||||
if (GetConsoleWindow() != NULL) {
|
||||
// Avoid racing with the new process by waiting until we're exiting to start it.
|
||||
atexit(restart_on_exit);
|
||||
}
|
||||
|
||||
// We use an async exit call here because we can't block the HTTP thread or we'll hang shutdown.
|
||||
lifetime::exit_sunshine(0, true);
|
||||
}
|
||||
|
||||
struct enum_wnd_context_t {
|
||||
|
||||
@ -1,231 +0,0 @@
|
||||
/**
|
||||
* @file src/platform/windows/publish.cpp
|
||||
* @brief todo
|
||||
*/
|
||||
#include <winsock2.h>
|
||||
|
||||
#include <windows.h>
|
||||
|
||||
#include <windns.h>
|
||||
#include <winerror.h>
|
||||
|
||||
#include <boost/asio/ip/host_name.hpp>
|
||||
|
||||
#include "misc.h"
|
||||
#include "src/config.h"
|
||||
#include "src/logging.h"
|
||||
#include "src/network.h"
|
||||
#include "src/nvhttp.h"
|
||||
#include "src/platform/common.h"
|
||||
#include "src/thread_safe.h"
|
||||
|
||||
#define _FN(x, ret, args) \
|
||||
typedef ret(*x##_fn) args; \
|
||||
static x##_fn x
|
||||
|
||||
using namespace std::literals;
|
||||
|
||||
#define __SV(quote) L##quote##sv
|
||||
#define SV(quote) __SV(quote)
|
||||
|
||||
extern "C" {
|
||||
#ifndef __MINGW32__
|
||||
constexpr auto DNS_REQUEST_PENDING = 9506L;
|
||||
constexpr auto DNS_QUERY_REQUEST_VERSION1 = 0x1;
|
||||
constexpr auto DNS_QUERY_RESULTS_VERSION1 = 0x1;
|
||||
#endif
|
||||
|
||||
#define SERVICE_DOMAIN "local"
|
||||
|
||||
constexpr auto SERVICE_INSTANCE_NAME = SV(SERVICE_NAME "." SERVICE_TYPE "." SERVICE_DOMAIN);
|
||||
constexpr auto SERVICE_TYPE_DOMAIN = SV(SERVICE_TYPE "." SERVICE_DOMAIN);
|
||||
|
||||
#ifndef __MINGW32__
|
||||
typedef struct _DNS_SERVICE_INSTANCE {
|
||||
LPWSTR pszInstanceName;
|
||||
LPWSTR pszHostName;
|
||||
|
||||
IP4_ADDRESS *ip4Address;
|
||||
IP6_ADDRESS *ip6Address;
|
||||
|
||||
WORD wPort;
|
||||
WORD wPriority;
|
||||
WORD wWeight;
|
||||
|
||||
// Property list
|
||||
DWORD dwPropertyCount;
|
||||
|
||||
PWSTR *keys;
|
||||
PWSTR *values;
|
||||
|
||||
DWORD dwInterfaceIndex;
|
||||
} DNS_SERVICE_INSTANCE, *PDNS_SERVICE_INSTANCE;
|
||||
#endif
|
||||
|
||||
typedef VOID WINAPI
|
||||
DNS_SERVICE_REGISTER_COMPLETE(
|
||||
_In_ DWORD Status,
|
||||
_In_ PVOID pQueryContext,
|
||||
_In_ PDNS_SERVICE_INSTANCE pInstance);
|
||||
|
||||
typedef DNS_SERVICE_REGISTER_COMPLETE *PDNS_SERVICE_REGISTER_COMPLETE;
|
||||
|
||||
#ifndef __MINGW32__
|
||||
typedef struct _DNS_SERVICE_CANCEL {
|
||||
PVOID reserved;
|
||||
} DNS_SERVICE_CANCEL, *PDNS_SERVICE_CANCEL;
|
||||
|
||||
typedef struct _DNS_SERVICE_REGISTER_REQUEST {
|
||||
ULONG Version;
|
||||
ULONG InterfaceIndex;
|
||||
PDNS_SERVICE_INSTANCE pServiceInstance;
|
||||
PDNS_SERVICE_REGISTER_COMPLETE pRegisterCompletionCallback;
|
||||
PVOID pQueryContext;
|
||||
HANDLE hCredentials;
|
||||
BOOL unicastEnabled;
|
||||
} DNS_SERVICE_REGISTER_REQUEST, *PDNS_SERVICE_REGISTER_REQUEST;
|
||||
#endif
|
||||
|
||||
_FN(_DnsServiceFreeInstance, VOID, (_In_ PDNS_SERVICE_INSTANCE pInstance));
|
||||
_FN(_DnsServiceDeRegister, DWORD, (_In_ PDNS_SERVICE_REGISTER_REQUEST pRequest, _Inout_opt_ PDNS_SERVICE_CANCEL pCancel));
|
||||
_FN(_DnsServiceRegister, DWORD, (_In_ PDNS_SERVICE_REGISTER_REQUEST pRequest, _Inout_opt_ PDNS_SERVICE_CANCEL pCancel));
|
||||
} /* extern "C" */
|
||||
|
||||
namespace platf::publish {
|
||||
VOID WINAPI
|
||||
register_cb(DWORD status, PVOID pQueryContext, PDNS_SERVICE_INSTANCE pInstance) {
|
||||
auto alarm = (safe::alarm_t<PDNS_SERVICE_INSTANCE>::element_type *) pQueryContext;
|
||||
|
||||
if (status) {
|
||||
print_status("register_cb()"sv, status);
|
||||
}
|
||||
|
||||
alarm->ring(pInstance);
|
||||
}
|
||||
|
||||
static int
|
||||
service(bool enable, PDNS_SERVICE_INSTANCE &existing_instance) {
|
||||
auto alarm = safe::make_alarm<PDNS_SERVICE_INSTANCE>();
|
||||
|
||||
std::wstring name { SERVICE_INSTANCE_NAME.data(), SERVICE_INSTANCE_NAME.size() };
|
||||
std::wstring domain { SERVICE_TYPE_DOMAIN.data(), SERVICE_TYPE_DOMAIN.size() };
|
||||
|
||||
auto host = from_utf8(boost::asio::ip::host_name() + ".local");
|
||||
|
||||
DNS_SERVICE_INSTANCE instance {};
|
||||
instance.pszInstanceName = name.data();
|
||||
instance.wPort = net::map_port(nvhttp::PORT_HTTP);
|
||||
instance.pszHostName = host.data();
|
||||
|
||||
// Setting these values ensures Windows mDNS answers comply with RFC 1035.
|
||||
// If these are unset, Windows will send a TXT record that has zero strings,
|
||||
// which is illegal. Setting them to a single empty value causes Windows to
|
||||
// send a single empty string for the TXT record, which is the correct thing
|
||||
// to do when advertising a service without any TXT strings.
|
||||
//
|
||||
// Most clients aren't strictly checking TXT record compliance with RFC 1035,
|
||||
// but Apple's mDNS resolver does and rejects the entire answer if an invalid
|
||||
// TXT record is present.
|
||||
PWCHAR keys[] = { nullptr };
|
||||
PWCHAR values[] = { nullptr };
|
||||
instance.dwPropertyCount = 1;
|
||||
instance.keys = keys;
|
||||
instance.values = values;
|
||||
|
||||
DNS_SERVICE_REGISTER_REQUEST req {};
|
||||
req.Version = DNS_QUERY_REQUEST_VERSION1;
|
||||
req.pQueryContext = alarm.get();
|
||||
req.pServiceInstance = enable ? &instance : existing_instance;
|
||||
req.pRegisterCompletionCallback = register_cb;
|
||||
|
||||
DNS_STATUS status {};
|
||||
|
||||
if (enable) {
|
||||
status = _DnsServiceRegister(&req, nullptr);
|
||||
if (status != DNS_REQUEST_PENDING) {
|
||||
print_status("DnsServiceRegister()"sv, status);
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
else {
|
||||
status = _DnsServiceDeRegister(&req, nullptr);
|
||||
if (status != DNS_REQUEST_PENDING) {
|
||||
print_status("DnsServiceDeRegister()"sv, status);
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
alarm->wait();
|
||||
|
||||
auto registered_instance = alarm->status();
|
||||
if (enable) {
|
||||
// Store this instance for later deregistration
|
||||
existing_instance = registered_instance;
|
||||
}
|
||||
else if (registered_instance) {
|
||||
// Deregistration was successful
|
||||
_DnsServiceFreeInstance(registered_instance);
|
||||
existing_instance = nullptr;
|
||||
}
|
||||
|
||||
return registered_instance ? 0 : -1;
|
||||
}
|
||||
|
||||
class mdns_registration_t: public ::platf::deinit_t {
|
||||
public:
|
||||
mdns_registration_t():
|
||||
existing_instance(nullptr) {
|
||||
if (service(true, existing_instance)) {
|
||||
BOOST_LOG(error) << "Unable to register Sunshine mDNS service"sv;
|
||||
return;
|
||||
}
|
||||
|
||||
BOOST_LOG(info) << "Registered Sunshine mDNS service"sv;
|
||||
}
|
||||
|
||||
~mdns_registration_t() override {
|
||||
if (existing_instance) {
|
||||
if (service(false, existing_instance)) {
|
||||
BOOST_LOG(error) << "Unable to unregister Sunshine mDNS service"sv;
|
||||
return;
|
||||
}
|
||||
|
||||
BOOST_LOG(info) << "Unregistered Sunshine mDNS service"sv;
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
PDNS_SERVICE_INSTANCE existing_instance;
|
||||
};
|
||||
|
||||
int
|
||||
load_funcs(HMODULE handle) {
|
||||
auto fg = util::fail_guard([handle]() {
|
||||
FreeLibrary(handle);
|
||||
});
|
||||
|
||||
_DnsServiceFreeInstance = (_DnsServiceFreeInstance_fn) GetProcAddress(handle, "DnsServiceFreeInstance");
|
||||
_DnsServiceDeRegister = (_DnsServiceDeRegister_fn) GetProcAddress(handle, "DnsServiceDeRegister");
|
||||
_DnsServiceRegister = (_DnsServiceRegister_fn) GetProcAddress(handle, "DnsServiceRegister");
|
||||
|
||||
if (!(_DnsServiceFreeInstance && _DnsServiceDeRegister && _DnsServiceRegister)) {
|
||||
BOOST_LOG(error) << "mDNS service not available in dnsapi.dll"sv;
|
||||
return -1;
|
||||
}
|
||||
|
||||
fg.disable();
|
||||
return 0;
|
||||
}
|
||||
|
||||
std::unique_ptr<::platf::deinit_t>
|
||||
start() {
|
||||
HMODULE handle = LoadLibrary("dnsapi.dll");
|
||||
|
||||
if (!handle || load_funcs(handle)) {
|
||||
BOOST_LOG(error) << "Couldn't load dnsapi.dll, You'll need to add PC manually from Moonlight"sv;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
return std::make_unique<mdns_registration_t>();
|
||||
}
|
||||
} // namespace platf::publish
|
||||
721
src/process.cpp
721
src/process.cpp
@ -1,721 +0,0 @@
|
||||
/**
|
||||
* @file src/process.cpp
|
||||
* @brief Handles the startup and shutdown of the apps started by a streaming Session.
|
||||
*/
|
||||
#define BOOST_BIND_GLOBAL_PLACEHOLDERS
|
||||
|
||||
#include "process.h"
|
||||
|
||||
#include <filesystem>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
#include <boost/algorithm/string.hpp>
|
||||
#include <boost/crc.hpp>
|
||||
#include <boost/filesystem.hpp>
|
||||
#include <boost/program_options/parsers.hpp>
|
||||
#include <boost/property_tree/json_parser.hpp>
|
||||
#include <boost/property_tree/ptree.hpp>
|
||||
|
||||
#include <openssl/evp.h>
|
||||
#include <openssl/sha.h>
|
||||
|
||||
#include "config.h"
|
||||
#include "crypto.h"
|
||||
#include "logging.h"
|
||||
#include "platform/common.h"
|
||||
#include "system_tray.h"
|
||||
#include "utility.h"
|
||||
|
||||
#ifdef _WIN32
|
||||
// from_utf8() string conversion function
|
||||
#include "platform/windows/misc.h"
|
||||
|
||||
// _SH constants for _wfsopen()
|
||||
#include <share.h>
|
||||
#endif
|
||||
|
||||
#define DEFAULT_APP_IMAGE_PATH SUNSHINE_ASSETS_DIR "/box.png"
|
||||
|
||||
namespace proc {
|
||||
using namespace std::literals;
|
||||
namespace bp = boost::process;
|
||||
namespace pt = boost::property_tree;
|
||||
|
||||
proc_t proc;
|
||||
|
||||
class deinit_t: public platf::deinit_t {
|
||||
public:
|
||||
~deinit_t() {
|
||||
proc.terminate();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Initializes proc functions
|
||||
* @return Unique pointer to `deinit_t` to manage cleanup
|
||||
*/
|
||||
std::unique_ptr<platf::deinit_t>
|
||||
init() {
|
||||
return std::make_unique<deinit_t>();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Terminates all child processes in a process group.
|
||||
* @param proc The child process itself.
|
||||
* @param group The group of all children in the process tree.
|
||||
* @param exit_timeout The timeout to wait for the process group to gracefully exit.
|
||||
*/
|
||||
void
|
||||
terminate_process_group(bp::child &proc, bp::group &group, std::chrono::seconds exit_timeout) {
|
||||
if (group.valid() && platf::process_group_running((std::uintptr_t) group.native_handle())) {
|
||||
if (exit_timeout.count() > 0) {
|
||||
// Request processes in the group to exit gracefully
|
||||
if (platf::request_process_group_exit((std::uintptr_t) group.native_handle())) {
|
||||
// If the request was successful, wait for a little while for them to exit.
|
||||
BOOST_LOG(info) << "Successfully requested the app to exit. Waiting up to "sv << exit_timeout.count() << " seconds for it to close."sv;
|
||||
|
||||
// group::wait_for() and similar functions are broken and deprecated, so we use a simple polling loop
|
||||
while (platf::process_group_running((std::uintptr_t) group.native_handle()) && (--exit_timeout).count() >= 0) {
|
||||
std::this_thread::sleep_for(1s);
|
||||
}
|
||||
|
||||
if (exit_timeout.count() < 0) {
|
||||
BOOST_LOG(warning) << "App did not fully exit within the timeout. Terminating the app's remaining processes."sv;
|
||||
}
|
||||
else {
|
||||
BOOST_LOG(info) << "All app processes have successfully exited."sv;
|
||||
}
|
||||
}
|
||||
else {
|
||||
BOOST_LOG(info) << "App did not respond to a graceful termination request. Forcefully terminating the app's processes."sv;
|
||||
}
|
||||
}
|
||||
else {
|
||||
BOOST_LOG(info) << "No graceful exit timeout was specified for this app. Forcefully terminating the app's processes."sv;
|
||||
}
|
||||
|
||||
// We always call terminate() even if we waited successfully for all processes above.
|
||||
// This ensures the process group state is consistent with the OS in boost.
|
||||
group.terminate();
|
||||
group.detach();
|
||||
}
|
||||
|
||||
if (proc.valid()) {
|
||||
// avoid zombie process
|
||||
proc.detach();
|
||||
}
|
||||
}
|
||||
|
||||
boost::filesystem::path
|
||||
find_working_directory(const std::string &cmd, bp::environment &env) {
|
||||
// Parse the raw command string into parts to get the actual command portion
|
||||
#ifdef _WIN32
|
||||
auto parts = boost::program_options::split_winmain(cmd);
|
||||
#else
|
||||
auto parts = boost::program_options::split_unix(cmd);
|
||||
#endif
|
||||
if (parts.empty()) {
|
||||
BOOST_LOG(error) << "Unable to parse command: "sv << cmd;
|
||||
return boost::filesystem::path();
|
||||
}
|
||||
|
||||
BOOST_LOG(debug) << "Parsed target ["sv << parts.at(0) << "] from command ["sv << cmd << ']';
|
||||
|
||||
// If the target is a URL, don't parse any further here
|
||||
if (parts.at(0).find("://") != std::string::npos) {
|
||||
return boost::filesystem::path();
|
||||
}
|
||||
|
||||
// If the cmd path is not an absolute path, resolve it using our PATH variable
|
||||
boost::filesystem::path cmd_path(parts.at(0));
|
||||
if (!cmd_path.is_absolute()) {
|
||||
cmd_path = boost::process::search_path(parts.at(0));
|
||||
if (cmd_path.empty()) {
|
||||
BOOST_LOG(error) << "Unable to find executable ["sv << parts.at(0) << "]. Is it in your PATH?"sv;
|
||||
return boost::filesystem::path();
|
||||
}
|
||||
}
|
||||
|
||||
BOOST_LOG(debug) << "Resolved target ["sv << parts.at(0) << "] to path ["sv << cmd_path << ']';
|
||||
|
||||
// Now that we have a complete path, we can just use parent_path()
|
||||
return cmd_path.parent_path();
|
||||
}
|
||||
|
||||
int
|
||||
proc_t::execute(int app_id, std::shared_ptr<rtsp_stream::launch_session_t> launch_session) {
|
||||
// Ensure starting from a clean slate
|
||||
terminate();
|
||||
|
||||
auto iter = std::find_if(_apps.begin(), _apps.end(), [&app_id](const auto app) {
|
||||
return app.id == std::to_string(app_id);
|
||||
});
|
||||
|
||||
if (iter == _apps.end()) {
|
||||
BOOST_LOG(error) << "Couldn't find app with ID ["sv << app_id << ']';
|
||||
return 404;
|
||||
}
|
||||
|
||||
_app_id = app_id;
|
||||
_app = *iter;
|
||||
_app_prep_begin = std::begin(_app.prep_cmds);
|
||||
_app_prep_it = _app_prep_begin;
|
||||
|
||||
// Add Stream-specific environment variables
|
||||
_env["SUNSHINE_APP_ID"] = std::to_string(_app_id);
|
||||
_env["SUNSHINE_APP_NAME"] = _app.name;
|
||||
_env["SUNSHINE_CLIENT_WIDTH"] = std::to_string(launch_session->width);
|
||||
_env["SUNSHINE_CLIENT_HEIGHT"] = std::to_string(launch_session->height);
|
||||
_env["SUNSHINE_CLIENT_FPS"] = std::to_string(launch_session->fps);
|
||||
_env["SUNSHINE_CLIENT_HDR"] = launch_session->enable_hdr ? "true" : "false";
|
||||
_env["SUNSHINE_CLIENT_GCMAP"] = std::to_string(launch_session->gcmap);
|
||||
_env["SUNSHINE_CLIENT_HOST_AUDIO"] = launch_session->host_audio ? "true" : "false";
|
||||
_env["SUNSHINE_CLIENT_ENABLE_SOPS"] = launch_session->enable_sops ? "true" : "false";
|
||||
int channelCount = launch_session->surround_info & (65535);
|
||||
switch (channelCount) {
|
||||
case 2:
|
||||
_env["SUNSHINE_CLIENT_AUDIO_CONFIGURATION"] = "2.0";
|
||||
break;
|
||||
case 6:
|
||||
_env["SUNSHINE_CLIENT_AUDIO_CONFIGURATION"] = "5.1";
|
||||
break;
|
||||
case 8:
|
||||
_env["SUNSHINE_CLIENT_AUDIO_CONFIGURATION"] = "7.1";
|
||||
break;
|
||||
}
|
||||
|
||||
if (!_app.output.empty() && _app.output != "null"sv) {
|
||||
#ifdef _WIN32
|
||||
// fopen() interprets the filename as an ANSI string on Windows, so we must convert it
|
||||
// to UTF-16 and use the wchar_t variants for proper Unicode log file path support.
|
||||
auto woutput = platf::from_utf8(_app.output);
|
||||
|
||||
// Use _SH_DENYNO to allow us to open this log file again for writing even if it is
|
||||
// still open from a previous execution. This is required to handle the case of a
|
||||
// detached process executing again while the previous process is still running.
|
||||
_pipe.reset(_wfsopen(woutput.c_str(), L"a", _SH_DENYNO));
|
||||
#else
|
||||
_pipe.reset(fopen(_app.output.c_str(), "a"));
|
||||
#endif
|
||||
}
|
||||
|
||||
std::error_code ec;
|
||||
// Executed when returning from function
|
||||
auto fg = util::fail_guard([&]() {
|
||||
terminate();
|
||||
});
|
||||
|
||||
for (; _app_prep_it != std::end(_app.prep_cmds); ++_app_prep_it) {
|
||||
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.do_cmd, _env) :
|
||||
boost::filesystem::path(_app.working_dir);
|
||||
BOOST_LOG(info) << "Executing Do Cmd: ["sv << cmd.do_cmd << ']';
|
||||
auto child = platf::run_command(cmd.elevated, true, cmd.do_cmd, working_dir, _env, _pipe.get(), ec, nullptr);
|
||||
|
||||
if (ec) {
|
||||
BOOST_LOG(error) << "Couldn't run ["sv << cmd.do_cmd << "]: System: "sv << ec.message();
|
||||
// We don't want any prep commands failing launch of the desktop.
|
||||
// This is to prevent the issue where users reboot their PC and need to log in with Sunshine.
|
||||
// permission_denied is typically returned when the user impersonation fails, which can happen when user is not signed in yet.
|
||||
if (!(_app.cmd.empty() && ec == std::errc::permission_denied)) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
child.wait();
|
||||
auto ret = child.exit_code();
|
||||
if (ret != 0 && ec != std::errc::permission_denied) {
|
||||
BOOST_LOG(error) << '[' << cmd.do_cmd << "] failed with code ["sv << ret << ']';
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
for (auto &cmd : _app.detached) {
|
||||
boost::filesystem::path working_dir = _app.working_dir.empty() ?
|
||||
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_command(_app.elevated, true, cmd, working_dir, _env, _pipe.get(), ec, nullptr);
|
||||
if (ec) {
|
||||
BOOST_LOG(warning) << "Couldn't spawn ["sv << cmd << "]: System: "sv << ec.message();
|
||||
}
|
||||
else {
|
||||
child.detach();
|
||||
}
|
||||
}
|
||||
|
||||
if (_app.cmd.empty()) {
|
||||
BOOST_LOG(info) << "Executing [Desktop]"sv;
|
||||
placebo = true;
|
||||
}
|
||||
else {
|
||||
boost::filesystem::path working_dir = _app.working_dir.empty() ?
|
||||
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_command(_app.elevated, true, _app.cmd, working_dir, _env, _pipe.get(), ec, &_process_group);
|
||||
if (ec) {
|
||||
BOOST_LOG(warning) << "Couldn't run ["sv << _app.cmd << "]: System: "sv << ec.message();
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
_app_launch_time = std::chrono::steady_clock::now();
|
||||
|
||||
fg.disable();
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int
|
||||
proc_t::running() {
|
||||
if (placebo) {
|
||||
return _app_id;
|
||||
}
|
||||
else if (_app.wait_all && _process_group && platf::process_group_running((std::uintptr_t) _process_group.native_handle())) {
|
||||
// The app is still running if any process in the group is still running
|
||||
return _app_id;
|
||||
}
|
||||
else if (_process.running()) {
|
||||
// The app is still running only if the initial process launched is still running
|
||||
return _app_id;
|
||||
}
|
||||
else if (_app.auto_detach && _process.native_exit_code() == 0 &&
|
||||
std::chrono::steady_clock::now() - _app_launch_time < 5s) {
|
||||
BOOST_LOG(info) << "App exited gracefully within 5 seconds of launch. Treating the app as a detached command."sv;
|
||||
BOOST_LOG(info) << "Adjust this behavior in the Applications tab or apps.json if this is not what you want."sv;
|
||||
placebo = true;
|
||||
return _app_id;
|
||||
}
|
||||
|
||||
// Perform cleanup actions now if needed
|
||||
if (_process) {
|
||||
BOOST_LOG(info) << "App exited with code ["sv << _process.native_exit_code() << ']';
|
||||
terminate();
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
void
|
||||
proc_t::terminate() {
|
||||
std::error_code ec;
|
||||
placebo = false;
|
||||
terminate_process_group(_process, _process_group, _app.exit_timeout);
|
||||
_process = bp::child();
|
||||
_process_group = bp::group();
|
||||
|
||||
for (; _app_prep_it != _app_prep_begin; --_app_prep_it) {
|
||||
auto &cmd = *(_app_prep_it - 1);
|
||||
|
||||
if (cmd.undo_cmd.empty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
boost::filesystem::path working_dir = _app.working_dir.empty() ?
|
||||
find_working_directory(cmd.undo_cmd, _env) :
|
||||
boost::filesystem::path(_app.working_dir);
|
||||
BOOST_LOG(info) << "Executing Undo Cmd: ["sv << cmd.undo_cmd << ']';
|
||||
auto child = platf::run_command(cmd.elevated, true, cmd.undo_cmd, working_dir, _env, _pipe.get(), ec, nullptr);
|
||||
|
||||
if (ec) {
|
||||
BOOST_LOG(warning) << "System: "sv << ec.message();
|
||||
}
|
||||
|
||||
child.wait();
|
||||
auto ret = child.exit_code();
|
||||
|
||||
if (ret != 0) {
|
||||
BOOST_LOG(warning) << "Return code ["sv << ret << ']';
|
||||
}
|
||||
}
|
||||
|
||||
_pipe.reset();
|
||||
#if defined SUNSHINE_TRAY && SUNSHINE_TRAY >= 1
|
||||
bool has_run = _app_id > 0;
|
||||
|
||||
// Only show the Stopped notification if we actually have an app to stop
|
||||
// Since terminate() is always run when a new app has started
|
||||
if (proc::proc.get_last_run_app_name().length() > 0 && has_run) {
|
||||
system_tray::update_tray_stopped(proc::proc.get_last_run_app_name());
|
||||
}
|
||||
#endif
|
||||
|
||||
_app_id = -1;
|
||||
}
|
||||
|
||||
const std::vector<ctx_t> &
|
||||
proc_t::get_apps() const {
|
||||
return _apps;
|
||||
}
|
||||
std::vector<ctx_t> &
|
||||
proc_t::get_apps() {
|
||||
return _apps;
|
||||
}
|
||||
|
||||
// Gets application image from application list.
|
||||
// Returns image from assets directory if found there.
|
||||
// Returns default image if image configuration is not set.
|
||||
// Returns http content-type header compatible image type.
|
||||
std::string
|
||||
proc_t::get_app_image(int app_id) {
|
||||
auto iter = std::find_if(_apps.begin(), _apps.end(), [&app_id](const auto app) {
|
||||
return app.id == std::to_string(app_id);
|
||||
});
|
||||
auto app_image_path = iter == _apps.end() ? std::string() : iter->image_path;
|
||||
|
||||
return validate_app_image_path(app_image_path);
|
||||
}
|
||||
|
||||
std::string
|
||||
proc_t::get_last_run_app_name() {
|
||||
return _app.name;
|
||||
}
|
||||
|
||||
proc_t::~proc_t() {
|
||||
// It's not safe to call terminate() here because our proc_t is a static variable
|
||||
// that may be destroyed after the Boost loggers have been destroyed. Instead,
|
||||
// we return a deinit_t to main() to handle termination when we're exiting.
|
||||
// Once we reach this point here, termination must have already happened.
|
||||
assert(!placebo);
|
||||
assert(!_process.running());
|
||||
}
|
||||
|
||||
std::string_view::iterator
|
||||
find_match(std::string_view::iterator begin, std::string_view::iterator end) {
|
||||
int stack = 0;
|
||||
|
||||
--begin;
|
||||
do {
|
||||
++begin;
|
||||
switch (*begin) {
|
||||
case '(':
|
||||
++stack;
|
||||
break;
|
||||
case ')':
|
||||
--stack;
|
||||
}
|
||||
} while (begin != end && stack != 0);
|
||||
|
||||
if (begin == end) {
|
||||
throw std::out_of_range("Missing closing bracket \')\'");
|
||||
}
|
||||
return begin;
|
||||
}
|
||||
|
||||
std::string
|
||||
parse_env_val(bp::native_environment &env, const std::string_view &val_raw) {
|
||||
auto pos = std::begin(val_raw);
|
||||
auto dollar = std::find(pos, std::end(val_raw), '$');
|
||||
|
||||
std::stringstream ss;
|
||||
|
||||
while (dollar != std::end(val_raw)) {
|
||||
auto next = dollar + 1;
|
||||
if (next != std::end(val_raw)) {
|
||||
switch (*next) {
|
||||
case '(': {
|
||||
ss.write(pos, (dollar - pos));
|
||||
auto var_begin = next + 1;
|
||||
auto var_end = find_match(next, std::end(val_raw));
|
||||
auto var_name = std::string { var_begin, var_end };
|
||||
|
||||
#ifdef _WIN32
|
||||
// Windows treats environment variable names in a case-insensitive manner,
|
||||
// so we look for a case-insensitive match here. This is critical for
|
||||
// correctly appending to PATH on Windows.
|
||||
auto itr = std::find_if(env.cbegin(), env.cend(),
|
||||
[&](const auto &e) { return boost::iequals(e.get_name(), var_name); });
|
||||
if (itr != env.cend()) {
|
||||
// Use an existing case-insensitive match
|
||||
var_name = itr->get_name();
|
||||
}
|
||||
#endif
|
||||
|
||||
ss << env[var_name].to_string();
|
||||
|
||||
pos = var_end + 1;
|
||||
next = var_end;
|
||||
|
||||
break;
|
||||
}
|
||||
case '$':
|
||||
ss.write(pos, (next - pos));
|
||||
pos = next + 1;
|
||||
++next;
|
||||
break;
|
||||
}
|
||||
|
||||
dollar = std::find(next, std::end(val_raw), '$');
|
||||
}
|
||||
else {
|
||||
dollar = next;
|
||||
}
|
||||
}
|
||||
|
||||
ss.write(pos, (dollar - pos));
|
||||
|
||||
return ss.str();
|
||||
}
|
||||
|
||||
std::string
|
||||
validate_app_image_path(std::string app_image_path) {
|
||||
if (app_image_path.empty()) {
|
||||
return DEFAULT_APP_IMAGE_PATH;
|
||||
}
|
||||
|
||||
// get the image extension and convert it to lowercase
|
||||
auto image_extension = std::filesystem::path(app_image_path).extension().string();
|
||||
boost::to_lower(image_extension);
|
||||
|
||||
// return the default box image if extension is not "png"
|
||||
if (image_extension != ".png") {
|
||||
return DEFAULT_APP_IMAGE_PATH;
|
||||
}
|
||||
|
||||
// check if image is in assets directory
|
||||
auto full_image_path = std::filesystem::path(SUNSHINE_ASSETS_DIR) / app_image_path;
|
||||
if (std::filesystem::exists(full_image_path)) {
|
||||
return full_image_path.string();
|
||||
}
|
||||
else if (app_image_path == "./assets/steam.png") {
|
||||
// handle old default steam image definition
|
||||
return SUNSHINE_ASSETS_DIR "/steam.png";
|
||||
}
|
||||
|
||||
// check if specified image exists
|
||||
std::error_code code;
|
||||
if (!std::filesystem::exists(app_image_path, code)) {
|
||||
// return default box image if image does not exist
|
||||
BOOST_LOG(warning) << "Couldn't find app image at path ["sv << app_image_path << ']';
|
||||
return DEFAULT_APP_IMAGE_PATH;
|
||||
}
|
||||
|
||||
// image is a png, and not in assets directory
|
||||
// return only "content-type" http header compatible image type
|
||||
return app_image_path;
|
||||
}
|
||||
|
||||
std::optional<std::string>
|
||||
calculate_sha256(const std::string &filename) {
|
||||
crypto::md_ctx_t ctx { EVP_MD_CTX_create() };
|
||||
if (!ctx) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
if (!EVP_DigestInit_ex(ctx.get(), EVP_sha256(), nullptr)) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
// Read file and update calculated SHA
|
||||
char buf[1024 * 16];
|
||||
std::ifstream file(filename, std::ifstream::binary);
|
||||
while (file.good()) {
|
||||
file.read(buf, sizeof(buf));
|
||||
if (!EVP_DigestUpdate(ctx.get(), buf, file.gcount())) {
|
||||
return std::nullopt;
|
||||
}
|
||||
}
|
||||
file.close();
|
||||
|
||||
unsigned char result[SHA256_DIGEST_LENGTH];
|
||||
if (!EVP_DigestFinal_ex(ctx.get(), result, nullptr)) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
// Transform byte-array to string
|
||||
std::stringstream ss;
|
||||
ss << std::hex << std::setfill('0');
|
||||
for (const auto &byte : result) {
|
||||
ss << std::setw(2) << (int) byte;
|
||||
}
|
||||
return ss.str();
|
||||
}
|
||||
|
||||
uint32_t
|
||||
calculate_crc32(const std::string &input) {
|
||||
boost::crc_32_type result;
|
||||
result.process_bytes(input.data(), input.length());
|
||||
return result.checksum();
|
||||
}
|
||||
|
||||
std::tuple<std::string, std::string>
|
||||
calculate_app_id(const std::string &app_name, std::string app_image_path, int index) {
|
||||
// Generate id by hashing name with image data if present
|
||||
std::vector<std::string> to_hash;
|
||||
to_hash.push_back(app_name);
|
||||
auto file_path = validate_app_image_path(app_image_path);
|
||||
if (file_path != DEFAULT_APP_IMAGE_PATH) {
|
||||
auto file_hash = calculate_sha256(file_path);
|
||||
if (file_hash) {
|
||||
to_hash.push_back(file_hash.value());
|
||||
}
|
||||
else {
|
||||
// Fallback to just hashing image path
|
||||
to_hash.push_back(file_path);
|
||||
}
|
||||
}
|
||||
|
||||
// Create combined strings for hash
|
||||
std::stringstream ss;
|
||||
for_each(to_hash.begin(), to_hash.end(), [&ss](const std::string &s) { ss << s; });
|
||||
auto input_no_index = ss.str();
|
||||
ss << index;
|
||||
auto input_with_index = ss.str();
|
||||
|
||||
// CRC32 then truncate to signed 32-bit range due to client limitations
|
||||
auto id_no_index = std::to_string(abs((int32_t) calculate_crc32(input_no_index)));
|
||||
auto id_with_index = std::to_string(abs((int32_t) calculate_crc32(input_with_index)));
|
||||
|
||||
return std::make_tuple(id_no_index, id_with_index);
|
||||
}
|
||||
|
||||
std::optional<proc::proc_t>
|
||||
parse(const std::string &file_name) {
|
||||
pt::ptree tree;
|
||||
|
||||
try {
|
||||
pt::read_json(file_name, tree);
|
||||
|
||||
auto &apps_node = tree.get_child("apps"s);
|
||||
auto &env_vars = tree.get_child("env"s);
|
||||
|
||||
auto this_env = boost::this_process::environment();
|
||||
|
||||
for (auto &[name, val] : env_vars) {
|
||||
this_env[name] = parse_env_val(this_env, val.get_value<std::string>());
|
||||
}
|
||||
|
||||
std::set<std::string> ids;
|
||||
std::vector<proc::ctx_t> apps;
|
||||
int i = 0;
|
||||
for (auto &[_, app_node] : apps_node) {
|
||||
proc::ctx_t ctx;
|
||||
|
||||
auto prep_nodes_opt = app_node.get_child_optional("prep-cmd"s);
|
||||
auto detached_nodes_opt = app_node.get_child_optional("detached"s);
|
||||
auto exclude_global_prep = app_node.get_optional<bool>("exclude-global-prep-cmd"s);
|
||||
auto output = app_node.get_optional<std::string>("output"s);
|
||||
auto name = parse_env_val(this_env, app_node.get<std::string>("name"s));
|
||||
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);
|
||||
auto auto_detach = app_node.get_optional<bool>("auto-detach"s);
|
||||
auto wait_all = app_node.get_optional<bool>("wait-all"s);
|
||||
auto exit_timeout = app_node.get_optional<int>("exit-timeout"s);
|
||||
|
||||
std::vector<proc::cmd_t> prep_cmds;
|
||||
if (!exclude_global_prep.value_or(false)) {
|
||||
prep_cmds.reserve(config::sunshine.prep_cmds.size());
|
||||
for (auto &prep_cmd : config::sunshine.prep_cmds) {
|
||||
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),
|
||||
std::move(prep_cmd.elevated));
|
||||
}
|
||||
}
|
||||
|
||||
if (prep_nodes_opt) {
|
||||
auto &prep_nodes = *prep_nodes_opt;
|
||||
|
||||
prep_cmds.reserve(prep_cmds.size() + prep_nodes.size());
|
||||
for (auto &[_, prep_node] : prep_nodes) {
|
||||
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");
|
||||
|
||||
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)));
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<std::string> detached;
|
||||
if (detached_nodes_opt) {
|
||||
auto &detached_nodes = *detached_nodes_opt;
|
||||
|
||||
detached.reserve(detached_nodes.size());
|
||||
for (auto &[_, detached_val] : detached_nodes) {
|
||||
detached.emplace_back(parse_env_val(this_env, detached_val.get_value<std::string>()));
|
||||
}
|
||||
}
|
||||
|
||||
if (output) {
|
||||
ctx.output = parse_env_val(this_env, *output);
|
||||
}
|
||||
|
||||
if (cmd) {
|
||||
ctx.cmd = parse_env_val(this_env, *cmd);
|
||||
}
|
||||
|
||||
if (working_dir) {
|
||||
ctx.working_dir = parse_env_val(this_env, *working_dir);
|
||||
#ifdef _WIN32
|
||||
// The working directory, unlike the command itself, should not be quoted
|
||||
// when it contains spaces. Unlike POSIX, Windows forbids quotes in paths,
|
||||
// so we can safely strip them all out here to avoid confusing the user.
|
||||
boost::erase_all(ctx.working_dir, "\"");
|
||||
#endif
|
||||
}
|
||||
|
||||
if (image_path) {
|
||||
ctx.image_path = parse_env_val(this_env, *image_path);
|
||||
}
|
||||
|
||||
ctx.elevated = elevated.value_or(false);
|
||||
ctx.auto_detach = auto_detach.value_or(true);
|
||||
ctx.wait_all = wait_all.value_or(true);
|
||||
ctx.exit_timeout = std::chrono::seconds { exit_timeout.value_or(5) };
|
||||
|
||||
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
|
||||
ctx.id = std::get<0>(possible_ids);
|
||||
}
|
||||
else {
|
||||
// Fallback to include index on collision
|
||||
ctx.id = std::get<1>(possible_ids);
|
||||
}
|
||||
ids.insert(ctx.id);
|
||||
|
||||
ctx.name = std::move(name);
|
||||
ctx.prep_cmds = std::move(prep_cmds);
|
||||
ctx.detached = std::move(detached);
|
||||
|
||||
apps.emplace_back(std::move(ctx));
|
||||
}
|
||||
|
||||
return proc::proc_t {
|
||||
std::move(this_env), std::move(apps)
|
||||
};
|
||||
}
|
||||
catch (std::exception &e) {
|
||||
BOOST_LOG(error) << e.what();
|
||||
}
|
||||
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
void
|
||||
refresh(const std::string &file_name) {
|
||||
auto proc_opt = proc::parse(file_name);
|
||||
|
||||
if (proc_opt) {
|
||||
proc = std::move(*proc_opt);
|
||||
}
|
||||
}
|
||||
} // namespace proc
|
||||
136
src/process.h
136
src/process.h
@ -1,136 +0,0 @@
|
||||
/**
|
||||
* @file src/process.h
|
||||
* @brief todo
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#ifndef __kernel_entry
|
||||
#define __kernel_entry
|
||||
#endif
|
||||
|
||||
#include <optional>
|
||||
#include <unordered_map>
|
||||
|
||||
#include <boost/process.hpp>
|
||||
|
||||
#include "config.h"
|
||||
#include "platform/common.h"
|
||||
#include "rtsp.h"
|
||||
#include "utility.h"
|
||||
|
||||
namespace proc {
|
||||
using file_t = util::safe_ptr_v2<FILE, int, fclose>;
|
||||
|
||||
typedef config::prep_cmd_t cmd_t;
|
||||
/**
|
||||
* pre_cmds -- guaranteed to be executed unless any of the commands fail.
|
||||
* detached -- commands detached from Sunshine
|
||||
* cmd -- Runs indefinitely until:
|
||||
* No session is running and a different set of commands it to be executed
|
||||
* Command exits
|
||||
* working_dir -- the process working directory. This is required for some games to run properly.
|
||||
* cmd_output --
|
||||
* empty -- The output of the commands are appended to the output of sunshine
|
||||
* "null" -- The output of the commands are discarded
|
||||
* filename -- The output of the commands are appended to filename
|
||||
*/
|
||||
struct ctx_t {
|
||||
std::vector<cmd_t> prep_cmds;
|
||||
|
||||
/**
|
||||
* Some applications, such as Steam, either exit quickly, or keep running indefinitely.
|
||||
*
|
||||
* Apps that launch normal child processes and terminate will be handled by the process
|
||||
* grouping logic (wait_all). However, apps that launch child processes indirectly or
|
||||
* into another process group (such as UWP apps) can only be handled by the auto-detach
|
||||
* heuristic which catches processes that exit 0 very quickly, but we won't have proper
|
||||
* process tracking for those.
|
||||
*
|
||||
* For cases where users just want to kick off a background process and never manage the
|
||||
* lifetime of that process, they can use detached commands for that.
|
||||
*/
|
||||
std::vector<std::string> detached;
|
||||
|
||||
std::string name;
|
||||
std::string cmd;
|
||||
std::string working_dir;
|
||||
std::string output;
|
||||
std::string image_path;
|
||||
std::string id;
|
||||
bool elevated;
|
||||
bool auto_detach;
|
||||
bool wait_all;
|
||||
std::chrono::seconds exit_timeout;
|
||||
};
|
||||
|
||||
class proc_t {
|
||||
public:
|
||||
KITTY_DEFAULT_CONSTR_MOVE_THROW(proc_t)
|
||||
|
||||
proc_t(
|
||||
boost::process::environment &&env,
|
||||
std::vector<ctx_t> &&apps):
|
||||
_app_id(0),
|
||||
_env(std::move(env)),
|
||||
_apps(std::move(apps)) {}
|
||||
|
||||
int
|
||||
execute(int app_id, std::shared_ptr<rtsp_stream::launch_session_t> launch_session);
|
||||
|
||||
/**
|
||||
* @return _app_id if a process is running, otherwise returns 0
|
||||
*/
|
||||
int
|
||||
running();
|
||||
|
||||
~proc_t();
|
||||
|
||||
const std::vector<ctx_t> &
|
||||
get_apps() const;
|
||||
std::vector<ctx_t> &
|
||||
get_apps();
|
||||
std::string
|
||||
get_app_image(int app_id);
|
||||
std::string
|
||||
get_last_run_app_name();
|
||||
void
|
||||
terminate();
|
||||
|
||||
private:
|
||||
int _app_id;
|
||||
|
||||
boost::process::environment _env;
|
||||
std::vector<ctx_t> _apps;
|
||||
ctx_t _app;
|
||||
std::chrono::steady_clock::time_point _app_launch_time;
|
||||
|
||||
// If no command associated with _app_id, yet it's still running
|
||||
bool placebo {};
|
||||
|
||||
boost::process::child _process;
|
||||
boost::process::group _process_group;
|
||||
|
||||
file_t _pipe;
|
||||
std::vector<cmd_t>::const_iterator _app_prep_it;
|
||||
std::vector<cmd_t>::const_iterator _app_prep_begin;
|
||||
};
|
||||
|
||||
/**
|
||||
* Calculate a stable id based on name and image data
|
||||
* @return tuple of id calculated without index (for use if no collision) and one with
|
||||
*/
|
||||
std::tuple<std::string, std::string>
|
||||
calculate_app_id(const std::string &app_name, std::string app_image_path, int index);
|
||||
|
||||
std::string
|
||||
validate_app_image_path(std::string app_image_path);
|
||||
void
|
||||
refresh(const std::string &file_name);
|
||||
std::optional<proc::proc_t>
|
||||
parse(const std::string &file_name);
|
||||
|
||||
std::unique_ptr<platf::deinit_t>
|
||||
init();
|
||||
|
||||
extern proc_t proc;
|
||||
} // namespace proc
|
||||
1198
src/rtsp.cpp
1198
src/rtsp.cpp
File diff suppressed because it is too large
Load Diff
52
src/rtsp.h
52
src/rtsp.h
@ -1,52 +0,0 @@
|
||||
/**
|
||||
* @file src/rtsp.h
|
||||
* @brief todo
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include <atomic>
|
||||
|
||||
#include "crypto.h"
|
||||
#include "thread_safe.h"
|
||||
|
||||
namespace rtsp_stream {
|
||||
constexpr auto RTSP_SETUP_PORT = 21;
|
||||
|
||||
struct launch_session_t {
|
||||
uint32_t id;
|
||||
|
||||
crypto::aes_t gcm_key;
|
||||
crypto::aes_t iv;
|
||||
|
||||
std::string av_ping_payload;
|
||||
uint32_t control_connect_data;
|
||||
|
||||
bool host_audio;
|
||||
std::string unique_id;
|
||||
int width;
|
||||
int height;
|
||||
int fps;
|
||||
int gcmap;
|
||||
int appid;
|
||||
int surround_info;
|
||||
bool enable_hdr;
|
||||
bool enable_sops;
|
||||
|
||||
std::optional<crypto::cipher::gcm_t> rtsp_cipher;
|
||||
std::string rtsp_url_scheme;
|
||||
uint32_t rtsp_iv_counter;
|
||||
};
|
||||
|
||||
void
|
||||
launch_session_raise(std::shared_ptr<launch_session_t> launch_session);
|
||||
|
||||
void
|
||||
launch_session_clear(uint32_t launch_session_id);
|
||||
|
||||
int
|
||||
session_count();
|
||||
|
||||
void
|
||||
rtpThread();
|
||||
|
||||
} // namespace rtsp_stream
|
||||
1964
src/stream.cpp
1964
src/stream.cpp
File diff suppressed because it is too large
Load Diff
54
src/stream.h
54
src/stream.h
@ -1,54 +0,0 @@
|
||||
/**
|
||||
* @file src/stream.h
|
||||
* @brief todo
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include <boost/asio.hpp>
|
||||
|
||||
#include "audio.h"
|
||||
#include "crypto.h"
|
||||
#include "video.h"
|
||||
|
||||
namespace stream {
|
||||
constexpr auto VIDEO_STREAM_PORT = 9;
|
||||
constexpr auto CONTROL_PORT = 10;
|
||||
constexpr auto AUDIO_STREAM_PORT = 11;
|
||||
|
||||
struct session_t;
|
||||
struct config_t {
|
||||
audio::config_t audio;
|
||||
video::config_t monitor;
|
||||
|
||||
int packetsize;
|
||||
int minRequiredFecPackets;
|
||||
int mlFeatureFlags;
|
||||
int controlProtocolType;
|
||||
int audioQosType;
|
||||
int videoQosType;
|
||||
|
||||
uint32_t encryptionFlagsEnabled;
|
||||
|
||||
std::optional<int> gcmap;
|
||||
};
|
||||
|
||||
namespace session {
|
||||
enum class state_e : int {
|
||||
STOPPED,
|
||||
STOPPING,
|
||||
STARTING,
|
||||
RUNNING,
|
||||
};
|
||||
|
||||
std::shared_ptr<session_t>
|
||||
alloc(config_t &config, rtsp_stream::launch_session_t &launch_session);
|
||||
int
|
||||
start(session_t &session, const std::string &addr_string);
|
||||
void
|
||||
stop(session_t &session);
|
||||
void
|
||||
join(session_t &session);
|
||||
state_e
|
||||
state(session_t &session);
|
||||
} // namespace session
|
||||
} // namespace stream
|
||||
@ -1,391 +0,0 @@
|
||||
/**
|
||||
* @file src/system_tray.cpp
|
||||
* @brief todo
|
||||
*/
|
||||
// macros
|
||||
#if defined SUNSHINE_TRAY && SUNSHINE_TRAY >= 1
|
||||
|
||||
#if defined(_WIN32)
|
||||
#define WIN32_LEAN_AND_MEAN
|
||||
#include <accctrl.h>
|
||||
#include <aclapi.h>
|
||||
#define TRAY_ICON WEB_DIR "images/sunshine.ico"
|
||||
#define TRAY_ICON_PLAYING WEB_DIR "images/sunshine-playing.ico"
|
||||
#define TRAY_ICON_PAUSING WEB_DIR "images/sunshine-pausing.ico"
|
||||
#define TRAY_ICON_LOCKED WEB_DIR "images/sunshine-locked.ico"
|
||||
#elif defined(__linux__) || defined(linux) || defined(__linux)
|
||||
#define TRAY_ICON "sunshine-tray"
|
||||
#define TRAY_ICON_PLAYING "sunshine-playing"
|
||||
#define TRAY_ICON_PAUSING "sunshine-pausing"
|
||||
#define TRAY_ICON_LOCKED "sunshine-locked"
|
||||
#elif defined(__APPLE__) || defined(__MACH__)
|
||||
#define TRAY_ICON WEB_DIR "images/logo-sunshine-16.png"
|
||||
#define TRAY_ICON_PLAYING WEB_DIR "images/sunshine-playing-16.png"
|
||||
#define TRAY_ICON_PAUSING WEB_DIR "images/sunshine-pausing-16.png"
|
||||
#define TRAY_ICON_LOCKED WEB_DIR "images/sunshine-locked-16.png"
|
||||
#include <dispatch/dispatch.h>
|
||||
#endif
|
||||
|
||||
// standard includes
|
||||
#include <csignal>
|
||||
#include <string>
|
||||
|
||||
// lib includes
|
||||
#include "tray/tray.h"
|
||||
#include <boost/filesystem.hpp>
|
||||
#include <boost/process/environment.hpp>
|
||||
|
||||
// local includes
|
||||
#include "confighttp.h"
|
||||
#include "logging.h"
|
||||
#include "platform/common.h"
|
||||
#include "process.h"
|
||||
#include "src/entry_handler.h"
|
||||
#include "version.h"
|
||||
|
||||
using namespace std::literals;
|
||||
|
||||
// system_tray namespace
|
||||
namespace system_tray {
|
||||
static std::atomic<bool> tray_initialized = false;
|
||||
|
||||
/**
|
||||
* @brief Callback for opening the UI from the system tray.
|
||||
* @param item The tray menu item.
|
||||
*/
|
||||
void
|
||||
tray_open_ui_cb(struct tray_menu *item) {
|
||||
BOOST_LOG(info) << "Opening UI from system tray"sv;
|
||||
launch_ui();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Callback for opening GitHub Sponsors from the system tray.
|
||||
* @param item The tray menu item.
|
||||
*/
|
||||
void
|
||||
tray_donate_github_cb(struct tray_menu *item) {
|
||||
platf::open_url("https://github.com/sponsors/LizardByte");
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Callback for opening MEE6 donation from the system tray.
|
||||
* @param item The tray menu item.
|
||||
*/
|
||||
void
|
||||
tray_donate_mee6_cb(struct tray_menu *item) {
|
||||
platf::open_url("https://mee6.xyz/m/804382334370578482");
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Callback for opening Patreon from the system tray.
|
||||
* @param item The tray menu item.
|
||||
*/
|
||||
void
|
||||
tray_donate_patreon_cb(struct tray_menu *item) {
|
||||
platf::open_url("https://www.patreon.com/LizardByte");
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Callback for opening PayPal donation from the system tray.
|
||||
* @param item The tray menu item.
|
||||
*/
|
||||
void
|
||||
tray_donate_paypal_cb(struct tray_menu *item) {
|
||||
platf::open_url("https://www.paypal.com/paypalme/ReenigneArcher");
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Callback for restarting Sunshine from the system tray.
|
||||
* @param item The tray menu item.
|
||||
*/
|
||||
void
|
||||
tray_restart_cb(struct tray_menu *item) {
|
||||
BOOST_LOG(info) << "Restarting from system tray"sv;
|
||||
|
||||
platf::restart();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Callback for exiting Sunshine from the system tray.
|
||||
* @param item The tray menu item.
|
||||
*/
|
||||
void
|
||||
tray_quit_cb(struct tray_menu *item) {
|
||||
BOOST_LOG(info) << "Quitting from system tray"sv;
|
||||
|
||||
#ifdef _WIN32
|
||||
// If we're running in a service, return a special status to
|
||||
// tell it to terminate too, otherwise it will just respawn us.
|
||||
if (GetConsoleWindow() == NULL) {
|
||||
lifetime::exit_sunshine(ERROR_SHUTDOWN_IN_PROGRESS, true);
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
|
||||
lifetime::exit_sunshine(0, true);
|
||||
}
|
||||
|
||||
// Tray menu
|
||||
static struct tray tray = {
|
||||
.icon = TRAY_ICON,
|
||||
.tooltip = PROJECT_NAME,
|
||||
.menu =
|
||||
(struct tray_menu[]) {
|
||||
// todo - use boost/locale to translate menu strings
|
||||
{ .text = "Open Sunshine", .cb = tray_open_ui_cb },
|
||||
{ .text = "-" },
|
||||
{ .text = "Donate",
|
||||
.submenu =
|
||||
(struct tray_menu[]) {
|
||||
{ .text = "GitHub Sponsors", .cb = tray_donate_github_cb },
|
||||
{ .text = "MEE6", .cb = tray_donate_mee6_cb },
|
||||
{ .text = "Patreon", .cb = tray_donate_patreon_cb },
|
||||
{ .text = "PayPal", .cb = tray_donate_paypal_cb },
|
||||
{ .text = nullptr } } },
|
||||
{ .text = "-" },
|
||||
{ .text = "Restart", .cb = tray_restart_cb },
|
||||
{ .text = "Quit", .cb = tray_quit_cb },
|
||||
{ .text = nullptr } },
|
||||
.iconPathCount = 4,
|
||||
.allIconPaths = { TRAY_ICON, TRAY_ICON_LOCKED, TRAY_ICON_PLAYING, TRAY_ICON_PAUSING },
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Create the system tray.
|
||||
* @details This function has an endless loop, so it should be run in a separate thread.
|
||||
* @return 1 if the system tray failed to create, otherwise 0 once the tray has been terminated.
|
||||
*/
|
||||
int
|
||||
system_tray() {
|
||||
#ifdef _WIN32
|
||||
// If we're running as SYSTEM, Explorer.exe will not have permission to open our thread handle
|
||||
// to monitor for thread termination. If Explorer fails to open our thread, our tray icon
|
||||
// will persist forever if we terminate unexpectedly. To avoid this, we will modify our thread
|
||||
// DACL to add an ACE that allows SYNCHRONIZE access to Everyone.
|
||||
{
|
||||
PACL old_dacl;
|
||||
PSECURITY_DESCRIPTOR sd;
|
||||
auto error = GetSecurityInfo(GetCurrentThread(),
|
||||
SE_KERNEL_OBJECT,
|
||||
DACL_SECURITY_INFORMATION,
|
||||
nullptr,
|
||||
nullptr,
|
||||
&old_dacl,
|
||||
nullptr,
|
||||
&sd);
|
||||
if (error != ERROR_SUCCESS) {
|
||||
BOOST_LOG(warning) << "GetSecurityInfo() failed: "sv << error;
|
||||
return 1;
|
||||
}
|
||||
|
||||
auto free_sd = util::fail_guard([sd]() {
|
||||
LocalFree(sd);
|
||||
});
|
||||
|
||||
SID_IDENTIFIER_AUTHORITY sid_authority = SECURITY_WORLD_SID_AUTHORITY;
|
||||
PSID world_sid;
|
||||
if (!AllocateAndInitializeSid(&sid_authority, 1, SECURITY_WORLD_RID, 0, 0, 0, 0, 0, 0, 0, &world_sid)) {
|
||||
error = GetLastError();
|
||||
BOOST_LOG(warning) << "AllocateAndInitializeSid() failed: "sv << error;
|
||||
return 1;
|
||||
}
|
||||
|
||||
auto free_sid = util::fail_guard([world_sid]() {
|
||||
FreeSid(world_sid);
|
||||
});
|
||||
|
||||
EXPLICIT_ACCESS ea {};
|
||||
ea.grfAccessPermissions = SYNCHRONIZE;
|
||||
ea.grfAccessMode = GRANT_ACCESS;
|
||||
ea.grfInheritance = NO_INHERITANCE;
|
||||
ea.Trustee.TrusteeForm = TRUSTEE_IS_SID;
|
||||
ea.Trustee.ptstrName = (LPSTR) world_sid;
|
||||
|
||||
PACL new_dacl;
|
||||
error = SetEntriesInAcl(1, &ea, old_dacl, &new_dacl);
|
||||
if (error != ERROR_SUCCESS) {
|
||||
BOOST_LOG(warning) << "SetEntriesInAcl() failed: "sv << error;
|
||||
return 1;
|
||||
}
|
||||
|
||||
auto free_new_dacl = util::fail_guard([new_dacl]() {
|
||||
LocalFree(new_dacl);
|
||||
});
|
||||
|
||||
error = SetSecurityInfo(GetCurrentThread(),
|
||||
SE_KERNEL_OBJECT,
|
||||
DACL_SECURITY_INFORMATION,
|
||||
nullptr,
|
||||
nullptr,
|
||||
new_dacl,
|
||||
nullptr);
|
||||
if (error != ERROR_SUCCESS) {
|
||||
BOOST_LOG(warning) << "SetSecurityInfo() failed: "sv << error;
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Wait for the shell to be initialized before registering the tray icon.
|
||||
// This ensures the tray icon works reliably after a logoff/logon cycle.
|
||||
while (GetShellWindow() == nullptr) {
|
||||
Sleep(1000);
|
||||
}
|
||||
#endif
|
||||
|
||||
if (tray_init(&tray) < 0) {
|
||||
BOOST_LOG(warning) << "Failed to create system tray"sv;
|
||||
return 1;
|
||||
}
|
||||
else {
|
||||
BOOST_LOG(info) << "System tray created"sv;
|
||||
}
|
||||
|
||||
tray_initialized = true;
|
||||
while (tray_loop(1) == 0) {
|
||||
BOOST_LOG(debug) << "System tray loop"sv;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Run the system tray with platform specific options.
|
||||
* @note macOS requires that UI elements be created on the main thread, so the system tray is not currently implemented for macOS.
|
||||
*/
|
||||
void
|
||||
run_tray() {
|
||||
// create the system tray
|
||||
#if defined(__APPLE__) || defined(__MACH__)
|
||||
// macOS requires that UI elements be created on the main thread
|
||||
// creating tray using dispatch queue does not work, although the code doesn't actually throw any (visible) errors
|
||||
|
||||
// dispatch_async(dispatch_get_main_queue(), ^{
|
||||
// system_tray();
|
||||
// });
|
||||
|
||||
BOOST_LOG(info) << "system_tray() is not yet implemented for this platform."sv;
|
||||
#else // Windows, Linux
|
||||
// create tray in separate thread
|
||||
std::thread tray_thread(system_tray);
|
||||
tray_thread.detach();
|
||||
#endif
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Exit the system tray.
|
||||
* @return 0 after exiting the system tray.
|
||||
*/
|
||||
int
|
||||
end_tray() {
|
||||
tray_initialized = false;
|
||||
tray_exit();
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Sets the tray icon in playing mode and spawns the appropriate notification
|
||||
* @param app_name The started application name
|
||||
*/
|
||||
void
|
||||
update_tray_playing(std::string app_name) {
|
||||
if (!tray_initialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
tray.notification_title = NULL;
|
||||
tray.notification_text = NULL;
|
||||
tray.notification_cb = NULL;
|
||||
tray.notification_icon = NULL;
|
||||
tray.icon = TRAY_ICON_PLAYING;
|
||||
tray_update(&tray);
|
||||
tray.icon = TRAY_ICON_PLAYING;
|
||||
tray.notification_title = "Stream Started";
|
||||
char msg[256];
|
||||
snprintf(msg, std::size(msg), "Streaming started for %s", app_name.c_str());
|
||||
tray.notification_text = msg;
|
||||
tray.tooltip = msg;
|
||||
tray.notification_icon = TRAY_ICON_PLAYING;
|
||||
tray_update(&tray);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Sets the tray icon in pausing mode (stream stopped but app running) and spawns the appropriate notification
|
||||
* @param app_name The paused application name
|
||||
*/
|
||||
void
|
||||
update_tray_pausing(std::string app_name) {
|
||||
if (!tray_initialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
tray.notification_title = NULL;
|
||||
tray.notification_text = NULL;
|
||||
tray.notification_cb = NULL;
|
||||
tray.notification_icon = NULL;
|
||||
tray.icon = TRAY_ICON_PAUSING;
|
||||
tray_update(&tray);
|
||||
char msg[256];
|
||||
snprintf(msg, std::size(msg), "Streaming paused for %s", app_name.c_str());
|
||||
tray.icon = TRAY_ICON_PAUSING;
|
||||
tray.notification_title = "Stream Paused";
|
||||
tray.notification_text = msg;
|
||||
tray.tooltip = msg;
|
||||
tray.notification_icon = TRAY_ICON_PAUSING;
|
||||
tray_update(&tray);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Sets the tray icon in stopped mode (app and stream stopped) and spawns the appropriate notification
|
||||
* @param app_name The started application name
|
||||
*/
|
||||
void
|
||||
update_tray_stopped(std::string app_name) {
|
||||
if (!tray_initialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
tray.notification_title = NULL;
|
||||
tray.notification_text = NULL;
|
||||
tray.notification_cb = NULL;
|
||||
tray.notification_icon = NULL;
|
||||
tray.icon = TRAY_ICON;
|
||||
tray_update(&tray);
|
||||
char msg[256];
|
||||
snprintf(msg, std::size(msg), "Application %s successfully stopped", app_name.c_str());
|
||||
tray.icon = TRAY_ICON;
|
||||
tray.notification_icon = TRAY_ICON;
|
||||
tray.notification_title = "Application Stopped";
|
||||
tray.notification_text = msg;
|
||||
tray.tooltip = PROJECT_NAME;
|
||||
tray_update(&tray);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Spawns a notification for PIN Pairing. Clicking it opens the PIN Web UI Page
|
||||
*/
|
||||
void
|
||||
update_tray_require_pin() {
|
||||
if (!tray_initialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
tray.notification_title = NULL;
|
||||
tray.notification_text = NULL;
|
||||
tray.notification_cb = NULL;
|
||||
tray.notification_icon = NULL;
|
||||
tray.icon = TRAY_ICON;
|
||||
tray_update(&tray);
|
||||
tray.icon = TRAY_ICON;
|
||||
tray.notification_title = "Incoming Pairing Request";
|
||||
tray.notification_text = "Click here to complete the pairing process";
|
||||
tray.notification_icon = TRAY_ICON_LOCKED;
|
||||
tray.tooltip = PROJECT_NAME;
|
||||
tray.notification_cb = []() {
|
||||
launch_ui_with_path("/pin");
|
||||
};
|
||||
tray_update(&tray);
|
||||
}
|
||||
|
||||
} // namespace system_tray
|
||||
#endif
|
||||
@ -1,39 +0,0 @@
|
||||
/**
|
||||
* @file src/system_tray.h
|
||||
* @brief todo
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
// system_tray namespace
|
||||
namespace system_tray {
|
||||
|
||||
void
|
||||
tray_open_ui_cb(struct tray_menu *item);
|
||||
void
|
||||
tray_donate_github_cb(struct tray_menu *item);
|
||||
void
|
||||
tray_donate_mee6_cb(struct tray_menu *item);
|
||||
void
|
||||
tray_donate_patreon_cb(struct tray_menu *item);
|
||||
void
|
||||
tray_donate_paypal_cb(struct tray_menu *item);
|
||||
void
|
||||
tray_quit_cb(struct tray_menu *item);
|
||||
|
||||
int
|
||||
system_tray();
|
||||
int
|
||||
run_tray();
|
||||
int
|
||||
end_tray();
|
||||
void
|
||||
update_tray_playing(std::string app_name);
|
||||
void
|
||||
update_tray_pausing(std::string app_name);
|
||||
void
|
||||
update_tray_stopped(std::string app_name);
|
||||
void
|
||||
update_tray_require_pin();
|
||||
|
||||
} // namespace system_tray
|
||||
384
src/upnp.cpp
384
src/upnp.cpp
@ -1,384 +0,0 @@
|
||||
/**
|
||||
* @file src/upnp.cpp
|
||||
* @brief todo
|
||||
*/
|
||||
#include <miniupnpc/miniupnpc.h>
|
||||
#include <miniupnpc/upnpcommands.h>
|
||||
|
||||
#include "config.h"
|
||||
#include "confighttp.h"
|
||||
#include "globals.h"
|
||||
#include "logging.h"
|
||||
#include "network.h"
|
||||
#include "nvhttp.h"
|
||||
#include "rtsp.h"
|
||||
#include "stream.h"
|
||||
#include "upnp.h"
|
||||
#include "utility.h"
|
||||
|
||||
using namespace std::literals;
|
||||
|
||||
namespace upnp {
|
||||
constexpr auto INET6_ADDRESS_STRLEN = 46;
|
||||
|
||||
constexpr auto PORT_MAPPING_LIFETIME = 3600s;
|
||||
constexpr auto REFRESH_INTERVAL = 120s;
|
||||
|
||||
constexpr auto IPv4 = 0;
|
||||
constexpr auto IPv6 = 1;
|
||||
|
||||
using device_t = util::safe_ptr<UPNPDev, freeUPNPDevlist>;
|
||||
|
||||
KITTY_USING_MOVE_T(urls_t, UPNPUrls, , {
|
||||
FreeUPNPUrls(&el);
|
||||
});
|
||||
|
||||
struct mapping_t {
|
||||
struct {
|
||||
std::string wan;
|
||||
std::string lan;
|
||||
std::string proto;
|
||||
} port;
|
||||
|
||||
std::string description;
|
||||
};
|
||||
|
||||
static std::string_view
|
||||
status_string(int status) {
|
||||
switch (status) {
|
||||
case 0:
|
||||
return "No IGD device found"sv;
|
||||
case 1:
|
||||
return "Valid IGD device found"sv;
|
||||
case 2:
|
||||
return "Valid IGD device found, but it isn't connected"sv;
|
||||
case 3:
|
||||
return "A UPnP device has been found, but it wasn't recognized as an IGD"sv;
|
||||
}
|
||||
|
||||
return "Unknown status"sv;
|
||||
}
|
||||
|
||||
class deinit_t: public platf::deinit_t {
|
||||
public:
|
||||
deinit_t() {
|
||||
auto rtsp = std::to_string(net::map_port(rtsp_stream::RTSP_SETUP_PORT));
|
||||
auto video = std::to_string(net::map_port(stream::VIDEO_STREAM_PORT));
|
||||
auto audio = std::to_string(net::map_port(stream::AUDIO_STREAM_PORT));
|
||||
auto control = std::to_string(net::map_port(stream::CONTROL_PORT));
|
||||
auto gs_http = std::to_string(net::map_port(nvhttp::PORT_HTTP));
|
||||
auto gs_https = std::to_string(net::map_port(nvhttp::PORT_HTTPS));
|
||||
auto wm_http = std::to_string(net::map_port(confighttp::PORT_HTTPS));
|
||||
|
||||
mappings.assign({
|
||||
{ { rtsp, rtsp, "TCP"s }, "Sunshine - RTSP"s },
|
||||
{ { video, video, "UDP"s }, "Sunshine - Video"s },
|
||||
{ { audio, audio, "UDP"s }, "Sunshine - Audio"s },
|
||||
{ { control, control, "UDP"s }, "Sunshine - Control"s },
|
||||
{ { gs_http, gs_http, "TCP"s }, "Sunshine - Client HTTP"s },
|
||||
{ { gs_https, gs_https, "TCP"s }, "Sunshine - Client HTTPS"s },
|
||||
});
|
||||
|
||||
// Only map port for the Web Manager if it is configured to accept connection from WAN
|
||||
if (net::from_enum_string(config::nvhttp.origin_web_ui_allowed) > net::LAN) {
|
||||
mappings.emplace_back(mapping_t { { wm_http, wm_http, "TCP"s }, "Sunshine - Web UI"s });
|
||||
}
|
||||
|
||||
// Start the mapping thread
|
||||
upnp_thread = std::thread { &deinit_t::upnp_thread_proc, this };
|
||||
}
|
||||
|
||||
~deinit_t() {
|
||||
upnp_thread.join();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Opens pinholes for IPv6 traffic if the IGD is capable.
|
||||
* @details Not many IGDs support this feature, so we perform error logging with debug level.
|
||||
* @return true if the pinholes were opened successfully.
|
||||
*/
|
||||
bool
|
||||
create_ipv6_pinholes() {
|
||||
int err;
|
||||
device_t device { upnpDiscover(2000, nullptr, nullptr, 0, IPv6, 2, &err) };
|
||||
if (!device || err) {
|
||||
BOOST_LOG(debug) << "Couldn't discover any IPv6 UPNP devices"sv;
|
||||
return false;
|
||||
}
|
||||
|
||||
IGDdatas data;
|
||||
urls_t urls;
|
||||
std::array<char, INET6_ADDRESS_STRLEN> lan_addr;
|
||||
auto status = UPNP_GetValidIGD(device.get(), &urls.el, &data, lan_addr.data(), lan_addr.size());
|
||||
if (status != 1 && status != 2) {
|
||||
BOOST_LOG(debug) << "No valid IPv6 IGD: "sv << status_string(status);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (data.IPv6FC.controlurl[0] != 0) {
|
||||
int firewallEnabled, pinholeAllowed;
|
||||
|
||||
// Check if this firewall supports IPv6 pinholes
|
||||
err = UPNP_GetFirewallStatus(urls->controlURL_6FC, data.IPv6FC.servicetype, &firewallEnabled, &pinholeAllowed);
|
||||
if (err == UPNPCOMMAND_SUCCESS) {
|
||||
BOOST_LOG(debug) << "UPnP IPv6 firewall control available. Firewall is "sv
|
||||
<< (firewallEnabled ? "enabled"sv : "disabled"sv)
|
||||
<< ", pinhole is "sv
|
||||
<< (pinholeAllowed ? "allowed"sv : "disallowed"sv);
|
||||
|
||||
if (pinholeAllowed) {
|
||||
// Create pinholes for each port
|
||||
auto mapping_period = std::to_string(PORT_MAPPING_LIFETIME.count());
|
||||
auto shutdown_event = mail::man->event<bool>(mail::shutdown);
|
||||
|
||||
for (auto it = std::begin(mappings); it != std::end(mappings) && !shutdown_event->peek(); ++it) {
|
||||
auto mapping = *it;
|
||||
char uniqueId[8];
|
||||
|
||||
// Open a pinhole for the LAN port, since there will be no WAN->LAN port mapping on IPv6
|
||||
err = UPNP_AddPinhole(urls->controlURL_6FC,
|
||||
data.IPv6FC.servicetype,
|
||||
"", "0",
|
||||
lan_addr.data(),
|
||||
mapping.port.lan.c_str(),
|
||||
mapping.port.proto.c_str(),
|
||||
mapping_period.c_str(),
|
||||
uniqueId);
|
||||
if (err == UPNPCOMMAND_SUCCESS) {
|
||||
BOOST_LOG(debug) << "Successfully created pinhole for "sv << mapping.port.proto << ' ' << mapping.port.lan;
|
||||
}
|
||||
else {
|
||||
BOOST_LOG(debug) << "Failed to create pinhole for "sv << mapping.port.proto << ' ' << mapping.port.lan << ": "sv << err;
|
||||
}
|
||||
}
|
||||
|
||||
return err == 0;
|
||||
}
|
||||
else {
|
||||
BOOST_LOG(debug) << "IPv6 pinholes are not allowed by the IGD"sv;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
else {
|
||||
BOOST_LOG(debug) << "Failed to get IPv6 firewall status: "sv << err;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
else {
|
||||
BOOST_LOG(debug) << "IPv6 Firewall Control is not supported by the IGD"sv;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Maps a port via UPnP.
|
||||
* @param data IGDdatas from UPNP_GetValidIGD()
|
||||
* @param urls urls_t from UPNP_GetValidIGD()
|
||||
* @param lan_addr Local IP address to map to
|
||||
* @param mapping Information about port to map
|
||||
* @return `true` on success.
|
||||
*/
|
||||
bool
|
||||
map_upnp_port(const IGDdatas &data, const urls_t &urls, const std::string &lan_addr, const mapping_t &mapping) {
|
||||
char intClient[16];
|
||||
char intPort[6];
|
||||
char desc[80];
|
||||
char enabled[4];
|
||||
char leaseDuration[16];
|
||||
bool indefinite = false;
|
||||
|
||||
// First check if this port is already mapped successfully
|
||||
BOOST_LOG(debug) << "Checking for existing UPnP port mapping for "sv << mapping.port.wan;
|
||||
auto err = UPNP_GetSpecificPortMappingEntry(
|
||||
urls->controlURL,
|
||||
data.first.servicetype,
|
||||
// In params
|
||||
mapping.port.wan.c_str(),
|
||||
mapping.port.proto.c_str(),
|
||||
nullptr,
|
||||
// Out params
|
||||
intClient, intPort, desc, enabled, leaseDuration);
|
||||
if (err == 714) { // NoSuchEntryInArray
|
||||
BOOST_LOG(debug) << "Mapping entry not found for "sv << mapping.port.wan;
|
||||
}
|
||||
else if (err == UPNPCOMMAND_SUCCESS) {
|
||||
// Some routers change the description, so we can't check that here
|
||||
if (!std::strcmp(intClient, lan_addr.c_str())) {
|
||||
if (std::atoi(leaseDuration) == 0) {
|
||||
BOOST_LOG(debug) << "Static mapping entry found for "sv << mapping.port.wan;
|
||||
|
||||
// It's a static mapping, so we're done here
|
||||
return true;
|
||||
}
|
||||
else {
|
||||
BOOST_LOG(debug) << "Mapping entry found for "sv << mapping.port.wan << " ("sv << leaseDuration << " seconds remaining)"sv;
|
||||
}
|
||||
}
|
||||
else {
|
||||
BOOST_LOG(warning) << "UPnP conflict detected with: "sv << intClient;
|
||||
|
||||
// Some UPnP IGDs won't let unauthenticated clients delete other conflicting port mappings
|
||||
// for security reasons, but we will give it a try anyway.
|
||||
err = UPNP_DeletePortMapping(
|
||||
urls->controlURL,
|
||||
data.first.servicetype,
|
||||
mapping.port.wan.c_str(),
|
||||
mapping.port.proto.c_str(),
|
||||
nullptr);
|
||||
if (err) {
|
||||
BOOST_LOG(error) << "Unable to delete conflicting UPnP port mapping: "sv << err;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
BOOST_LOG(error) << "UPNP_GetSpecificPortMappingEntry() failed: "sv << err;
|
||||
|
||||
// If we get a strange error from the router, we'll assume it's some old broken IGDv1
|
||||
// device and only use indefinite lease durations to hopefully avoid confusing it.
|
||||
if (err != 606) { // Unauthorized
|
||||
indefinite = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Add/update the port mapping
|
||||
auto mapping_period = std::to_string(indefinite ? 0 : PORT_MAPPING_LIFETIME.count());
|
||||
err = UPNP_AddPortMapping(
|
||||
urls->controlURL,
|
||||
data.first.servicetype,
|
||||
mapping.port.wan.c_str(),
|
||||
mapping.port.lan.c_str(),
|
||||
lan_addr.data(),
|
||||
mapping.description.c_str(),
|
||||
mapping.port.proto.c_str(),
|
||||
nullptr,
|
||||
mapping_period.c_str());
|
||||
|
||||
if (err != UPNPCOMMAND_SUCCESS && !indefinite) {
|
||||
// This may be an old/broken IGD that doesn't like non-static mappings.
|
||||
BOOST_LOG(debug) << "Trying static mapping after failure: "sv << err;
|
||||
err = UPNP_AddPortMapping(
|
||||
urls->controlURL,
|
||||
data.first.servicetype,
|
||||
mapping.port.wan.c_str(),
|
||||
mapping.port.lan.c_str(),
|
||||
lan_addr.data(),
|
||||
mapping.description.c_str(),
|
||||
mapping.port.proto.c_str(),
|
||||
nullptr,
|
||||
"0");
|
||||
}
|
||||
|
||||
if (err) {
|
||||
BOOST_LOG(error) << "Failed to map "sv << mapping.port.proto << ' ' << mapping.port.lan << ": "sv << err;
|
||||
return false;
|
||||
}
|
||||
|
||||
BOOST_LOG(debug) << "Successfully mapped "sv << mapping.port.proto << ' ' << mapping.port.lan;
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Unmaps all ports.
|
||||
* @param data IGDdatas from UPNP_GetValidIGD()
|
||||
* @param data urls_t from UPNP_GetValidIGD()
|
||||
*/
|
||||
void
|
||||
unmap_all_upnp_ports(const urls_t &urls, const IGDdatas &data) {
|
||||
for (auto it = std::begin(mappings); it != std::end(mappings); ++it) {
|
||||
auto status = UPNP_DeletePortMapping(
|
||||
urls->controlURL,
|
||||
data.first.servicetype,
|
||||
it->port.wan.c_str(),
|
||||
it->port.proto.c_str(),
|
||||
nullptr);
|
||||
|
||||
if (status && status != 714) { // NoSuchEntryInArray
|
||||
BOOST_LOG(warning) << "Failed to unmap "sv << it->port.proto << ' ' << it->port.lan << ": "sv << status;
|
||||
}
|
||||
else {
|
||||
BOOST_LOG(debug) << "Successfully unmapped "sv << it->port.proto << ' ' << it->port.lan;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Maintains UPnP port forwarding rules
|
||||
*/
|
||||
void
|
||||
upnp_thread_proc() {
|
||||
auto shutdown_event = mail::man->event<bool>(mail::shutdown);
|
||||
bool mapped = false;
|
||||
IGDdatas data;
|
||||
urls_t mapped_urls;
|
||||
auto address_family = net::af_from_enum_string(config::sunshine.address_family);
|
||||
|
||||
// Refresh UPnP rules every few minutes. They can be lost if the router reboots,
|
||||
// WAN IP address changes, or various other conditions.
|
||||
do {
|
||||
int err = 0;
|
||||
device_t device { upnpDiscover(2000, nullptr, nullptr, 0, IPv4, 2, &err) };
|
||||
if (!device || err) {
|
||||
BOOST_LOG(warning) << "Couldn't discover any IPv4 UPNP devices"sv;
|
||||
mapped = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
for (auto dev = device.get(); dev != nullptr; dev = dev->pNext) {
|
||||
BOOST_LOG(debug) << "Found device: "sv << dev->descURL;
|
||||
}
|
||||
|
||||
std::array<char, INET6_ADDRESS_STRLEN> lan_addr;
|
||||
|
||||
urls_t urls;
|
||||
auto status = UPNP_GetValidIGD(device.get(), &urls.el, &data, lan_addr.data(), lan_addr.size());
|
||||
if (status != 1 && status != 2) {
|
||||
BOOST_LOG(error) << status_string(status);
|
||||
mapped = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
std::string lan_addr_str { lan_addr.data() };
|
||||
|
||||
BOOST_LOG(debug) << "Found valid IGD device: "sv << urls->rootdescURL;
|
||||
|
||||
for (auto it = std::begin(mappings); it != std::end(mappings) && !shutdown_event->peek(); ++it) {
|
||||
map_upnp_port(data, urls, lan_addr_str, *it);
|
||||
}
|
||||
|
||||
if (!mapped) {
|
||||
BOOST_LOG(info) << "Completed UPnP port mappings to "sv << lan_addr_str << " via "sv << urls->rootdescURL;
|
||||
}
|
||||
|
||||
// If we are listening on IPv6 and the IGD has an IPv6 firewall enabled, try to create IPv6 firewall pinholes
|
||||
if (address_family == net::af_e::BOTH) {
|
||||
if (create_ipv6_pinholes() && !mapped) {
|
||||
// Only log the first time through
|
||||
BOOST_LOG(info) << "Successfully opened IPv6 pinholes on the IGD"sv;
|
||||
}
|
||||
}
|
||||
|
||||
mapped = true;
|
||||
mapped_urls = std::move(urls);
|
||||
} while (!shutdown_event->view(REFRESH_INTERVAL));
|
||||
|
||||
if (mapped) {
|
||||
// Unmap ports upon termination
|
||||
BOOST_LOG(info) << "Unmapping UPNP ports..."sv;
|
||||
unmap_all_upnp_ports(mapped_urls, data);
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<mapping_t> mappings;
|
||||
std::thread upnp_thread;
|
||||
};
|
||||
|
||||
std::unique_ptr<platf::deinit_t>
|
||||
start() {
|
||||
if (!config::sunshine.flags[config::flag::UPNP]) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
return std::make_unique<deinit_t>();
|
||||
}
|
||||
} // namespace upnp
|
||||
12
src/upnp.h
12
src/upnp.h
@ -1,12 +0,0 @@
|
||||
/**
|
||||
* @file src/upnp.h
|
||||
* @brief todo
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "platform/common.h"
|
||||
|
||||
namespace upnp {
|
||||
[[nodiscard]] std::unique_ptr<platf::deinit_t>
|
||||
start();
|
||||
}
|
||||
83
src/uuid.h
83
src/uuid.h
@ -1,83 +0,0 @@
|
||||
/**
|
||||
* @file src/uuid.h
|
||||
* @brief todo
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include <random>
|
||||
|
||||
namespace uuid_util {
|
||||
union uuid_t {
|
||||
std::uint8_t b8[16];
|
||||
std::uint16_t b16[8];
|
||||
std::uint32_t b32[4];
|
||||
std::uint64_t b64[2];
|
||||
|
||||
static uuid_t
|
||||
generate(std::default_random_engine &engine) {
|
||||
std::uniform_int_distribution<std::uint8_t> dist(0, std::numeric_limits<std::uint8_t>::max());
|
||||
|
||||
uuid_t buf;
|
||||
for (auto &el : buf.b8) {
|
||||
el = dist(engine);
|
||||
}
|
||||
|
||||
buf.b8[7] &= (std::uint8_t) 0b00101111;
|
||||
buf.b8[9] &= (std::uint8_t) 0b10011111;
|
||||
|
||||
return buf;
|
||||
}
|
||||
|
||||
static uuid_t
|
||||
generate() {
|
||||
std::random_device r;
|
||||
|
||||
std::default_random_engine engine { r() };
|
||||
|
||||
return generate(engine);
|
||||
}
|
||||
|
||||
[[nodiscard]] std::string
|
||||
string() const {
|
||||
std::string result;
|
||||
|
||||
result.reserve(sizeof(uuid_t) * 2 + 4);
|
||||
|
||||
auto hex = util::hex(*this, true);
|
||||
auto hex_view = hex.to_string_view();
|
||||
|
||||
std::string_view slices[] = {
|
||||
hex_view.substr(0, 8),
|
||||
hex_view.substr(8, 4),
|
||||
hex_view.substr(12, 4),
|
||||
hex_view.substr(16, 4)
|
||||
};
|
||||
auto last_slice = hex_view.substr(20, 12);
|
||||
|
||||
for (auto &slice : slices) {
|
||||
std::copy(std::begin(slice), std::end(slice), std::back_inserter(result));
|
||||
|
||||
result.push_back('-');
|
||||
}
|
||||
|
||||
std::copy(std::begin(last_slice), std::end(last_slice), std::back_inserter(result));
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
constexpr bool
|
||||
operator==(const uuid_t &other) const {
|
||||
return b64[0] == other.b64[0] && b64[1] == other.b64[1];
|
||||
}
|
||||
|
||||
constexpr bool
|
||||
operator<(const uuid_t &other) const {
|
||||
return (b64[0] < other.b64[0] || (b64[0] == other.b64[0] && b64[1] < other.b64[1]));
|
||||
}
|
||||
|
||||
constexpr bool
|
||||
operator>(const uuid_t &other) const {
|
||||
return (b64[0] > other.b64[0] || (b64[0] == other.b64[0] && b64[1] > other.b64[1]));
|
||||
}
|
||||
};
|
||||
} // namespace uuid_util
|
||||
1
third-party/Simple-Web-Server
vendored
1
third-party/Simple-Web-Server
vendored
@ -1 +0,0 @@
|
||||
Subproject commit 27b41f5ee154cca0fce4fe2955dd886d04e3a4ed
|
||||
1
third-party/googletest
vendored
1
third-party/googletest
vendored
@ -1 +0,0 @@
|
||||
Subproject commit f8d7d77c06936315286eb55f8de22cd23c188571
|
||||
1
third-party/nanors
vendored
1
third-party/nanors
vendored
@ -1 +0,0 @@
|
||||
Subproject commit e9e242e98e27037830490b2a752895ca68f75f8b
|
||||
1
third-party/tray
vendored
1
third-party/tray
vendored
@ -1 +0,0 @@
|
||||
Subproject commit 4d8b798cafdd11285af9409c16b5f792968e0045
|
||||
Loading…
Reference in New Issue
Block a user