@@ -6,7 +6,10 @@
#define BOOST_BIND_GLOBAL_PLACEHOLDERS
// standard includes
#include <algorithm>
#include <chrono>
#include <filesystem>
#include <format>
#include <mutex>
#include <string>
#include <utility>
@@ -171,5 +174,6 @@
// uniqueID, session
std::unordered_map<std::string, pair_session_t> map_id_sess; ///< Pairing sessions keyed by temporary unique ID.
std::recursive_mutex map_id_sess_mutex; ///< Mutex protecting pairing-session storage and lifecycle transitions.
client_t client_root; ///< In-memory representation of the paired-client database.
std::atomic<uint32_t> session_id_counter; ///< Monotonic counter used to allocate GameStream session IDs.
@@ -421,6 +425,152 @@
return launch_session;
}
/**
* @brief Write a completed response to the client waiting for PIN approval.
*
* @param sess Pairing session that owns the waiting response.
* @param tree XML response body to write.
* @return `true` when a live response was available.
*/
bool write_pairing_response(pair_session_t &sess, const pt::ptree &tree) {
std::ostringstream data;
pt::write_xml(data, tree);
auto &response = sess.async_insert_pin.response;
if (response.has_left() && response.left()) {
response.left()->close_connection_after_response = true;
response.left()->write(data.str());
} else if (response.has_right() && response.right()) {
response.right()->close_connection_after_response = true;
response.right()->write(data.str());
} else {
return false;
}
response = std::monostate {};
return true;
}
/**
* @brief Expire stale pairing sessions while the session mutex is held.
*
* @param now Monotonic time used to evaluate session deadlines.
*/
void expire_pair_sessions_unlocked(const std::chrono::steady_clock::time_point now) {
for (auto it = map_id_sess.begin(); it != map_id_sess.end();) {
if (it->second.async_insert_pin.expires_at > now) {
++it;
continue;
}
pt::ptree tree;
tree.put("root.paired", 0);
tree.put("root.<xmlattr>.status_code", 408);
tree.put("root.<xmlattr>.status_message", "Pairing session expired");
write_pairing_response(it->second, tree);
it = map_id_sess.erase(it);
}
}
pair_session_insert_e insert_pair_session(pair_session_t sess, std::string &pairing_id) {
std::scoped_lock lock(map_id_sess_mutex);
const auto now = std::chrono::steady_clock::now();
expire_pair_sessions_unlocked(now);
pairing_id.clear();
if (map_id_sess.contains(sess.client.uniqueID)) {
return pair_session_insert_e::ALREADY_EXISTS;
}
if (map_id_sess.size() >= MAX_PENDING_PAIRING_SESSIONS) {
return pair_session_insert_e::FULL;
}
do {
pairing_id = util::hex_vec(crypto::rand(PAIRING_ID_SIZE / 2), true);
} while (std::ranges::any_of(map_id_sess, [&](const auto &entry) {
return entry.second.async_insert_pin.id == pairing_id;
}));
sess.async_insert_pin.id = pairing_id;
sess.async_insert_pin.expires_at = now + PAIRING_SESSION_TIMEOUT;
map_id_sess.emplace(sess.client.uniqueID, std::move(sess));
return pair_session_insert_e::ADDED;
}
bool is_valid_pairing_id(const std::string_view pairing_id) {
return pairing_id.size() == PAIRING_ID_SIZE && std::ranges::all_of(pairing_id, [](const char character) {
return (character >= '0' && character <= '9') || (character >= 'a' && character <= 'f') || (character >= 'A' && character <= 'F');
});
}
bool is_valid_pairing_pin(const std::string_view pin) {
return pin.size() == 4 && std::ranges::all_of(pin, [](const char character) {
return character >= '0' && character <= '9';
});
}
bool is_valid_pairing_name(const std::string_view name) {
return !name.empty() && name.size() <= MAX_PAIRING_CLIENT_NAME_SIZE;
}
void expire_pair_sessions(const std::chrono::steady_clock::time_point now) {
std::scoped_lock lock(map_id_sess_mutex);
expire_pair_sessions_unlocked(now);
}
std::vector<pending_pairing_t> get_pending_pairings() {
std::scoped_lock lock(map_id_sess_mutex);
expire_pair_sessions_unlocked(std::chrono::steady_clock::now());
std::vector<const pair_session_t *> pending_sessions;
pending_sessions.reserve(map_id_sess.size());
for (const auto &entry : map_id_sess) {
const auto &sess = entry.second;
if (sess.last_phase == PAIR_PHASE::NONE) {
pending_sessions.push_back(&sess);
}
}
std::ranges::sort(pending_sessions, {}, [](const pair_session_t *sess) {
return sess->async_insert_pin.expires_at;
});
std::vector<pending_pairing_t> result;
result.reserve(pending_sessions.size());
for (const auto *sess : pending_sessions) {
result.push_back({
.id = sess->async_insert_pin.id,
.name = sess->async_insert_pin.device_name,
.address = sess->async_insert_pin.address,
});
}
return result;
}
bool cancel_pairing(const std::string_view pairing_id) {
if (!is_valid_pairing_id(pairing_id)) {
return false;
}
std::scoped_lock lock(map_id_sess_mutex);
expire_pair_sessions_unlocked(std::chrono::steady_clock::now());
const auto sess_it = std::ranges::find_if(map_id_sess, [&](const auto &entry) {
return entry.second.last_phase == PAIR_PHASE::NONE && entry.second.async_insert_pin.id == pairing_id;
});
if (sess_it == map_id_sess.end()) {
return false;
}
pt::ptree tree;
tree.put("root.paired", 0);
tree.put("root.<xmlattr>.status_code", 400);
tree.put("root.<xmlattr>.status_message", "Pairing request cancelled by operator");
write_pairing_response(sess_it->second, tree);
map_id_sess.erase(sess_it);
return true;
}
void remove_session(const pair_session_t &sess) {
std::scoped_lock lock(map_id_sess_mutex);
map_id_sess.erase(sess.client.uniqueID);
}
@@ -436,7 +586,7 @@
tree.put("root.paired", 0);
tree.put("root.<xmlattr>.status_code", 400);
tree.put("root.<xmlattr>.status_message", status_msg);
remove_session(sess); // Security measure, delete the session when something went wrong and force a re-pair
sess.failed = true;
}
/**
@@ -610,6 +760,5 @@
tree.put("root.paired", 0);
}
remove_session(sess);
tree.put("root.<xmlattr>.status_code", 200);
}
@@ -719,27 +868,59 @@
if (it->second == "getservercert"sv) {
pair_session_t sess;
sess.client.uniqueID = std::move(uniqID);
sess.client.uniqueID = uniqID;
sess.client.cert = util::from_hex_vec(get_arg(args, "clientcert"), true);
sess.async_insert_pin.salt = get_arg(args, "salt");
sess.async_insert_pin.device_name = get_arg(args, "devicename");
sess.async_insert_pin.address = net::addr_to_normalized_string(request->remote_endpoint().address());
BOOST_LOG(debug) << sess.client.cert;
auto ptr = map_id_sess.emplace(sess.client.uniqueID, std::move(sess)).first;
const bool pin_stdin = config::sunshine.flags[config::flag::PIN_STDIN];
if (!pin_stdin) {
sess.async_insert_pin.response = response;
}
ptr->second.async_insert_pin.salt = std::move(get_arg(args, "salt"));
if (config::sunshine.flags[config::flag::PIN_STDIN]) {
std::string pairing_id;
switch (insert_pair_session(std::move(sess), pairing_id)) {
case pair_session_insert_e::ALREADY_EXISTS:
tree.put("root.paired", 0);
tree.put("root.<xmlattr>.status_code", 409);
tree.put("root.<xmlattr>.status_message", "A pairing session with this uniqueid already exists");
return;
case pair_session_insert_e::FULL:
tree.put("root.paired", 0);
tree.put("root.<xmlattr>.status_code", 503);
tree.put("root.<xmlattr>.status_message", "Too many pending pairing sessions");
return;
case pair_session_insert_e::ADDED:
break;
}
if (pin_stdin) {
std::string pin;
std::cout << "Please insert pin: "sv;
std::getline(std::cin, pin);
getservercert(ptr->second, tree, pin);
std::scoped_lock lock(map_id_sess_mutex);
expire_pair_sessions_unlocked(std::chrono::steady_clock::now());
const auto sess_it = map_id_sess.find(uniqID);
if (sess_it == map_id_sess.end() || sess_it->second.async_insert_pin.id != pairing_id) {
tree.put("root.paired", 0);
tree.put("root.<xmlattr>.status_code", 408);
tree.put("root.<xmlattr>.status_message", "Pairing session expired");
return;
}
getservercert(sess_it->second, tree, pin);
if (sess_it->second.failed) {
map_id_sess.erase(sess_it);
}
return;
} 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;
}
@@ -750,6 +931,8 @@
}
}
std::scoped_lock lock(map_id_sess_mutex);
expire_pair_sessions_unlocked(std::chrono::steady_clock::now());
auto sess_it = map_id_sess.find(uniqID);
if (sess_it == std::end(map_id_sess)) {
tree.put("root.<xmlattr>.status_code", 400);
@@ -758,6 +941,7 @@
return;
}
bool pairing_complete = false;
if (it = args.find("clientchallenge"); it != std::end(args)) {
auto challenge = util::from_hex_vec(it->second, true);
clientchallenge(sess_it->second, tree, challenge);
@@ -767,58 +951,43 @@
} else if (it = args.find("clientpairingsecret"); it != std::end(args)) {
auto pairingsecret = util::from_hex_vec(it->second, true);
clientpairingsecret(sess_it->second, add_cert, tree, pairingsecret);
pairing_complete = true;
} else {
tree.put("root.<xmlattr>.status_code", 404);
tree.put("root.<xmlattr>.status_message", "Invalid pairing request");
fail_pair(sess_it->second, tree, "Invalid pairing request");
}
}
bool pin(std::string pin, std::string name) {
pt::ptree tree;
if (map_id_sess.empty()) {
return false;
if (pairing_complete || sess_it->second.failed) {
map_id_sess.erase(sess_it);
}
}
bool pin(const std::string_view pairing_id, std::string pin, std::string name) {
// 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",
std::format("Pin must be 4 digits, {} provided", pin.size())
);
if (!is_valid_pairing_id(pairing_id) || !is_valid_pairing_pin(pin) || !is_valid_pairing_name(name)) {
return false;
}
// ensure all pin characters are numeric
std::scoped_lock lock(map_id_sess_mutex);
expire_pair_sessions_unlocked(std::chrono::steady_clock::now());
const auto sess_it = std::ranges::find_if(map_id_sess, [&](const auto &entry) {
return entry.second.last_phase == PAIR_PHASE::NONE && entry.second.async_insert_pin.id == pairing_id;
});
if (sess_it == map_id_sess.end()) {
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;
auto &sess = sess_it->second;
pt::ptree tree;
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;
if (!sess.failed) {
sess.client.name = std::move(name);
}
// reset async_response
async_response = std::decay_t<decltype(async_response.left())>();
// response to the current request
return true;
const bool response_written = write_pairing_response(sess, tree);
const bool success = response_written && !sess.failed;
if (!response_written || sess.failed) {
map_id_sess.erase(sess_it);
}
return success;
}
/**