remove modules

This commit is contained in:
pigeatgarlic 2024-04-03 14:26:17 -07:00
parent 5d79fdbdfa
commit 70a00f2a58
43 changed files with 14 additions and 10971 deletions

32
.gitmodules vendored
View File

@ -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

View File

@ -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}

View File

@ -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"

View File

@ -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()

View File

@ -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()

View File

@ -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)

View File

@ -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 ")

View File

@ -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

View File

@ -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

View File

@ -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" },
};

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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;

View File

@ -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

View File

@ -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

View File

@ -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;
}

View File

@ -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

View File

@ -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

File diff suppressed because it is too large Load Diff

View File

@ -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

View File

@ -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);
}
/**

View File

@ -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

View File

@ -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);
}
/**

View File

@ -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

View File

@ -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 {

View File

@ -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

View File

@ -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

View File

@ -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

File diff suppressed because it is too large Load Diff

View File

@ -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

File diff suppressed because it is too large Load Diff

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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();
}

View File

@ -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 +0,0 @@
Subproject commit 27b41f5ee154cca0fce4fe2955dd886d04e3a4ed

@ -1 +0,0 @@
Subproject commit f8d7d77c06936315286eb55f8de22cd23c188571

1
third-party/nanors vendored

@ -1 +0,0 @@
Subproject commit e9e242e98e27037830490b2a752895ca68f75f8b

1
third-party/tray vendored

@ -1 +0,0 @@
Subproject commit 4d8b798cafdd11285af9409c16b5f792968e0045