This commit is contained in:
pigeatgarlic 2023-10-26 16:35:33 +07:00
parent 172ee1cf9d
commit f1d509adef
17 changed files with 80 additions and 2285 deletions

View File

@ -38,8 +38,6 @@ set(SUNSHINE_TARGET_FILES
third-party/moonlight-common-c/src/RtspParser.c
third-party/moonlight-common-c/src/Video.h
third-party/tray/tray.h
src/upnp.cpp
src/upnp.h
src/cbs.cpp
src/utility.h
src/uuid.h
@ -53,8 +51,6 @@ set(SUNSHINE_TARGET_FILES
src/nvhttp.h
src/httpcommon.cpp
src/httpcommon.h
src/confighttp.cpp
src/confighttp.h
src/rtsp.cpp
src/rtsp.h
src/stream.cpp
@ -73,8 +69,6 @@ set(SUNSHINE_TARGET_FILES
src/network.cpp
src/network.h
src/move_by_copy.h
src/system_tray.cpp
src/system_tray.h
src/task_pool.h
src/thread_pool.h
src/thread_safe.h
@ -84,8 +78,6 @@ set(SUNSHINE_TARGET_FILES
src/stat_trackers.cpp
${PLATFORM_TARGET_FILES})
set_source_files_properties(src/upnp.cpp PROPERTIES COMPILE_FLAGS -Wno-pedantic)
set_source_files_properties(third-party/nanors/rs.c
PROPERTIES COMPILE_FLAGS "-include deps/obl/autoshim.h -ftree-vectorize")

View File

@ -1211,50 +1211,7 @@ namespace config {
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;
}

View File

@ -1,791 +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 "httpcommon.h"
#include "main.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 header = read_file(WEB_DIR "header.html");
std::string content = read_file(WEB_DIR "index.html");
SimpleWeb::CaseInsensitiveMultimap headers;
headers.emplace("Content-Type", "text/html; charset=utf-8");
response->write(header + content, headers);
}
void
getPinPage(resp_https_t response, req_https_t request) {
if (!authenticate(response, request)) return;
print_req(request);
std::string header = read_file(WEB_DIR "header.html");
std::string content = read_file(WEB_DIR "pin.html");
SimpleWeb::CaseInsensitiveMultimap headers;
headers.emplace("Content-Type", "text/html; charset=utf-8");
response->write(header + content, headers);
}
void
getAppsPage(resp_https_t response, req_https_t request) {
if (!authenticate(response, request)) return;
print_req(request);
std::string header = read_file(WEB_DIR "header.html");
std::string content = 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(header + content, headers);
}
void
getClientsPage(resp_https_t response, req_https_t request) {
if (!authenticate(response, request)) return;
print_req(request);
std::string header = read_file(WEB_DIR "header.html");
std::string content = read_file(WEB_DIR "clients.html");
SimpleWeb::CaseInsensitiveMultimap headers;
headers.emplace("Content-Type", "text/html; charset=utf-8");
response->write(header + content, headers);
}
void
getConfigPage(resp_https_t response, req_https_t request) {
if (!authenticate(response, request)) return;
print_req(request);
std::string header = read_file(WEB_DIR "header.html");
std::string content = read_file(WEB_DIR "config.html");
SimpleWeb::CaseInsensitiveMultimap headers;
headers.emplace("Content-Type", "text/html; charset=utf-8");
response->write(header + content, headers);
}
void
getPasswordPage(resp_https_t response, req_https_t request) {
if (!authenticate(response, request)) return;
print_req(request);
std::string header = read_file(WEB_DIR "header.html");
std::string content = read_file(WEB_DIR "password.html");
SimpleWeb::CaseInsensitiveMultimap headers;
headers.emplace("Content-Type", "text/html; charset=utf-8");
response->write(header + 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 header = read_file(WEB_DIR "header-no-nav.html");
std::string content = read_file(WEB_DIR "welcome.html");
SimpleWeb::CaseInsensitiveMultimap headers;
headers.emplace("Content-Type", "text/html; charset=utf-8");
response->write(header + content, headers);
}
void
getTroubleshootingPage(resp_https_t response, req_https_t request) {
if (!authenticate(response, request)) return;
print_req(request);
std::string header = read_file(WEB_DIR "header.html");
std::string content = read_file(WEB_DIR "troubleshooting.html");
SimpleWeb::CaseInsensitiveMultimap headers;
headers.emplace("Content-Type", "text/html; charset=utf-8");
response->write(header + 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 / "node_modules");
// .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 node_modules directory
if (!isChildPath(filePath, nodeModulesPath)) {
BOOST_LOG(warning) << "Someone requested a path " << filePath << " that is outside the node_modules 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 = 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 = 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(read_file(config::sunshine.config_file.c_str()));
for (auto &[name, value] : vars) {
outputTree.put(std::move(name), std::move(value));
}
}
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;
}
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 = 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/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["^/node_modules\\/.+$"]["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

@ -19,18 +19,13 @@
// local includes
#include "config.h"
#include "confighttp.h"
#include "httpcommon.h"
#include "main.h"
#include "nvhttp.h"
#include "platform/common.h"
#include "process.h"
#include "rtsp.h"
#include "system_tray.h"
#include "thread_pool.h"
#include "upnp.h"
#include "version.h"
#include "video.h"
#include "stream.h"
extern "C" {
#include <libavutil/log.h>
@ -43,6 +38,8 @@ extern "C" {
safe::mail_t mail::man;
namespace asio = boost::asio;
using asio::ip::udp;
using namespace std::literals;
namespace bl = boost::log;
@ -133,276 +130,7 @@ namespace restore_nvprefs_undo {
} // namespace restore_nvprefs_undo
#endif
namespace lifetime {
static char **argv;
static 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 Gets the argv array passed to main().
*/
char **
get_argv() {
return argv;
}
} // namespace lifetime
#ifdef _WIN32
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(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
/**
* @brief Checks if NVIDIA's GameStream software is running.
* @return `true` if GameStream is enabled.
*/
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;
}
#endif
/**
* @brief Launch the Web UI.
*
* EXAMPLES:
* ```cpp
* launch_ui();
* ```
*/
void
launch_ui() {
std::string url = "https://localhost:" + std::to_string(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(map_port(confighttp::PORT_HTTPS)) + path;
platf::open_url(url);
}
/**
* @brief Flush the log.
@ -431,51 +159,37 @@ on_signal(int sig, FN &&fn) {
std::signal(sig, on_signal_forwarder);
}
namespace gen_creds {
int
entry(const char *name, int argc, char *argv[]) {
if (argc < 2 || argv[0] == "help"sv || argv[1] == "help"sv) {
print_help(name);
return 0;
/**
*
*/
void
videoBroadcastThreadmain() {
auto shutdown_event = mail::man->event<bool>(mail::broadcast_shutdown);
auto packets = mail::man->queue<video::packet_t>(mail::video_packets);
auto timebase = boost::posix_time::microsec_clock::universal_time();
// Video traffic is sent on this thread
platf::adjust_thread_priority(platf::thread_priority_e::high);
// IMPORTANT
while (auto packet = packets->pop()) {
if (shutdown_event->peek()) {
break;
}
http::save_user_creds(config::sunshine.credentials_file, argv[0], argv[1]);
return 0;
BOOST_LOG(warning) << "Packet size: "sv << packet.get()->data_size();
BOOST_LOG(warning) << "Packet val: "sv << packet.get()->data();
}
} // namespace gen_creds
std::map<std::string_view, std::function<int(const char *name, int argc, char **argv)>> cmd_to_func {
{ "creds"sv, gen_creds::entry },
{ "help"sv, help::entry },
{ "version"sv, version::entry },
#ifdef _WIN32
{ "restore-nvprefs-undo"sv, restore_nvprefs_undo::entry },
#endif
};
#ifdef _WIN32
LRESULT CALLBACK
SessionMonitorWindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {
switch (uMsg) {
case WM_CLOSE:
DestroyWindow(hwnd);
return 0;
case WM_DESTROY:
PostQuitMessage(0);
return 0;
case WM_ENDSESSION: {
// Terminate ourselves with a blocking exit call
std::cout << "Received WM_ENDSESSION"sv << std::endl;
lifetime::exit_sunshine(0, false);
return 0;
}
default:
return DefWindowProc(hwnd, uMsg, wParam, lParam);
}
shutdown_event->raise(true);
}
#endif
/**
* @brief Main application entry point.
* @param argc The number of arguments.
@ -488,8 +202,6 @@ SessionMonitorWindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {
*/
int
main(int argc, char *argv[]) {
lifetime::argv = argv;
task_pool_util::TaskPool::task_id_t force_shutdown = nullptr;
#ifdef _WIN32
@ -586,22 +298,6 @@ main(int argc, char *argv[]) {
bl::core::get()->add_sink(sink);
auto fg = util::fail_guard(log_flush);
if (!config::sunshine.cmd.name.empty()) {
auto fn = cmd_to_func.find(config::sunshine.cmd.name);
if (fn == std::end(cmd_to_func)) {
BOOST_LOG(fatal) << "Unknown command: "sv << config::sunshine.cmd.name;
BOOST_LOG(info) << "Possible commands:"sv;
for (auto &[key, _] : cmd_to_func) {
BOOST_LOG(info) << '\t' << key;
}
return 7;
}
return fn->second(argv[0], config::sunshine.cmd.argc, config::sunshine.cmd.argv);
}
#ifdef WIN32
// Modify relevant NVIDIA control panel settings if the system has corresponding gpu
if (nvprefs_instance.load()) {
@ -617,86 +313,11 @@ main(int argc, char *argv[]) {
// Wait as long as possible to terminate Sunshine.exe during logoff/shutdown
SetProcessShutdownParameters(0x100, SHUTDOWN_NORETRY);
// We must create a hidden window to receive shutdown notifications since we load gdi32.dll
std::promise<HWND> session_monitor_hwnd_promise;
auto session_monitor_hwnd_future = session_monitor_hwnd_promise.get_future();
std::promise<void> session_monitor_join_thread_promise;
auto session_monitor_join_thread_future = session_monitor_join_thread_promise.get_future();
std::thread session_monitor_thread([&]() {
session_monitor_join_thread_promise.set_value_at_thread_exit();
WNDCLASSA wnd_class {};
wnd_class.lpszClassName = "SunshineSessionMonitorClass";
wnd_class.lpfnWndProc = SessionMonitorWindowProc;
if (!RegisterClassA(&wnd_class)) {
session_monitor_hwnd_promise.set_value(NULL);
BOOST_LOG(error) << "Failed to register session monitor window class"sv << std::endl;
return;
}
auto wnd = CreateWindowExA(
0,
wnd_class.lpszClassName,
"Sunshine Session Monitor Window",
0,
CW_USEDEFAULT,
CW_USEDEFAULT,
CW_USEDEFAULT,
CW_USEDEFAULT,
nullptr,
nullptr,
nullptr,
nullptr);
session_monitor_hwnd_promise.set_value(wnd);
if (!wnd) {
BOOST_LOG(error) << "Failed to create session monitor window"sv << std::endl;
return;
}
ShowWindow(wnd, SW_HIDE);
// Run the message loop for our window
MSG msg {};
while (GetMessage(&msg, nullptr, 0, 0) > 0) {
TranslateMessage(&msg);
DispatchMessage(&msg);
}
});
auto session_monitor_join_thread_guard = util::fail_guard([&]() {
if (session_monitor_hwnd_future.wait_for(1s) == std::future_status::ready) {
if (HWND session_monitor_hwnd = session_monitor_hwnd_future.get()) {
PostMessage(session_monitor_hwnd, WM_CLOSE, 0, 0);
}
if (session_monitor_join_thread_future.wait_for(1s) == std::future_status::ready) {
session_monitor_thread.join();
return;
}
else {
BOOST_LOG(warning) << "session_monitor_join_thread_future reached timeout";
}
}
else {
BOOST_LOG(warning) << "session_monitor_hwnd_future reached timeout";
}
session_monitor_thread.detach();
});
#endif
BOOST_LOG(info) << PROJECT_NAME << " version: " << PROJECT_VER << std::endl;
task_pool.start(1);
#if defined SUNSHINE_TRAY && SUNSHINE_TRAY >= 1
// create tray thread and detach it
system_tray::run_tray();
#endif
// Create signal handler after logging has been initialized
auto shutdown_event = mail::man->event<bool>(mail::shutdown);
@ -726,7 +347,6 @@ main(int argc, char *argv[]) {
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.
@ -736,66 +356,30 @@ 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;
}
BOOST_LOG(error) << "Hello from thinkmay."sv;
reed_solomon_init();
auto input_deinit_guard = input::init();
if (video::probe_encoders()) {
BOOST_LOG(error) << "Video failed to find working encoder"sv;
}
if (http::init()) {
BOOST_LOG(fatal) << "HTTP interface failed to initialize"sv;
#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;
}
std::unique_ptr<platf::deinit_t> mDNS;
auto sync_mDNS = std::async(std::launch::async, [&mDNS]() {
mDNS = platf::publish::start();
});
auto video = std::thread { videoBroadcastThreadmain };
std::unique_ptr<platf::deinit_t> upnp_unmap;
auto sync_upnp = std::async(std::launch::async, [&upnp_unmap]() {
upnp_unmap = upnp::start();
});
stream::config_t config;
config.monitor = { 1920, 1080, 60, 1000, 1, 0, 1, 0, 0 };
auto gcm_key = util::from_hex<crypto::aes_t>(std::string("localhost"), true);
auto iv = util::from_hex<crypto::aes_t>(std::string("localhost"), true);
auto session = stream::session::alloc(config, gcm_key, iv);
auto capture = std::thread { stream::videoThread, &(*session) };
// FIXME: Temporary workaround: Simple-Web_server needs to be updated or replaced
if (shutdown_event->peek()) {
return lifetime::desired_exit_code;
}
std::thread httpThread { nvhttp::start };
std::thread configThread { confighttp::start };
#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
rtsp_stream::rtpThread();
httpThread.join();
configThread.join();
shutdown_event->view();
task_pool.stop();
task_pool.join();
// stop system tray
#if defined SUNSHINE_TRAY && SUNSHINE_TRAY >= 1
system_tray::end_tray();
#endif
#ifdef WIN32
// Restore global NVIDIA control panel settings
@ -805,7 +389,7 @@ main(int argc, char *argv[]) {
}
#endif
return lifetime::desired_exit_code;
return 0;
}
/**

View File

@ -75,24 +75,4 @@ namespace mail {
MAIL(hdr);
#undef MAIL
} // namespace mail
namespace lifetime {
void
exit_sunshine(int exit_code, bool async);
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
} // namespace mail

View File

@ -29,7 +29,6 @@
#include "platform/common.h"
#include "process.h"
#include "rtsp.h"
#include "system_tray.h"
#include "utility.h"
#include "uuid.h"
#include "video.h"
@ -517,9 +516,6 @@ namespace nvhttp {
getservercert(ptr->second, tree, pin);
}
else {
#if defined SUNSHINE_TRAY && SUNSHINE_TRAY >= 1
system_tray::update_tray_require_pin();
#endif
ptr->second.async_insert_pin.response = std::move(response);
fg.disable();
@ -717,8 +713,8 @@ namespace nvhttp {
}
void
launch(bool &host_audio, resp_https_t response, req_https_t request) {
print_req<SimpleWeb::HTTPS>(request);
launch(bool &host_audio, resp_http_t response, req_http_t request) {
print_req<SimpleWeb::HTTP>(request);
pt::ptree tree;
auto g = util::fail_guard([&]() {
@ -797,8 +793,8 @@ namespace nvhttp {
}
void
resume(bool &host_audio, resp_https_t response, req_https_t request) {
print_req<SimpleWeb::HTTPS>(request);
resume(bool &host_audio, resp_http_t response, req_http_t request) {
print_req<SimpleWeb::HTTP>(request);
pt::ptree tree;
auto g = util::fail_guard([&]() {
@ -868,8 +864,8 @@ namespace nvhttp {
}
void
cancel(resp_https_t response, req_https_t request) {
print_req<SimpleWeb::HTTPS>(request);
cancel(resp_http_t response, req_http_t request) {
print_req<SimpleWeb::HTTP>(request);
pt::ptree tree;
auto g = util::fail_guard([&]() {
@ -922,6 +918,7 @@ namespace nvhttp {
*/
void
start() {
// char* secret = "";
auto shutdown_event = mail::man->event<bool>(mail::shutdown);
auto port_http = map_port(PORT_HTTP);
@ -950,80 +947,13 @@ namespace nvhttp {
// /launch will store it in host_audio
bool host_audio {};
https_server_t https_server { config::nvhttp.cert, config::nvhttp.pkey };
http_server_t http_server;
// Verify certificates after establishing connection
https_server.verify = [&cert_chain, add_cert](SSL *ssl) {
crypto::x509_t x509 { SSL_get_peer_certificate(ssl) };
if (!x509) {
BOOST_LOG(info) << "unknown -- denied"sv;
return 0;
}
int verified = 0;
auto fg = util::fail_guard([&]() {
char subject_name[256];
X509_NAME_oneline(X509_get_subject_name(x509.get()), subject_name, sizeof(subject_name));
BOOST_LOG(debug) << subject_name << " -- "sv << (verified ? "verified"sv : "denied"sv);
});
while (add_cert->peek()) {
char subject_name[256];
auto cert = add_cert->pop();
X509_NAME_oneline(X509_get_subject_name(cert.get()), subject_name, sizeof(subject_name));
BOOST_LOG(debug) << "Added cert ["sv << subject_name << ']';
cert_chain.add(std::move(cert));
}
auto err_str = cert_chain.verify(x509.get());
if (err_str) {
BOOST_LOG(warning) << "SSL Verification error :: "sv << err_str;
return verified;
}
verified = 1;
return verified;
};
https_server.on_verify_failed = [](resp_https_t resp, req_https_t req) {
pt::ptree tree;
auto g = util::fail_guard([&]() {
std::ostringstream data;
pt::write_xml(data, tree);
resp->write(data.str());
resp->close_connection_after_response = true;
});
tree.put("root.<xmlattr>.status_code"s, 401);
tree.put("root.<xmlattr>.query"s, req->path);
tree.put("root.<xmlattr>.status_message"s, "The client is not authorized. Certificate verification failed."s);
};
https_server.default_resource["GET"] = not_found<SimpleWeb::HTTPS>;
https_server.resource["^/serverinfo$"]["GET"] = serverinfo<SimpleWeb::HTTPS>;
https_server.resource["^/pair$"]["GET"] = [&add_cert](auto resp, auto req) { pair<SimpleWeb::HTTPS>(add_cert, resp, req); };
https_server.resource["^/applist$"]["GET"] = applist;
https_server.resource["^/appasset$"]["GET"] = appasset;
https_server.resource["^/launch$"]["GET"] = [&host_audio](auto resp, auto req) { launch(host_audio, resp, req); };
https_server.resource["^/resume$"]["GET"] = [&host_audio](auto resp, auto req) { resume(host_audio, resp, req); };
https_server.resource["^/cancel$"]["GET"] = cancel;
https_server.config.reuse_address = true;
https_server.config.address = net::af_to_any_address_string(address_family);
https_server.config.port = port_https;
http_server.default_resource["GET"] = not_found<SimpleWeb::HTTP>;
http_server.resource["^/serverinfo$"]["GET"] = serverinfo<SimpleWeb::HTTP>;
http_server.resource["^/pair$"]["GET"] = [&add_cert](auto resp, auto req) { pair<SimpleWeb::HTTP>(add_cert, resp, req); };
http_server.resource["^/launch$"]["GET"] = [&host_audio](auto resp, auto req) { launch(host_audio, resp, req); };
http_server.resource["^/resume$"]["GET"] = [&host_audio](auto resp, auto req) { resume(host_audio, resp, req); };
http_server.resource["^/cancel$"]["GET"] = cancel;
http_server.config.reuse_address = true;
http_server.config.address = net::af_to_any_address_string(address_family);
@ -1044,16 +974,13 @@ namespace nvhttp {
return;
}
};
std::thread ssl { accept_and_run, &https_server };
std::thread tcp { accept_and_run, &http_server };
// Wait for any event
shutdown_event->view();
https_server.stop();
http_server.stop();
ssl.join();
tcp.join();
}

View File

@ -237,19 +237,12 @@ 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);
}
struct sockaddr_in

View File

@ -45,8 +45,20 @@
#define PROC_THREAD_ATTRIBUTE_JOB_LIST ProcThreadAttributeValue(13, FALSE, TRUE, FALSE)
#endif
#ifndef HAS_QOS_FLOWID
typedef UINT32 QOS_FLOWID;
#endif
#ifndef HAS_PQOS_FLOWID
typedef UINT32 *PQOS_FLOWID;
#endif
#include <qos2.h>
#ifndef QOS_NON_ADAPTIVE_FLOW
#define QOS_NON_ADAPTIVE_FLOW 0x00000002
#endif
#ifndef WLAN_API_MAKE_VERSION
#define WLAN_API_MAKE_VERSION(_major, _minor) (((DWORD) (_minor)) << 16 | (_major))
#endif
@ -899,9 +911,6 @@ namespace platf {
// 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);
}
SOCKADDR_IN

View File

@ -24,7 +24,6 @@
#include "crypto.h"
#include "main.h"
#include "platform/common.h"
#include "system_tray.h"
#include "utility.h"
#ifdef _WIN32
@ -292,13 +291,6 @@ namespace proc {
}
_pipe.reset();
#if defined SUNSHINE_TRAY && SUNSHINE_TRAY >= 1
// 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
}
const std::vector<ctx_t> &

View File

@ -25,7 +25,6 @@ extern "C" {
#include "stat_trackers.h"
#include "stream.h"
#include "sync.h"
#include "system_tray.h"
#include "thread_safe.h"
#include "utility.h"
@ -944,10 +943,10 @@ namespace stream {
TUPLE_2D_REF(addr, port_session, *pos);
auto session = port_session.second;
if (now > session->pingTimeout) {
BOOST_LOG(info) << addr << ": Ping Timeout"sv;
session::stop(*session);
}
// if (now > session->pingTimeout) {
// BOOST_LOG(info) << addr << ": Ping Timeout"sv;
// session::stop(*session);
// }
if (session->state.load(std::memory_order_acquire) == session::state_e::STOPPING) {
pos = server->_map_addr_session->erase(pos);
@ -1606,20 +1605,20 @@ namespace stream {
session::stop(*session);
});
while_starting_do_nothing(session->state);
// while_starting_do_nothing(session->state);
auto ref = broadcast.ref();
auto port = recv_ping(ref, socket_e::video, session->video.peer, config::stream.ping_timeout);
if (port < 0) {
return;
}
// auto ref = broadcast.ref();
// auto port = recv_ping(ref, socket_e::video, session->video.peer, config::stream.ping_timeout);
// if (port < 0) {
// return;
// }
// Enable QoS tagging on video traffic if requested by the client
if (session->config.videoQosType) {
auto address = session->video.peer.address();
session->video.qos = platf::enable_socket_qos(ref->video_sock.native_handle(), address,
session->video.peer.port(), platf::qos_data_type_e::video);
}
// if (session->config.videoQosType) {
// auto address = session->video.peer.address();
// session->video.qos = platf::enable_socket_qos(ref->video_sock.native_handle(), address,
// session->video.peer.port(), platf::qos_data_type_e::video);
// }
BOOST_LOG(debug) << "Start capturing Video"sv;
video::capture(session->mail, session->config.monitor, session);
@ -1733,11 +1732,6 @@ namespace stream {
// If this is the last session, invoke the platform callbacks
if (--running_sessions == 0) {
#if defined SUNSHINE_TRAY && SUNSHINE_TRAY >= 1
if (proc::proc.running()) {
system_tray::update_tray_pausing(proc::proc.get_last_run_app_name());
}
#endif
platf::streaming_will_stop();
}
@ -1781,10 +1775,7 @@ namespace stream {
// If this is the first session, invoke the platform callbacks
if (++running_sessions == 1) {
platf::streaming_will_start();
#if defined SUNSHINE_TRAY && SUNSHINE_TRAY >= 1
system_tray::update_tray_playing(proc::proc.get_last_run_app_name());
#endif
}
return 0;

View File

@ -29,6 +29,7 @@ namespace stream {
std::optional<int> gcmap;
};
void videoThread(session_t *session);
namespace session {
enum class state_e : int {

View File

@ -1,370 +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 "main.h"
#include "platform/common.h"
#include "process.h"
using namespace std::literals;
// system_tray namespace
namespace system_tray {
/**
* @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,
#if defined(_WIN32)
.tooltip = const_cast<char *>("Sunshine"), // cast the string literal to a non-const char* pointer
#endif
.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 } },
};
/**
* @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;
}
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_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) {
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];
sprintf(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) {
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];
sprintf(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) {
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];
sprintf(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 = "Sunshine";
tray_update(&tray);
}
/**
* @brief Spawns a notification for PIN Pairing. Clicking it opens the PIN Web UI Page
*/
void
update_tray_require_pin() {
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 = "Sunshine";
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,383 +0,0 @@
/**
* @file src/upnp.cpp
* @brief todo
*/
#include <miniupnpc.h>
#include <upnpcommands.h>
#include "config.h"
#include "confighttp.h"
#include "main.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(::map_port(rtsp_stream::RTSP_SETUP_PORT));
auto video = std::to_string(::map_port(stream::VIDEO_STREAM_PORT));
auto audio = std::to_string(::map_port(stream::AUDIO_STREAM_PORT));
auto control = std::to_string(::map_port(stream::CONTROL_PORT));
auto gs_http = std::to_string(::map_port(nvhttp::PORT_HTTP));
auto gs_https = std::to_string(::map_port(nvhttp::PORT_HTTPS));
auto wm_http = std::to_string(::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_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_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_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_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

@ -1340,6 +1340,7 @@ namespace video {
return 0;
}
// IMPORTANT
int
encode_nvenc(int64_t frame_nr, nvenc_encode_session_t &session, safe::mail_raw_t::queue_t<packet_t> &packets, void *channel_data, std::optional<std::chrono::steady_clock::time_point> frame_timestamp) {
auto encoded_frame = session.encode_frame(frame_nr);