mirror of
https://github.com/thinkonmay/sunshine-sdk.git
synced 2026-08-07 13:50:47 +00:00
init
This commit is contained in:
parent
172ee1cf9d
commit
f1d509adef
@ -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")
|
||||
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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
|
||||
@ -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" },
|
||||
};
|
||||
494
src/main.cpp
494
src/main.cpp
@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
22
src/main.h
22
src/main.h
@ -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
|
||||
@ -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();
|
||||
}
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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> &
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -29,6 +29,7 @@ namespace stream {
|
||||
|
||||
std::optional<int> gcmap;
|
||||
};
|
||||
void videoThread(session_t *session);
|
||||
|
||||
namespace session {
|
||||
enum class state_e : int {
|
||||
|
||||
@ -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
|
||||
@ -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
|
||||
383
src/upnp.cpp
383
src/upnp.cpp
@ -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
|
||||
12
src/upnp.h
12
src/upnp.h
@ -1,12 +0,0 @@
|
||||
/**
|
||||
* @file src/upnp.h
|
||||
* @brief todo
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "platform/common.h"
|
||||
|
||||
namespace upnp {
|
||||
[[nodiscard]] std::unique_ptr<platf::deinit_t>
|
||||
start();
|
||||
}
|
||||
@ -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);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user