PR #2042 introduced another location for storing authorized clients but did not correctly consider how the load/store logic should differ for those places. One location (named_devices) could contain clients which had not completed pairing, while the other (certs) had only fully paired clients. Despite differences in trust level of clients in each list, the logic for loading/saving config treated them identically. The result is that clients that had not successfully completed pairing would be treated as fully paired after a state reload. Fix this state confusion by consolidating to a single location for authorized client state and ensuring it only contains information on fully paired clients.
1191 lines
40 KiB
C++
1191 lines
40 KiB
C++
/**
|
|
* @file src/nvhttp.cpp
|
|
* @brief Definitions for the nvhttp (GameStream) server.
|
|
*/
|
|
// macros
|
|
#define BOOST_BIND_GLOBAL_PLACEHOLDERS
|
|
|
|
// standard includes
|
|
#include <filesystem>
|
|
#include <utility>
|
|
|
|
// lib includes
|
|
#include <Simple-Web-Server/server_http.hpp>
|
|
#include <Simple-Web-Server/server_https.hpp>
|
|
#include <boost/asio/ssl/context.hpp>
|
|
#include <boost/asio/ssl/context_base.hpp>
|
|
#include <boost/property_tree/json_parser.hpp>
|
|
#include <boost/property_tree/ptree.hpp>
|
|
#include <boost/property_tree/xml_parser.hpp>
|
|
#include <string>
|
|
|
|
// local includes
|
|
#include "config.h"
|
|
#include "crypto.h"
|
|
#include "file_handler.h"
|
|
#include "globals.h"
|
|
#include "httpcommon.h"
|
|
#include "logging.h"
|
|
#include "network.h"
|
|
#include "nvhttp.h"
|
|
#include "platform/common.h"
|
|
#include "process.h"
|
|
#include "rtsp.h"
|
|
#include "system_tray.h"
|
|
#include "utility.h"
|
|
#include "uuid.h"
|
|
#include "video.h"
|
|
|
|
using namespace std::literals;
|
|
namespace nvhttp {
|
|
|
|
namespace fs = std::filesystem;
|
|
namespace pt = boost::property_tree;
|
|
|
|
crypto::cert_chain_t cert_chain;
|
|
|
|
class SunshineHTTPS: public SimpleWeb::HTTPS {
|
|
public:
|
|
SunshineHTTPS(boost::asio::io_service &io_service, boost::asio::ssl::context &ctx):
|
|
SimpleWeb::HTTPS(io_service, ctx) {}
|
|
|
|
virtual ~SunshineHTTPS() {
|
|
// Gracefully shutdown the TLS connection
|
|
SimpleWeb::error_code ec;
|
|
shutdown(ec);
|
|
}
|
|
};
|
|
|
|
class SunshineHTTPSServer: public SimpleWeb::ServerBase<SunshineHTTPS> {
|
|
public:
|
|
SunshineHTTPSServer(const std::string &certification_file, const std::string &private_key_file):
|
|
ServerBase<SunshineHTTPS>::ServerBase(443),
|
|
context(boost::asio::ssl::context::tls_server) {
|
|
// Disabling TLS 1.0 and 1.1 (see RFC 8996)
|
|
context.set_options(boost::asio::ssl::context::no_tlsv1);
|
|
context.set_options(boost::asio::ssl::context::no_tlsv1_1);
|
|
context.use_certificate_chain_file(certification_file);
|
|
context.use_private_key_file(private_key_file, boost::asio::ssl::context::pem);
|
|
}
|
|
|
|
std::function<int(SSL *)> verify;
|
|
std::function<void(std::shared_ptr<Response>, std::shared_ptr<Request>)> on_verify_failed;
|
|
|
|
protected:
|
|
boost::asio::ssl::context context;
|
|
|
|
void
|
|
after_bind() override {
|
|
if (verify) {
|
|
context.set_verify_mode(boost::asio::ssl::verify_peer | boost::asio::ssl::verify_fail_if_no_peer_cert | boost::asio::ssl::verify_client_once);
|
|
context.set_verify_callback([](int verified, boost::asio::ssl::verify_context &ctx) {
|
|
// To respond with an error message, a connection must be established
|
|
return 1;
|
|
});
|
|
}
|
|
}
|
|
|
|
// This is Server<HTTPS>::accept() with SSL validation support added
|
|
void
|
|
accept() override {
|
|
auto connection = create_connection(*io_service, context);
|
|
|
|
acceptor->async_accept(connection->socket->lowest_layer(), [this, connection](const SimpleWeb::error_code &ec) {
|
|
auto lock = connection->handler_runner->continue_lock();
|
|
if (!lock)
|
|
return;
|
|
|
|
if (ec != SimpleWeb::error::operation_aborted)
|
|
this->accept();
|
|
|
|
auto session = std::make_shared<Session>(config.max_request_streambuf_size, connection);
|
|
|
|
if (!ec) {
|
|
boost::asio::ip::tcp::no_delay option(true);
|
|
SimpleWeb::error_code ec;
|
|
session->connection->socket->lowest_layer().set_option(option, ec);
|
|
|
|
session->connection->set_timeout(config.timeout_request);
|
|
session->connection->socket->async_handshake(boost::asio::ssl::stream_base::server, [this, session](const SimpleWeb::error_code &ec) {
|
|
session->connection->cancel_timeout();
|
|
auto lock = session->connection->handler_runner->continue_lock();
|
|
if (!lock)
|
|
return;
|
|
if (!ec) {
|
|
if (verify && !verify(session->connection->socket->native_handle()))
|
|
this->write(session, on_verify_failed);
|
|
else
|
|
this->read(session);
|
|
}
|
|
else if (this->on_error)
|
|
this->on_error(session->request, ec);
|
|
});
|
|
}
|
|
else if (this->on_error)
|
|
this->on_error(session->request, ec);
|
|
});
|
|
}
|
|
};
|
|
|
|
using https_server_t = SunshineHTTPSServer;
|
|
using http_server_t = SimpleWeb::Server<SimpleWeb::HTTP>;
|
|
|
|
struct conf_intern_t {
|
|
std::string servercert;
|
|
std::string pkey;
|
|
} conf_intern;
|
|
|
|
struct named_cert_t {
|
|
std::string name;
|
|
std::string uuid;
|
|
std::string cert;
|
|
};
|
|
|
|
struct client_t {
|
|
std::vector<named_cert_t> named_devices;
|
|
};
|
|
|
|
struct pair_session_t {
|
|
struct {
|
|
std::string uniqueID;
|
|
std::string cert;
|
|
std::string name;
|
|
} client;
|
|
|
|
std::unique_ptr<crypto::aes_t> cipher_key;
|
|
std::vector<uint8_t> clienthash;
|
|
|
|
std::string serversecret;
|
|
std::string serverchallenge;
|
|
|
|
struct {
|
|
util::Either<
|
|
std::shared_ptr<typename SimpleWeb::ServerBase<SimpleWeb::HTTP>::Response>,
|
|
std::shared_ptr<typename SimpleWeb::ServerBase<SunshineHTTPS>::Response>>
|
|
response;
|
|
std::string salt;
|
|
} async_insert_pin;
|
|
};
|
|
|
|
// uniqueID, session
|
|
std::unordered_map<std::string, pair_session_t> map_id_sess;
|
|
client_t client_root;
|
|
std::atomic<uint32_t> session_id_counter;
|
|
|
|
using args_t = SimpleWeb::CaseInsensitiveMultimap;
|
|
using resp_https_t = std::shared_ptr<typename SimpleWeb::ServerBase<SunshineHTTPS>::Response>;
|
|
using req_https_t = std::shared_ptr<typename SimpleWeb::ServerBase<SunshineHTTPS>::Request>;
|
|
using resp_http_t = std::shared_ptr<typename SimpleWeb::ServerBase<SimpleWeb::HTTP>::Response>;
|
|
using req_http_t = std::shared_ptr<typename SimpleWeb::ServerBase<SimpleWeb::HTTP>::Request>;
|
|
|
|
enum class op_e {
|
|
ADD, ///< Add certificate
|
|
REMOVE ///< Remove certificate
|
|
};
|
|
|
|
std::string
|
|
get_arg(const args_t &args, const char *name, const char *default_value = nullptr) {
|
|
auto it = args.find(name);
|
|
if (it == std::end(args)) {
|
|
if (default_value != NULL) {
|
|
return std::string(default_value);
|
|
}
|
|
|
|
throw std::out_of_range(name);
|
|
}
|
|
return it->second;
|
|
}
|
|
|
|
void
|
|
save_state() {
|
|
pt::ptree root;
|
|
|
|
if (fs::exists(config::nvhttp.file_state)) {
|
|
try {
|
|
pt::read_json(config::nvhttp.file_state, root);
|
|
}
|
|
catch (std::exception &e) {
|
|
BOOST_LOG(error) << "Couldn't read "sv << config::nvhttp.file_state << ": "sv << e.what();
|
|
return;
|
|
}
|
|
}
|
|
|
|
root.erase("root"s);
|
|
|
|
root.put("root.uniqueid", http::unique_id);
|
|
client_t &client = client_root;
|
|
pt::ptree node;
|
|
|
|
pt::ptree named_cert_nodes;
|
|
for (auto &named_cert : client.named_devices) {
|
|
pt::ptree named_cert_node;
|
|
named_cert_node.put("name"s, named_cert.name);
|
|
named_cert_node.put("cert"s, named_cert.cert);
|
|
named_cert_node.put("uuid"s, named_cert.uuid);
|
|
named_cert_nodes.push_back(std::make_pair(""s, named_cert_node));
|
|
}
|
|
root.add_child("root.named_devices"s, named_cert_nodes);
|
|
|
|
try {
|
|
pt::write_json(config::nvhttp.file_state, root);
|
|
}
|
|
catch (std::exception &e) {
|
|
BOOST_LOG(error) << "Couldn't write "sv << config::nvhttp.file_state << ": "sv << e.what();
|
|
return;
|
|
}
|
|
}
|
|
|
|
void
|
|
load_state() {
|
|
if (!fs::exists(config::nvhttp.file_state)) {
|
|
BOOST_LOG(info) << "File "sv << config::nvhttp.file_state << " doesn't exist"sv;
|
|
http::unique_id = uuid_util::uuid_t::generate().string();
|
|
return;
|
|
}
|
|
|
|
pt::ptree tree;
|
|
try {
|
|
pt::read_json(config::nvhttp.file_state, tree);
|
|
}
|
|
catch (std::exception &e) {
|
|
BOOST_LOG(error) << "Couldn't read "sv << config::nvhttp.file_state << ": "sv << e.what();
|
|
|
|
return;
|
|
}
|
|
|
|
auto unique_id_p = tree.get_optional<std::string>("root.uniqueid");
|
|
if (!unique_id_p) {
|
|
// This file doesn't contain moonlight credentials
|
|
http::unique_id = uuid_util::uuid_t::generate().string();
|
|
return;
|
|
}
|
|
http::unique_id = std::move(*unique_id_p);
|
|
|
|
auto root = tree.get_child("root");
|
|
client_t client;
|
|
|
|
// Import from old format
|
|
if (root.get_child_optional("devices")) {
|
|
auto device_nodes = root.get_child("devices");
|
|
for (auto &[_, device_node] : device_nodes) {
|
|
auto uniqID = device_node.get<std::string>("uniqueid");
|
|
|
|
if (device_node.count("certs")) {
|
|
for (auto &[_, el] : device_node.get_child("certs")) {
|
|
named_cert_t named_cert;
|
|
named_cert.name = ""s;
|
|
named_cert.cert = el.get_value<std::string>();
|
|
named_cert.uuid = uuid_util::uuid_t::generate().string();
|
|
client.named_devices.emplace_back(named_cert);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (root.count("named_devices")) {
|
|
for (auto &[_, el] : root.get_child("named_devices")) {
|
|
named_cert_t named_cert;
|
|
named_cert.name = el.get_child("name").get_value<std::string>();
|
|
named_cert.cert = el.get_child("cert").get_value<std::string>();
|
|
named_cert.uuid = el.get_child("uuid").get_value<std::string>();
|
|
client.named_devices.emplace_back(named_cert);
|
|
}
|
|
}
|
|
|
|
// Empty certificate chain and import certs from file
|
|
cert_chain.clear();
|
|
for (auto &named_cert : client.named_devices) {
|
|
cert_chain.add(crypto::x509(named_cert.cert));
|
|
}
|
|
|
|
client_root = client;
|
|
}
|
|
|
|
void
|
|
add_authorized_client(const std::string &name, std::string &&cert) {
|
|
client_t &client = client_root;
|
|
named_cert_t named_cert;
|
|
named_cert.name = name;
|
|
named_cert.cert = std::move(cert);
|
|
named_cert.uuid = uuid_util::uuid_t::generate().string();
|
|
client.named_devices.emplace_back(named_cert);
|
|
|
|
if (!config::sunshine.flags[config::flag::FRESH_STATE]) {
|
|
save_state();
|
|
}
|
|
}
|
|
|
|
std::shared_ptr<rtsp_stream::launch_session_t>
|
|
make_launch_session(bool host_audio, const args_t &args) {
|
|
auto launch_session = std::make_shared<rtsp_stream::launch_session_t>();
|
|
|
|
launch_session->id = ++session_id_counter;
|
|
|
|
auto rikey = util::from_hex_vec(get_arg(args, "rikey"), true);
|
|
std::copy(rikey.cbegin(), rikey.cend(), std::back_inserter(launch_session->gcm_key));
|
|
|
|
launch_session->host_audio = host_audio;
|
|
std::stringstream mode = std::stringstream(get_arg(args, "mode", "0x0x0"));
|
|
// Split mode by the char "x", to populate width/height/fps
|
|
int x = 0;
|
|
std::string segment;
|
|
while (std::getline(mode, segment, 'x')) {
|
|
if (x == 0) launch_session->width = atoi(segment.c_str());
|
|
if (x == 1) launch_session->height = atoi(segment.c_str());
|
|
if (x == 2) launch_session->fps = atoi(segment.c_str());
|
|
x++;
|
|
}
|
|
launch_session->unique_id = (get_arg(args, "uniqueid", "unknown"));
|
|
launch_session->appid = util::from_view(get_arg(args, "appid", "unknown"));
|
|
launch_session->enable_sops = util::from_view(get_arg(args, "sops", "0"));
|
|
launch_session->surround_info = util::from_view(get_arg(args, "surroundAudioInfo", "196610"));
|
|
launch_session->surround_params = (get_arg(args, "surroundParams", ""));
|
|
launch_session->gcmap = util::from_view(get_arg(args, "gcmap", "0"));
|
|
launch_session->enable_hdr = util::from_view(get_arg(args, "hdrMode", "0"));
|
|
|
|
// Encrypted RTSP is enabled with client reported corever >= 1
|
|
auto corever = util::from_view(get_arg(args, "corever", "0"));
|
|
if (corever >= 1) {
|
|
launch_session->rtsp_cipher = crypto::cipher::gcm_t {
|
|
launch_session->gcm_key, false
|
|
};
|
|
launch_session->rtsp_iv_counter = 0;
|
|
}
|
|
launch_session->rtsp_url_scheme = launch_session->rtsp_cipher ? "rtspenc://"s : "rtsp://"s;
|
|
|
|
// Generate the unique identifiers for this connection that we will send later during RTSP handshake
|
|
unsigned char raw_payload[8];
|
|
RAND_bytes(raw_payload, sizeof(raw_payload));
|
|
launch_session->av_ping_payload = util::hex_vec(raw_payload);
|
|
RAND_bytes((unsigned char *) &launch_session->control_connect_data, sizeof(launch_session->control_connect_data));
|
|
|
|
launch_session->iv.resize(16);
|
|
uint32_t prepend_iv = util::endian::big<uint32_t>(util::from_view(get_arg(args, "rikeyid")));
|
|
auto prepend_iv_p = (uint8_t *) &prepend_iv;
|
|
std::copy(prepend_iv_p, prepend_iv_p + sizeof(prepend_iv), std::begin(launch_session->iv));
|
|
return launch_session;
|
|
}
|
|
|
|
void
|
|
getservercert(pair_session_t &sess, pt::ptree &tree, const std::string &pin) {
|
|
if (sess.async_insert_pin.salt.size() < 32) {
|
|
tree.put("root.paired", 0);
|
|
tree.put("root.<xmlattr>.status_code", 400);
|
|
tree.put("root.<xmlattr>.status_message", "Salt too short");
|
|
return;
|
|
}
|
|
|
|
std::string_view salt_view { sess.async_insert_pin.salt.data(), 32 };
|
|
|
|
auto salt = util::from_hex<std::array<uint8_t, 16>>(salt_view, true);
|
|
|
|
auto key = crypto::gen_aes_key(salt, pin);
|
|
sess.cipher_key = std::make_unique<crypto::aes_t>(key);
|
|
|
|
tree.put("root.paired", 1);
|
|
tree.put("root.plaincert", util::hex_vec(conf_intern.servercert, true));
|
|
tree.put("root.<xmlattr>.status_code", 200);
|
|
}
|
|
|
|
void
|
|
serverchallengeresp(pair_session_t &sess, pt::ptree &tree, const args_t &args) {
|
|
auto encrypted_response = util::from_hex_vec(get_arg(args, "serverchallengeresp"), true);
|
|
|
|
std::vector<uint8_t> decrypted;
|
|
crypto::cipher::ecb_t cipher(*sess.cipher_key, false);
|
|
|
|
cipher.decrypt(encrypted_response, decrypted);
|
|
|
|
sess.clienthash = std::move(decrypted);
|
|
|
|
auto serversecret = sess.serversecret;
|
|
auto sign = crypto::sign256(crypto::pkey(conf_intern.pkey), serversecret);
|
|
|
|
serversecret.insert(std::end(serversecret), std::begin(sign), std::end(sign));
|
|
|
|
tree.put("root.pairingsecret", util::hex_vec(serversecret, true));
|
|
tree.put("root.paired", 1);
|
|
tree.put("root.<xmlattr>.status_code", 200);
|
|
}
|
|
|
|
void
|
|
clientchallenge(pair_session_t &sess, pt::ptree &tree, const args_t &args) {
|
|
auto challenge = util::from_hex_vec(get_arg(args, "clientchallenge"), true);
|
|
|
|
crypto::cipher::ecb_t cipher(*sess.cipher_key, false);
|
|
|
|
std::vector<uint8_t> decrypted;
|
|
cipher.decrypt(challenge, decrypted);
|
|
|
|
auto x509 = crypto::x509(conf_intern.servercert);
|
|
auto sign = crypto::signature(x509);
|
|
auto serversecret = crypto::rand(16);
|
|
|
|
decrypted.insert(std::end(decrypted), std::begin(sign), std::end(sign));
|
|
decrypted.insert(std::end(decrypted), std::begin(serversecret), std::end(serversecret));
|
|
|
|
auto hash = crypto::hash({ (char *) decrypted.data(), decrypted.size() });
|
|
auto serverchallenge = crypto::rand(16);
|
|
|
|
std::string plaintext;
|
|
plaintext.reserve(hash.size() + serverchallenge.size());
|
|
|
|
plaintext.insert(std::end(plaintext), std::begin(hash), std::end(hash));
|
|
plaintext.insert(std::end(plaintext), std::begin(serverchallenge), std::end(serverchallenge));
|
|
|
|
std::vector<uint8_t> encrypted;
|
|
cipher.encrypt(plaintext, encrypted);
|
|
|
|
sess.serversecret = std::move(serversecret);
|
|
sess.serverchallenge = std::move(serverchallenge);
|
|
|
|
tree.put("root.paired", 1);
|
|
tree.put("root.challengeresponse", util::hex_vec(encrypted, true));
|
|
tree.put("root.<xmlattr>.status_code", 200);
|
|
}
|
|
|
|
void
|
|
clientpairingsecret(std::shared_ptr<safe::queue_t<crypto::x509_t>> &add_cert, pair_session_t &sess, pt::ptree &tree, const args_t &args) {
|
|
auto &client = sess.client;
|
|
|
|
auto pairingsecret = util::from_hex_vec(get_arg(args, "clientpairingsecret"), true);
|
|
if (pairingsecret.size() <= 16) {
|
|
tree.put("root.paired", 0);
|
|
tree.put("root.<xmlattr>.status_code", 400);
|
|
tree.put("root.<xmlattr>.status_message", "Clientpairingsecret too short");
|
|
return;
|
|
}
|
|
|
|
std::string_view secret { pairingsecret.data(), 16 };
|
|
std::string_view sign { pairingsecret.data() + secret.size(), pairingsecret.size() - secret.size() };
|
|
|
|
auto x509 = crypto::x509(client.cert);
|
|
auto x509_sign = crypto::signature(x509);
|
|
|
|
std::string data;
|
|
data.reserve(sess.serverchallenge.size() + x509_sign.size() + secret.size());
|
|
|
|
data.insert(std::end(data), std::begin(sess.serverchallenge), std::end(sess.serverchallenge));
|
|
data.insert(std::end(data), std::begin(x509_sign), std::end(x509_sign));
|
|
data.insert(std::end(data), std::begin(secret), std::end(secret));
|
|
|
|
auto hash = crypto::hash(data);
|
|
|
|
// if hash not correct, probably MITM
|
|
if (!std::memcmp(hash.data(), sess.clienthash.data(), hash.size()) && crypto::verify256(crypto::x509(client.cert), secret, sign)) {
|
|
tree.put("root.paired", 1);
|
|
add_cert->raise(crypto::x509(client.cert));
|
|
|
|
// The client is now successfully paired and will be authorized to connect
|
|
auto it = map_id_sess.find(client.uniqueID);
|
|
add_authorized_client(client.name, std::move(client.cert));
|
|
map_id_sess.erase(it);
|
|
}
|
|
else {
|
|
map_id_sess.erase(client.uniqueID);
|
|
tree.put("root.paired", 0);
|
|
}
|
|
|
|
tree.put("root.<xmlattr>.status_code", 200);
|
|
}
|
|
|
|
template <class T>
|
|
struct tunnel;
|
|
|
|
template <>
|
|
struct tunnel<SunshineHTTPS> {
|
|
static auto constexpr to_string = "HTTPS"sv;
|
|
};
|
|
|
|
template <>
|
|
struct tunnel<SimpleWeb::HTTP> {
|
|
static auto constexpr to_string = "NONE"sv;
|
|
};
|
|
|
|
template <class T>
|
|
void
|
|
print_req(std::shared_ptr<typename SimpleWeb::ServerBase<T>::Request> request) {
|
|
BOOST_LOG(debug) << "TUNNEL :: "sv << tunnel<T>::to_string;
|
|
|
|
BOOST_LOG(debug) << "METHOD :: "sv << request->method;
|
|
BOOST_LOG(debug) << "DESTINATION :: "sv << request->path;
|
|
|
|
for (auto &[name, val] : request->header) {
|
|
BOOST_LOG(debug) << name << " -- " << val;
|
|
}
|
|
|
|
BOOST_LOG(debug) << " [--] "sv;
|
|
|
|
for (auto &[name, val] : request->parse_query_string()) {
|
|
BOOST_LOG(debug) << name << " -- " << val;
|
|
}
|
|
|
|
BOOST_LOG(debug) << " [--] "sv;
|
|
}
|
|
|
|
template <class T>
|
|
void
|
|
not_found(std::shared_ptr<typename SimpleWeb::ServerBase<T>::Response> response, std::shared_ptr<typename SimpleWeb::ServerBase<T>::Request> request) {
|
|
print_req<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();
|
|
|
|
response->close_connection_after_response = true;
|
|
}
|
|
|
|
template <class T>
|
|
void
|
|
pair(std::shared_ptr<safe::queue_t<crypto::x509_t>> &add_cert, std::shared_ptr<typename SimpleWeb::ServerBase<T>::Response> response, std::shared_ptr<typename SimpleWeb::ServerBase<T>::Request> request) {
|
|
print_req<T>(request);
|
|
|
|
pt::ptree tree;
|
|
|
|
auto fg = util::fail_guard([&]() {
|
|
std::ostringstream data;
|
|
|
|
pt::write_xml(data, tree);
|
|
response->write(data.str());
|
|
response->close_connection_after_response = true;
|
|
});
|
|
|
|
auto args = request->parse_query_string();
|
|
if (args.find("uniqueid"s) == std::end(args)) {
|
|
tree.put("root.<xmlattr>.status_code", 400);
|
|
tree.put("root.<xmlattr>.status_message", "Missing uniqueid parameter");
|
|
|
|
return;
|
|
}
|
|
|
|
auto uniqID { get_arg(args, "uniqueid") };
|
|
auto sess_it = map_id_sess.find(uniqID);
|
|
|
|
args_t::const_iterator it;
|
|
if (it = args.find("phrase"); it != std::end(args)) {
|
|
if (it->second == "getservercert"sv) {
|
|
pair_session_t sess;
|
|
|
|
sess.client.uniqueID = std::move(uniqID);
|
|
sess.client.cert = util::from_hex_vec(get_arg(args, "clientcert"), true);
|
|
|
|
BOOST_LOG(debug) << sess.client.cert;
|
|
auto ptr = map_id_sess.emplace(sess.client.uniqueID, std::move(sess)).first;
|
|
|
|
ptr->second.async_insert_pin.salt = std::move(get_arg(args, "salt"));
|
|
if (config::sunshine.flags[config::flag::PIN_STDIN]) {
|
|
std::string pin;
|
|
|
|
std::cout << "Please insert pin: "sv;
|
|
std::getline(std::cin, pin);
|
|
|
|
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();
|
|
return;
|
|
}
|
|
}
|
|
else if (it->second == "pairchallenge"sv) {
|
|
tree.put("root.paired", 1);
|
|
tree.put("root.<xmlattr>.status_code", 200);
|
|
}
|
|
}
|
|
else if (it = args.find("clientchallenge"); it != std::end(args)) {
|
|
clientchallenge(sess_it->second, tree, args);
|
|
}
|
|
else if (it = args.find("serverchallengeresp"); it != std::end(args)) {
|
|
serverchallengeresp(sess_it->second, tree, args);
|
|
}
|
|
else if (it = args.find("clientpairingsecret"); it != std::end(args)) {
|
|
clientpairingsecret(add_cert, sess_it->second, tree, args);
|
|
}
|
|
else {
|
|
tree.put("root.<xmlattr>.status_code", 404);
|
|
tree.put("root.<xmlattr>.status_message", "Invalid pairing request");
|
|
}
|
|
}
|
|
|
|
bool
|
|
pin(std::string pin, std::string name) {
|
|
pt::ptree tree;
|
|
if (map_id_sess.empty()) {
|
|
return false;
|
|
}
|
|
|
|
// ensure pin is 4 digits
|
|
if (pin.size() != 4) {
|
|
tree.put("root.paired", 0);
|
|
tree.put("root.<xmlattr>.status_code", 400);
|
|
tree.put(
|
|
"root.<xmlattr>.status_message", "Pin must be 4 digits, " + std::to_string(pin.size()) + " provided");
|
|
return false;
|
|
}
|
|
|
|
// ensure all pin characters are numeric
|
|
if (!std::all_of(pin.begin(), pin.end(), ::isdigit)) {
|
|
tree.put("root.paired", 0);
|
|
tree.put("root.<xmlattr>.status_code", 400);
|
|
tree.put("root.<xmlattr>.status_message", "Pin must be numeric");
|
|
return false;
|
|
}
|
|
|
|
auto &sess = std::begin(map_id_sess)->second;
|
|
getservercert(sess, tree, pin);
|
|
sess.client.name = name;
|
|
|
|
// response to the request for pin
|
|
std::ostringstream data;
|
|
pt::write_xml(data, tree);
|
|
|
|
auto &async_response = sess.async_insert_pin.response;
|
|
if (async_response.has_left() && async_response.left()) {
|
|
async_response.left()->write(data.str());
|
|
}
|
|
else if (async_response.has_right() && async_response.right()) {
|
|
async_response.right()->write(data.str());
|
|
}
|
|
else {
|
|
return false;
|
|
}
|
|
|
|
// reset async_response
|
|
async_response = std::decay_t<decltype(async_response.left())>();
|
|
// response to the current request
|
|
return true;
|
|
}
|
|
|
|
template <class T>
|
|
void
|
|
serverinfo(std::shared_ptr<typename SimpleWeb::ServerBase<T>::Response> response, std::shared_ptr<typename SimpleWeb::ServerBase<T>::Request> request) {
|
|
print_req<T>(request);
|
|
|
|
int pair_status = 0;
|
|
if constexpr (std::is_same_v<SunshineHTTPS, T>) {
|
|
auto args = request->parse_query_string();
|
|
auto clientID = args.find("uniqueid"s);
|
|
|
|
if (clientID != std::end(args)) {
|
|
pair_status = 1;
|
|
}
|
|
}
|
|
|
|
auto local_endpoint = request->local_endpoint();
|
|
|
|
pt::ptree tree;
|
|
|
|
tree.put("root.<xmlattr>.status_code", 200);
|
|
tree.put("root.hostname", config::nvhttp.sunshine_name);
|
|
|
|
tree.put("root.appversion", VERSION);
|
|
tree.put("root.GfeVersion", GFE_VERSION);
|
|
tree.put("root.uniqueid", http::unique_id);
|
|
tree.put("root.HttpsPort", net::map_port(PORT_HTTPS));
|
|
tree.put("root.ExternalPort", net::map_port(PORT_HTTP));
|
|
tree.put("root.MaxLumaPixelsHEVC", video::active_hevc_mode > 1 ? "1869449984" : "0");
|
|
|
|
// Only include the MAC address for requests sent from paired clients over HTTPS.
|
|
// For HTTP requests, use a placeholder MAC address that Moonlight knows to ignore.
|
|
if constexpr (std::is_same_v<SunshineHTTPS, T>) {
|
|
tree.put("root.mac", platf::get_mac_address(net::addr_to_normalized_string(local_endpoint.address())));
|
|
}
|
|
else {
|
|
tree.put("root.mac", "00:00:00:00:00:00");
|
|
}
|
|
|
|
// Moonlight clients track LAN IPv6 addresses separately from LocalIP which is expected to
|
|
// always be an IPv4 address. If we return that same IPv6 address here, it will clobber the
|
|
// stored LAN IPv4 address. To avoid this, we need to return an IPv4 address in this field
|
|
// when we get a request over IPv6.
|
|
//
|
|
// HACK: We should return the IPv4 address of local interface here, but we don't currently
|
|
// have that implemented. For now, we will emulate the behavior of GFE+GS-IPv6-Forwarder,
|
|
// which returns 127.0.0.1 as LocalIP for IPv6 connections. Moonlight clients with IPv6
|
|
// support know to ignore this bogus address.
|
|
if (local_endpoint.address().is_v6() && !local_endpoint.address().to_v6().is_v4_mapped()) {
|
|
tree.put("root.LocalIP", "127.0.0.1");
|
|
}
|
|
else {
|
|
tree.put("root.LocalIP", net::addr_to_normalized_string(local_endpoint.address()));
|
|
}
|
|
|
|
uint32_t codec_mode_flags = SCM_H264;
|
|
if (video::last_encoder_probe_supported_yuv444_for_codec[0]) {
|
|
codec_mode_flags |= SCM_H264_HIGH8_444;
|
|
}
|
|
if (video::active_hevc_mode >= 2) {
|
|
codec_mode_flags |= SCM_HEVC;
|
|
if (video::last_encoder_probe_supported_yuv444_for_codec[1]) {
|
|
codec_mode_flags |= SCM_HEVC_REXT8_444;
|
|
}
|
|
}
|
|
if (video::active_hevc_mode >= 3) {
|
|
codec_mode_flags |= SCM_HEVC_MAIN10;
|
|
if (video::last_encoder_probe_supported_yuv444_for_codec[1]) {
|
|
codec_mode_flags |= SCM_HEVC_REXT10_444;
|
|
}
|
|
}
|
|
if (video::active_av1_mode >= 2) {
|
|
codec_mode_flags |= SCM_AV1_MAIN8;
|
|
if (video::last_encoder_probe_supported_yuv444_for_codec[2]) {
|
|
codec_mode_flags |= SCM_AV1_HIGH8_444;
|
|
}
|
|
}
|
|
if (video::active_av1_mode >= 3) {
|
|
codec_mode_flags |= SCM_AV1_MAIN10;
|
|
if (video::last_encoder_probe_supported_yuv444_for_codec[2]) {
|
|
codec_mode_flags |= SCM_AV1_HIGH10_444;
|
|
}
|
|
}
|
|
tree.put("root.ServerCodecModeSupport", codec_mode_flags);
|
|
|
|
auto current_appid = proc::proc.running();
|
|
tree.put("root.PairStatus", pair_status);
|
|
tree.put("root.currentgame", current_appid);
|
|
tree.put("root.state", current_appid > 0 ? "SUNSHINE_SERVER_BUSY" : "SUNSHINE_SERVER_FREE");
|
|
|
|
std::ostringstream data;
|
|
|
|
pt::write_xml(data, tree);
|
|
response->write(data.str());
|
|
response->close_connection_after_response = true;
|
|
}
|
|
|
|
pt::ptree
|
|
get_all_clients() {
|
|
pt::ptree named_cert_nodes;
|
|
client_t &client = client_root;
|
|
for (auto &named_cert : client.named_devices) {
|
|
pt::ptree named_cert_node;
|
|
named_cert_node.put("name"s, named_cert.name);
|
|
named_cert_node.put("uuid"s, named_cert.uuid);
|
|
named_cert_nodes.push_back(std::make_pair(""s, named_cert_node));
|
|
}
|
|
|
|
return named_cert_nodes;
|
|
}
|
|
|
|
void
|
|
applist(resp_https_t response, req_https_t request) {
|
|
print_req<SunshineHTTPS>(request);
|
|
|
|
pt::ptree tree;
|
|
|
|
auto g = util::fail_guard([&]() {
|
|
std::ostringstream data;
|
|
|
|
pt::write_xml(data, tree);
|
|
response->write(data.str());
|
|
response->close_connection_after_response = true;
|
|
});
|
|
|
|
auto &apps = tree.add_child("root", pt::ptree {});
|
|
|
|
apps.put("<xmlattr>.status_code", 200);
|
|
|
|
for (auto &proc : proc::proc.get_apps()) {
|
|
pt::ptree app;
|
|
|
|
app.put("IsHdrSupported"s, video::active_hevc_mode == 3 ? 1 : 0);
|
|
app.put("AppTitle"s, proc.name);
|
|
app.put("ID", proc.id);
|
|
|
|
apps.push_back(std::make_pair("App", std::move(app)));
|
|
}
|
|
}
|
|
|
|
void
|
|
launch(bool &host_audio, resp_https_t response, req_https_t request) {
|
|
print_req<SunshineHTTPS>(request);
|
|
|
|
pt::ptree tree;
|
|
auto g = util::fail_guard([&]() {
|
|
std::ostringstream data;
|
|
|
|
pt::write_xml(data, tree);
|
|
response->write(data.str());
|
|
response->close_connection_after_response = true;
|
|
});
|
|
|
|
if (rtsp_stream::session_count() == config::stream.channels) {
|
|
tree.put("root.resume", 0);
|
|
tree.put("root.<xmlattr>.status_code", 503);
|
|
tree.put("root.<xmlattr>.status_message", "The host's concurrent stream limit has been reached. Stop an existing stream or increase the 'Channels' value in the Sunshine Web UI.");
|
|
|
|
return;
|
|
}
|
|
|
|
auto args = request->parse_query_string();
|
|
if (
|
|
args.find("rikey"s) == std::end(args) ||
|
|
args.find("rikeyid"s) == std::end(args) ||
|
|
args.find("localAudioPlayMode"s) == std::end(args) ||
|
|
args.find("appid"s) == std::end(args)) {
|
|
tree.put("root.resume", 0);
|
|
tree.put("root.<xmlattr>.status_code", 400);
|
|
tree.put("root.<xmlattr>.status_message", "Missing a required launch parameter");
|
|
|
|
return;
|
|
}
|
|
|
|
auto appid = util::from_view(get_arg(args, "appid"));
|
|
|
|
auto current_appid = proc::proc.running();
|
|
if (current_appid > 0) {
|
|
tree.put("root.resume", 0);
|
|
tree.put("root.<xmlattr>.status_code", 400);
|
|
tree.put("root.<xmlattr>.status_message", "An app is already running on this host");
|
|
|
|
return;
|
|
}
|
|
|
|
// Probe encoders again before streaming to ensure our chosen
|
|
// encoder matches the active GPU (which could have changed
|
|
// due to hotplugging, driver crash, primary monitor change,
|
|
// or any number of other factors).
|
|
if (rtsp_stream::session_count() == 0) {
|
|
if (video::probe_encoders()) {
|
|
tree.put("root.<xmlattr>.status_code", 503);
|
|
tree.put("root.<xmlattr>.status_message", "Failed to initialize video capture/encoding. Is a display connected and turned on?");
|
|
tree.put("root.gamesession", 0);
|
|
|
|
return;
|
|
}
|
|
}
|
|
|
|
host_audio = util::from_view(get_arg(args, "localAudioPlayMode"));
|
|
auto launch_session = make_launch_session(host_audio, args);
|
|
|
|
auto encryption_mode = net::encryption_mode_for_address(request->remote_endpoint().address());
|
|
if (!launch_session->rtsp_cipher && encryption_mode == config::ENCRYPTION_MODE_MANDATORY) {
|
|
BOOST_LOG(error) << "Rejecting client that cannot comply with mandatory encryption requirement"sv;
|
|
|
|
tree.put("root.<xmlattr>.status_code", 403);
|
|
tree.put("root.<xmlattr>.status_message", "Encryption is mandatory for this host but unsupported by the client");
|
|
tree.put("root.gamesession", 0);
|
|
|
|
return;
|
|
}
|
|
|
|
if (appid > 0) {
|
|
auto err = proc::proc.execute(appid, launch_session);
|
|
if (err) {
|
|
tree.put("root.<xmlattr>.status_code", err);
|
|
tree.put("root.<xmlattr>.status_message", "Failed to start the specified application");
|
|
tree.put("root.gamesession", 0);
|
|
|
|
return;
|
|
}
|
|
}
|
|
|
|
tree.put("root.<xmlattr>.status_code", 200);
|
|
tree.put("root.sessionUrl0", launch_session->rtsp_url_scheme +
|
|
net::addr_to_url_escaped_string(request->local_endpoint().address()) + ':' +
|
|
std::to_string(net::map_port(rtsp_stream::RTSP_SETUP_PORT)));
|
|
tree.put("root.gamesession", 1);
|
|
|
|
rtsp_stream::launch_session_raise(launch_session);
|
|
}
|
|
|
|
void
|
|
resume(bool &host_audio, resp_https_t response, req_https_t request) {
|
|
print_req<SunshineHTTPS>(request);
|
|
|
|
pt::ptree tree;
|
|
auto g = util::fail_guard([&]() {
|
|
std::ostringstream data;
|
|
|
|
pt::write_xml(data, tree);
|
|
response->write(data.str());
|
|
response->close_connection_after_response = true;
|
|
});
|
|
|
|
// It is possible that due a race condition that this if-statement gives a false negative,
|
|
// that is automatically resolved in rtsp_server_t
|
|
if (rtsp_stream::session_count() == config::stream.channels) {
|
|
tree.put("root.resume", 0);
|
|
tree.put("root.<xmlattr>.status_code", 503);
|
|
tree.put("root.<xmlattr>.status_message", "The host's concurrent stream limit has been reached. Stop an existing stream or increase the 'Channels' value in the Sunshine Web UI.");
|
|
|
|
return;
|
|
}
|
|
|
|
auto current_appid = proc::proc.running();
|
|
if (current_appid == 0) {
|
|
tree.put("root.resume", 0);
|
|
tree.put("root.<xmlattr>.status_code", 503);
|
|
tree.put("root.<xmlattr>.status_message", "No running app to resume");
|
|
|
|
return;
|
|
}
|
|
|
|
auto args = request->parse_query_string();
|
|
if (
|
|
args.find("rikey"s) == std::end(args) ||
|
|
args.find("rikeyid"s) == std::end(args)) {
|
|
tree.put("root.resume", 0);
|
|
tree.put("root.<xmlattr>.status_code", 400);
|
|
tree.put("root.<xmlattr>.status_message", "Missing a required resume parameter");
|
|
|
|
return;
|
|
}
|
|
|
|
if (rtsp_stream::session_count() == 0) {
|
|
// Probe encoders again before streaming to ensure our chosen
|
|
// encoder matches the active GPU (which could have changed
|
|
// due to hotplugging, driver crash, primary monitor change,
|
|
// or any number of other factors).
|
|
if (video::probe_encoders()) {
|
|
tree.put("root.resume", 0);
|
|
tree.put("root.<xmlattr>.status_code", 503);
|
|
tree.put("root.<xmlattr>.status_message", "Failed to initialize video capture/encoding. Is a display connected and turned on?");
|
|
|
|
return;
|
|
}
|
|
|
|
// Newer Moonlight clients send localAudioPlayMode on /resume too,
|
|
// so we should use it if it's present in the args and there are
|
|
// no active sessions we could be interfering with.
|
|
if (args.find("localAudioPlayMode"s) != std::end(args)) {
|
|
host_audio = util::from_view(get_arg(args, "localAudioPlayMode"));
|
|
}
|
|
}
|
|
|
|
auto launch_session = make_launch_session(host_audio, args);
|
|
|
|
auto encryption_mode = net::encryption_mode_for_address(request->remote_endpoint().address());
|
|
if (!launch_session->rtsp_cipher && encryption_mode == config::ENCRYPTION_MODE_MANDATORY) {
|
|
BOOST_LOG(error) << "Rejecting client that cannot comply with mandatory encryption requirement"sv;
|
|
|
|
tree.put("root.<xmlattr>.status_code", 403);
|
|
tree.put("root.<xmlattr>.status_message", "Encryption is mandatory for this host but unsupported by the client");
|
|
tree.put("root.gamesession", 0);
|
|
|
|
return;
|
|
}
|
|
|
|
tree.put("root.<xmlattr>.status_code", 200);
|
|
tree.put("root.sessionUrl0", launch_session->rtsp_url_scheme +
|
|
net::addr_to_url_escaped_string(request->local_endpoint().address()) + ':' +
|
|
std::to_string(net::map_port(rtsp_stream::RTSP_SETUP_PORT)));
|
|
tree.put("root.resume", 1);
|
|
|
|
rtsp_stream::launch_session_raise(launch_session);
|
|
}
|
|
|
|
void
|
|
cancel(resp_https_t response, req_https_t request) {
|
|
print_req<SunshineHTTPS>(request);
|
|
|
|
pt::ptree tree;
|
|
auto g = util::fail_guard([&]() {
|
|
std::ostringstream data;
|
|
|
|
pt::write_xml(data, tree);
|
|
response->write(data.str());
|
|
response->close_connection_after_response = true;
|
|
});
|
|
|
|
// It is possible that due a race condition that this if-statement gives a false positive,
|
|
// the client should try again
|
|
if (rtsp_stream::session_count() != 0) {
|
|
tree.put("root.resume", 0);
|
|
tree.put("root.<xmlattr>.status_code", 503);
|
|
tree.put("root.<xmlattr>.status_message", "All sessions must be disconnected before quitting");
|
|
|
|
return;
|
|
}
|
|
|
|
tree.put("root.cancel", 1);
|
|
tree.put("root.<xmlattr>.status_code", 200);
|
|
|
|
if (proc::proc.running() > 0) {
|
|
proc::proc.terminate();
|
|
}
|
|
}
|
|
|
|
void
|
|
appasset(resp_https_t response, req_https_t request) {
|
|
print_req<SunshineHTTPS>(request);
|
|
|
|
auto args = request->parse_query_string();
|
|
auto app_image = proc::proc.get_app_image(util::from_view(get_arg(args, "appid")));
|
|
|
|
std::ifstream in(app_image, std::ios::binary);
|
|
SimpleWeb::CaseInsensitiveMultimap headers;
|
|
headers.emplace("Content-Type", "image/png");
|
|
response->write(SimpleWeb::StatusCode::success_ok, in, headers);
|
|
response->close_connection_after_response = true;
|
|
}
|
|
|
|
void
|
|
start() {
|
|
auto shutdown_event = mail::man->event<bool>(mail::shutdown);
|
|
|
|
auto port_http = net::map_port(PORT_HTTP);
|
|
auto port_https = net::map_port(PORT_HTTPS);
|
|
auto address_family = net::af_from_enum_string(config::sunshine.address_family);
|
|
|
|
bool clean_slate = config::sunshine.flags[config::flag::FRESH_STATE];
|
|
|
|
if (!clean_slate) {
|
|
load_state();
|
|
}
|
|
|
|
conf_intern.pkey = file_handler::read_file(config::nvhttp.pkey.c_str());
|
|
conf_intern.servercert = file_handler::read_file(config::nvhttp.cert.c_str());
|
|
|
|
auto add_cert = std::make_shared<safe::queue_t<crypto::x509_t>>(30);
|
|
|
|
// resume doesn't always get the parameter "localAudioPlayMode"
|
|
// 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 = [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<SunshineHTTPS>;
|
|
https_server.resource["^/serverinfo$"]["GET"] = serverinfo<SunshineHTTPS>;
|
|
https_server.resource["^/pair$"]["GET"] = [&add_cert](auto resp, auto req) { pair<SunshineHTTPS>(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.config.reuse_address = true;
|
|
http_server.config.address = net::af_to_any_address_string(address_family);
|
|
http_server.config.port = port_http;
|
|
|
|
auto accept_and_run = [&](auto *http_server) {
|
|
try {
|
|
http_server->start();
|
|
}
|
|
catch (boost::system::system_error &err) {
|
|
// It's possible the exception gets thrown after calling http_server->stop() from a different thread
|
|
if (shutdown_event->peek()) {
|
|
return;
|
|
}
|
|
|
|
BOOST_LOG(fatal) << "Couldn't start http server on ports ["sv << port_https << ", "sv << port_https << "]: "sv << err.what();
|
|
shutdown_event->raise(true);
|
|
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();
|
|
}
|
|
|
|
void
|
|
erase_all_clients() {
|
|
client_t client;
|
|
client_root = client;
|
|
cert_chain.clear();
|
|
save_state();
|
|
}
|
|
|
|
int
|
|
unpair_client(std::string uuid) {
|
|
int removed = 0;
|
|
client_t &client = client_root;
|
|
for (auto it = client.named_devices.begin(); it != client.named_devices.end();) {
|
|
if ((*it).uuid == uuid) {
|
|
it = client.named_devices.erase(it);
|
|
removed++;
|
|
}
|
|
else {
|
|
++it;
|
|
}
|
|
}
|
|
|
|
save_state();
|
|
load_state();
|
|
return removed;
|
|
}
|
|
} // namespace nvhttp
|