ref:aa92000dc6bf6eef03b37039812d0a6f65249911

refactor: pairing flow, pairing tests, and inline struct comments (#5602)

SHA: aa92000dc6bf6eef03b37039812d0a6f65249911
Author: Dave Lane <42013603+ReenigneArcher@users.noreply.github.com>
Date: 2026-09-03 12:51
Parents: 41c2571
15 files changed +535 -218
Type
src/config.h +35 −35
@@ -57,9 +57,9 @@
int min_threads; ///< Minimum number of threads or slices for CPU encoding.
struct {
std::string sw_preset;
std::string sw_tune;
std::string sw_preset; ///< FFmpeg software-encoder preset.
std::string sw_tune; ///< FFmpeg software-encoder tuning profile.
std::optional<int> svtav1_preset; ///< SVT-AV1 preset; unset uses the encoder default.
std::optional<int> svtav1_preset;
} sw; ///< Software encoder options.
nvenc::nvenc_config nv; ///< NVIDIA NVENC encoder settings.
@@ -68,54 +68,54 @@
bool nv_sunshine_high_power_mode; ///< Request NVIDIA high-power mode for Sunshine.
struct {
int preset;
int multipass;
int h264_coder;
int preset; ///< Legacy NVENC preset selection.
int multipass; ///< Legacy NVENC multipass mode.
int h264_coder; ///< Legacy NVENC H.264 entropy-coding mode.
int aq; ///< Legacy NVENC adaptive-quantization mode.
int vbv_percentage_increase; ///< Legacy NVENC VBV buffer-size percentage increase.
int aq;
int vbv_percentage_increase;
} nv_legacy; ///< Legacy NVIDIA encoder options kept for config compatibility.
struct {
std::optional<int> qsv_preset; ///< Intel Quick Sync preset; unset uses the encoder default.
std::optional<int> qsv_cavlc; ///< Intel Quick Sync CAVLC selection; unset uses the encoder default.
std::optional<int> qsv_preset;
std::optional<int> qsv_cavlc;
bool qsv_slow_hevc; ///< Whether to enable the slower Intel Quick Sync HEVC path.
bool qsv_slow_hevc;
} qsv; ///< Intel Quick Sync encoder options.
struct {
std::optional<int> amd_usage_h264; ///< AMF H.264 usage profile; unset uses the encoder default.
std::optional<int> amd_usage_h264;
std::optional<int> amd_usage_hevc;
std::optional<int> amd_usage_av1;
std::optional<int> amd_rc_h264;
std::optional<int> amd_rc_hevc;
std::optional<int> amd_rc_av1;
std::optional<int> amd_enforce_hrd;
std::optional<int> amd_quality_h264;
std::optional<int> amd_quality_hevc;
std::optional<int> amd_quality_av1;
std::optional<int> amd_preanalysis;
std::optional<int> amd_vbaq;
std::optional<int> amd_usage_hevc; ///< AMF HEVC usage profile; unset uses the encoder default.
std::optional<int> amd_usage_av1; ///< AMF AV1 usage profile; unset uses the encoder default.
std::optional<int> amd_rc_h264; ///< AMF H.264 rate-control mode; unset uses the encoder default.
std::optional<int> amd_rc_hevc; ///< AMF HEVC rate-control mode; unset uses the encoder default.
std::optional<int> amd_rc_av1; ///< AMF AV1 rate-control mode; unset uses the encoder default.
std::optional<int> amd_enforce_hrd; ///< AMF HRD enforcement setting; unset uses the encoder default.
std::optional<int> amd_quality_h264; ///< AMF H.264 quality preset; unset uses the encoder default.
std::optional<int> amd_quality_hevc; ///< AMF HEVC quality preset; unset uses the encoder default.
std::optional<int> amd_quality_av1; ///< AMF AV1 quality preset; unset uses the encoder default.
std::optional<int> amd_preanalysis; ///< AMF pre-analysis setting; unset uses the encoder default.
std::optional<int> amd_vbaq; ///< AMF variance-based adaptive-quantization setting; unset uses the encoder default.
std::optional<int> amd_max_au_size; ///< Maximum AMF H.264/HEVC access unit size in bits; unset uses the encoder default.
int amd_coder; ///< AMF entropy-coding mode.
int amd_coder;
} amd; ///< AMD AMF encoder options.
struct {
int vt_allow_sw; ///< Whether VideoToolbox may use software encoding.
int vt_allow_sw;
int vt_require_sw; ///< Whether VideoToolbox must use software encoding.
int vt_realtime; ///< Whether VideoToolbox uses realtime encoding mode.
int vt_coder; ///< VideoToolbox entropy-coding mode.
int vt_require_sw;
int vt_realtime;
int vt_coder;
} vt; ///< VideoToolbox encoder options.
struct {
std::optional<int> blbrc; ///< VA-API block-level bitrate-control setting; unset uses the encoder default.
std::optional<int> vaapi_quality; ///< VA-API quality setting; unset uses the encoder default.
std::optional<int> blbrc;
std::optional<int> vaapi_rc; ///< VA-API rate-control mode; unset uses the encoder default.
std::string vaapi_rc_str; ///< Text representation of the selected VA-API rate-control mode.
bool strict_rc_buffer; ///< Whether VA-API must strictly enforce the rate-control buffer.
std::optional<int> vaapi_quality;
std::optional<int> vaapi_rc;
std::string vaapi_rc_str;
bool strict_rc_buffer;
} vaapi; ///< VA-API encoder options.
struct {
int tune; // 0=default, 1=hq, 2=ll, 3=ull, 4=lossless
int rc_mode; // 0=driver, 1=cqp, 2=cbr, 4=vbr
int tune; ///< Vulkan encoder tuning mode: default, HQ, LL, ULL, or lossless.
int rc_mode; ///< Vulkan encoder rate-control mode: driver, CQP, CBR, or VBR.
} vk; ///< Vulkan encoder options.
std::string capture; ///< Capture backend name selected by configuration.
src/confighttp.cpp +1 −1
@@ -1766,7 +1766,7 @@
nlohmann::json output_tree;
output_tree["status"] = nvhttp::cancel_pairing(pairing_id);
send_response(response, output_tree);
} catch (std::exception &e) {
} catch (nlohmann::json::exception &e) {
BOOST_LOG(warning) << "CancelPairing: "sv << e.what();
bad_request(response, request, e.what());
}
src/input.cpp +1 −2
@@ -1670,8 +1670,7 @@
*/
template<typename Packet>
bool validate_fixed_input_packet(std::span<const std::uint8_t> packet, std::uint32_t declared_size) {
constexpr auto expected_size = static_cast<std::uint32_t>(sizeof(Packet) - sizeof(std::uint32_t));
if (declared_size != expected_size) {
if (constexpr auto expected_size = static_cast<std::uint32_t>(sizeof(Packet) - sizeof(std::uint32_t)); declared_size != expected_size) {
return false;
}
return packet.size() >= sizeof(Packet);
src/nvhttp.cpp +142 −95
@@ -49,7 +49,16 @@
namespace pt = boost::property_tree;
crypto::cert_chain_t cert_chain; ///< Enabled paired-client certificates accepted by Sunshine's GameStream HTTPS server.
/**
* @brief Get the mutex protecting paired-client authorization state.
*
* @return Mutex serializing paired-client state and certificate authorization changes.
*/
std::mutex &client_auth_mutex() {
static std::mutex mutex;
std::mutex client_auth_mutex; ///< Serializes paired-client state and certificate authorization changes.
return mutex;
}
/**
* @brief HTTPS server backend that adds Sunshine's client-certificate verification.
@@ -175,6 +184,16 @@
// 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.
/**
* @brief Get the mutex protecting pairing-session state.
*
* @return Mutex serializing pairing-session storage and lifecycle transitions.
*/
std::mutex &map_id_sess_mutex() {
static std::mutex mutex;
return mutex;
}
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.
@@ -286,7 +305,7 @@
/**
* @brief Rebuild the GameStream trust stores from enabled paired-client records.
*
* @note The caller must hold `client_auth_mutex`.
* @note The caller must hold `client_auth_mutex()`.
*/
void rebuild_client_cert_chain() {
cert_chain.clear();
@@ -310,12 +329,11 @@
*
* @param certificate Parsed client certificate to compare by canonical X.509 identity.
* @return `true` only when exactly one matching paired-client record is enabled.
* @note The caller must hold `client_auth_mutex`.
* @note The caller must hold `client_auth_mutex()`.
*/
bool is_client_enabled(const X509 *certificate) {
bool matched = false;
for (const auto &named_cert : client_root.named_devices) {
auto stored_certificate = crypto::x509(named_cert.cert);
if (!stored_certificate || X509_cmp(stored_certificate.get(), certificate) != 0) {
if (auto stored_certificate = crypto::x509(named_cert.cert); !stored_certificate || X509_cmp(stored_certificate.get(), certificate) != 0) {
continue;
}
@@ -333,12 +351,11 @@
*
* @param certificate Parsed client certificate presented during the TLS handshake.
* @return `nullptr` when authorized, otherwise a non-sensitive error string.
* @note The caller must hold `client_auth_mutex`.
* @note The caller must hold `client_auth_mutex()`.
*/
const char *verify_client_certificate(X509 *certificate) {
auto error = cert_chain.verify(certificate);
if (error) {
return error;
if (const auto verification_error = cert_chain.verify(certificate); verification_error) {
return verification_error;
}
return is_client_enabled(certificate) ? nullptr : "Client certificate identity is not enabled";
}
@@ -409,7 +426,7 @@
}
}
std::lock_guard lock {client_auth_mutex()};
std::lock_guard lock {client_auth_mutex};
client_root = std::move(client);
rebuild_client_cert_chain();
}
@@ -432,6 +449,6 @@
named_cert.cert = std::move(canonical_certificate);
named_cert.uuid = uuid_util::uuid_t::generate().string();
std::lock_guard lock {client_auth_mutex};
std::lock_guard lock {client_auth_mutex()};
client_root.named_devices.emplace_back(std::move(named_cert));
rebuild_client_cert_chain();
@@ -557,7 +574,7 @@
}
pair_session_insert_e insert_pair_session(pair_session_t sess, std::string &pairing_id) {
std::scoped_lock lock {map_id_sess_mutex()};
std::scoped_lock lock(map_id_sess_mutex);
const auto now = std::chrono::steady_clock::now();
expire_pair_sessions_unlocked(now);
pairing_id.clear();
@@ -570,7 +587,7 @@
}
do {
pairing_id = crypto::rand_alphabet(PAIRING_ID_SIZE, "0123456789abcdef"sv);
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;
}));
@@ -598,18 +615,17 @@
}
void expire_pair_sessions(const std::chrono::steady_clock::time_point now) {
std::scoped_lock lock(map_id_sess_mutex);
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);
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;
for (const auto &[_, sess] : map_id_sess) {
if (sess.last_phase == PAIR_PHASE::NONE) {
pending_sessions.push_back(&sess);
}
@@ -635,7 +651,7 @@
return false;
}
std::scoped_lock lock {map_id_sess_mutex()};
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) {
@@ -655,6 +671,6 @@
}
void remove_session(const pair_session_t &sess) {
std::scoped_lock lock(map_id_sess_mutex);
std::scoped_lock lock {map_id_sess_mutex()};
map_id_sess.erase(sess.client.uniqueID);
}
@@ -913,6 +929,84 @@
}
/**
* @brief Start a GameStream pairing session for a `getservercert` request.
*
* @tparam T HTTP server transport type.
* @param response HTTP response retained while Web UI PIN approval is pending.
* @param request HTTP request containing the client pairing parameters.
* @param args Parsed request query parameters.
* @param tree XML property tree used for immediate responses.
* @param unique_id Client-provided pairing-session identifier.
* @return `true` when the response remains pending for Web UI PIN approval.
*/
template<class T>
bool start_pairing_session(
const std::shared_ptr<typename SimpleWeb::ServerBase<T>::Response> &response,
const std::shared_ptr<typename SimpleWeb::ServerBase<T>::Request> &request,
const args_t &args,
pt::ptree &tree,
const std::string &unique_id
) {
pair_session_t sess;
sess.client.uniqueID = unique_id;
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;
const bool pin_stdin = config::sunshine.flags[config::flag::PIN_STDIN];
if (!pin_stdin) {
sess.async_insert_pin.response = response;
}
std::string pairing_id;
switch (insert_pair_session(std::move(sess), pairing_id)) {
using enum pair_session_insert_e;
case 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 false;
case 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 false;
case ADDED:
break;
}
if (!pin_stdin) {
#if defined SUNSHINE_TRAY && SUNSHINE_TRAY >= 1
system_tray::update_tray_require_pin();
#endif
return true;
}
std::string pin;
std::cout << "Please insert pin: "sv;
std::getline(std::cin, 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(unique_id);
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 false;
}
getservercert(sess_it->second, tree, pin);
if (sess_it->second.failed) {
map_id_sess.erase(sess_it);
}
return false;
}
/**
* @brief Dispatch the top-level GameStream pairing request by phase.
*
* @param response HTTP response object to populate.
@@ -942,75 +1036,20 @@
auto uniqID {get_arg(args, "uniqueid")};
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 = 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;
const bool pin_stdin = config::sunshine.flags[config::flag::PIN_STDIN];
if (!pin_stdin) {
sess.async_insert_pin.response = response;
}
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);
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
fg.disable();
return;
}
} else if (it->second == "pairchallenge"sv) {
tree.put("root.paired", 1);
const auto phrase_it = args.find("phrase");
if (phrase_it != std::end(args) && phrase_it->second == "getservercert"sv) {
if (start_pairing_session<T>(response, request, args, tree, uniqID)) {
fg.disable();
tree.put("root.<xmlattr>.status_code", 200);
return;
}
return;
}
if (phrase_it != std::end(args) && phrase_it->second == "pairchallenge"sv) {
tree.put("root.paired", 1);
tree.put("root.<xmlattr>.status_code", 200);
return;
}
std::scoped_lock lock(map_id_sess_mutex);
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)) {
@@ -1021,6 +1060,7 @@
}
bool pairing_complete = false;
args_t::const_iterator it;
if (it = args.find("clientchallenge"); it != std::end(args)) {
auto challenge = util::from_hex_vec(it->second, true);
clientchallenge(sess_it->second, tree, challenge);
@@ -1045,7 +1085,7 @@
return false;
}
std::scoped_lock lock(map_id_sess_mutex);
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;
@@ -1185,7 +1225,7 @@
nlohmann::json get_all_clients() {
nlohmann::json named_cert_nodes = nlohmann::json::array();
std::lock_guard lock {client_auth_mutex};
std::lock_guard lock {client_auth_mutex()};
for (const auto &named_cert : client_root.named_devices) {
nlohmann::json named_cert_node;
named_cert_node["name"] = named_cert.name;
@@ -1560,7 +1600,7 @@
BOOST_LOG(debug) << subject_name << " -- "sv << (verified ? "verified"sv : "denied"sv);
});
std::lock_guard lock {client_auth_mutex};
std::lock_guard lock {client_auth_mutex()};
auto err_str = verify_client_certificate(x509.get());
if (err_str) {
BOOST_LOG(warning) << "SSL Verification error :: "sv << err_str;
@@ -1657,14 +1697,14 @@
}
void erase_all_clients() {
std::lock_guard lock {client_auth_mutex};
std::lock_guard lock {client_auth_mutex()};
client_root = {};
cert_chain.clear();
save_state();
}
bool unpair_client(const std::string_view uuid) {
std::lock_guard lock {client_auth_mutex};
std::lock_guard lock {client_auth_mutex()};
bool removed = false;
for (auto it = client_root.named_devices.begin(); it != client_root.named_devices.end();) {
if ((*it).uuid == uuid) {
@@ -1681,7 +1721,7 @@
}
bool set_client_enabled(const std::string_view uuid, bool enabled) {
std::lock_guard lock {client_auth_mutex()};
std::lock_guard lock {client_auth_mutex};
for (auto &named_cert : client_root.named_devices) {
if (named_cert.uuid == uuid) {
named_cert.enabled = enabled;
@@ -1697,7 +1737,7 @@
* @brief Get cert by UUID.
*/
std::string get_cert_by_uuid(const std::string_view uuid) {
std::lock_guard lock {client_auth_mutex};
std::lock_guard lock {client_auth_mutex()};
for (const auto &named_cert : client_root.named_devices) {
if (named_cert.uuid == uuid) {
return named_cert.cert;
@@ -1721,8 +1761,15 @@
#ifdef SUNSHINE_TESTS
namespace test_support {
void pair_http(
std::shared_ptr<typename SimpleWeb::ServerBase<SimpleWeb::HTTP>::Response> response,
std::shared_ptr<typename SimpleWeb::ServerBase<SimpleWeb::HTTP>::Request> request
) {
pair<SimpleWeb::HTTP>(std::move(response), std::move(request));
}
void reset_client_state() {
std::lock_guard lock {client_auth_mutex()};
std::lock_guard lock {client_auth_mutex};
client_root = {};
cert_chain.clear();
}
@@ -1741,6 +1788,6 @@
return false;
}
std::lock_guard lock {client_auth_mutex()};
std::lock_guard lock {client_auth_mutex};
return verify_client_certificate(certificate.get()) == nullptr;
}
src/nvhttp.h +16 −5
@@ -120,9 +120,9 @@
*/
struct pair_session_t {
struct {
std::string uniqueID = {};
std::string cert = {};
std::string name = {};
std::string uniqueID = {}; ///< Client-provided pairing-session identifier.
std::string cert = {}; ///< Client certificate bytes exchanged during pairing.
std::string name = {}; ///< Client name recorded after successful pairing.
} client; ///< Client object or client certificate data owned by this state..
std::unique_ptr<crypto::aes_t> cipher_key = {}; ///< Cipher key.
@@ -135,8 +135,8 @@
util::Either<
std::shared_ptr<typename SimpleWeb::ServerBase<SimpleWeb::HTTP>::Response>,
std::shared_ptr<typename SimpleWeb::ServerBase<SunshineHTTPS>::Response>>
response;
response; ///< Pending HTTP or HTTPS response completed after PIN approval.
std::string salt = {}; ///< Client-provided salt used to derive the pairing key.
std::string salt = {};
std::string id = {}; ///< Unguessable identifier used by the Web UI to approve this session.
std::string device_name = {}; ///< Untrusted device name reported by the pairing client.
std::string address = {}; ///< Network address from which the pairing request originated.
@@ -352,6 +352,17 @@
* @brief Test-only accessors for paired-client authorization state.
*/
namespace test_support {
/**
* @brief Dispatch a plain-HTTP pairing request through the production handler.
*
* @param response HTTP response object to populate.
* @param request HTTP request data from the test client.
*/
void pair_http(
std::shared_ptr<typename SimpleWeb::ServerBase<SimpleWeb::HTTP>::Response> response,
std::shared_ptr<typename SimpleWeb::ServerBase<SimpleWeb::HTTP>::Request> request
);
/**
* @brief Clear in-memory paired-client records without changing persisted state.
*/
src/platform/common.h +23 −23
@@ -230,38 +230,38 @@
union {
struct {
std::uint16_t lowfreq;
std::uint16_t highfreq;
} rumble;
std::uint16_t lowfreq; ///< Low-frequency rumble motor intensity.
std::uint16_t highfreq; ///< High-frequency rumble motor intensity.
} rumble; ///< Main rumble-motor payload.
struct {
std::uint16_t left_trigger;
std::uint16_t right_trigger;
} rumble_triggers;
std::uint16_t left_trigger; ///< Left-trigger rumble motor intensity.
std::uint16_t right_trigger; ///< Right-trigger rumble motor intensity.
} rumble_triggers; ///< Trigger-rumble payload.
struct {
std::uint16_t report_rate;
std::uint8_t motion_type;
} motion_event_state;
std::uint16_t report_rate; ///< Requested motion-sensor report rate.
std::uint8_t motion_type; ///< Motion-sensor type to configure.
} motion_event_state; ///< Motion-event configuration payload.
struct {
std::uint8_t r;
std::uint8_t g;
std::uint8_t b;
} rgb_led;
std::uint8_t r; ///< Red LED channel intensity.
std::uint8_t g; ///< Green LED channel intensity.
std::uint8_t b; ///< Blue LED channel intensity.
} rgb_led; ///< RGB LED payload.
struct {
std::uint8_t solid;
std::uint8_t flashing;
} player_leds;
std::uint8_t solid; ///< Bit mask of solid player indicators.
std::uint8_t flashing; ///< Bit mask of flashing player indicators.
} player_leds; ///< Player-indicator LED payload.
struct {
uint16_t controllerNumber; ///< Controller number supplied to the adaptive-trigger backend.
uint8_t event_flags; ///< Flags describing which adaptive-trigger data is present.
uint8_t type_left; ///< Left adaptive-trigger effect type.
uint16_t controllerNumber;
uint8_t event_flags;
uint8_t type_left;
uint8_t type_right;
std::array<uint8_t, 10> left;
std::array<uint8_t, 10> right;
} adaptive_triggers;
uint8_t type_right; ///< Right adaptive-trigger effect type.
std::array<uint8_t, 10> left; ///< Left adaptive-trigger effect parameters.
std::array<uint8_t, 10> right; ///< Right adaptive-trigger effect parameters.
} adaptive_triggers; ///< Adaptive-trigger effect payload.
} data; ///< Controller feedback payload for the selected feedback type.
};
src/platform/linux/audio.cpp +3 −3
@@ -294,9 +294,9 @@
std::string requested_sink; ///< Requested sink.
struct {
std::uint32_t stereo = PA_INVALID_INDEX;
std::uint32_t surround51 = PA_INVALID_INDEX;
std::uint32_t surround71 = PA_INVALID_INDEX;
std::uint32_t stereo = PA_INVALID_INDEX; ///< PulseAudio module index for the stereo null sink.
std::uint32_t surround51 = PA_INVALID_INDEX; ///< PulseAudio module index for the 5.1 null sink.
std::uint32_t surround71 = PA_INVALID_INDEX; ///< PulseAudio module index for the 7.1 null sink.
} index; ///< PulseAudio module indexes for Sunshine-created null sinks.
std::unique_ptr<safe::event_t<ctx_event_e>> events; ///< Event queue receiving PulseAudio context state changes.
src/platform/linux/vaapi.cpp +10 −10
@@ -104,12 +104,12 @@
struct {
// DRM PRIME file descriptor for this object.
// Needs to be closed manually
int fd;
int fd; ///< DRM PRIME file descriptor for this object; the owner must close it.
// Total size of this object (may include regions which are not part of the surface)
uint32_t size; ///< Total byte size of the DRM object.
uint32_t size;
// Format modifier applied to this object, not sure what that means
uint64_t drm_format_modifier;
uint64_t drm_format_modifier; ///< DRM format modifier applied to this object.
} objects[4]; ///< DRM PRIME backing objects referenced by the descriptor..
// Number of layers making up the surface.
@@ -117,18 +117,18 @@
struct {
// DRM format fourcc of this layer (DRM_FOURCC_*).
uint32_t drm_format; ///< DRM fourcc pixel format for this layer.
uint32_t drm_format;
// Number of planes in this layer.
uint32_t num_planes;
uint32_t num_planes; ///< Number of image planes in this layer.
// references objects --> DRMPRIMESurfaceDescriptor.objects[object_index[0]]
uint32_t object_index[4];
uint32_t object_index[4]; ///< Index of the backing DRM object for each plane.
// Offset within the object of each plane.
uint32_t offset[4]; ///< Byte offset of each plane within its backing object.
uint32_t offset[4];
// Pitch of each plane.
uint32_t pitch[4];
uint32_t pitch[4]; ///< Row pitch in bytes for each plane.
} layers[4]; ///< DRM PRIME layer descriptions for the frame..
};
@@ -613,8 +613,8 @@
*/
typedef struct VAAPIDevicePriv {
union {
void *xdisplay; ///< Native X11 display handle for display-backed contexts.
int fd; ///< DRM file descriptor for device-backed contexts.
void *xdisplay;
int fd;
} drm; ///< Native display or DRM fd passed to FFmpeg's VA-API context.
int drm_fd; ///< DRM fd.
src/platform/windows/display.h +6 −6
@@ -362,13 +362,13 @@
union {
struct
{
UINT HwSchSupported : 1;
UINT HwSchEnabled : 1;
UINT HwSchEnabledByDefault : 1;
UINT IndependentVidPnVSyncControl : 1;
UINT Reserved : 28;
UINT HwSchSupported : 1; ///< Whether hardware-accelerated GPU scheduling is supported.
UINT HwSchEnabled : 1; ///< Whether hardware-accelerated GPU scheduling is enabled.
UINT HwSchEnabledByDefault : 1; ///< Whether hardware scheduling is enabled by default.
UINT IndependentVidPnVSyncControl : 1; ///< Whether independent VidPn VSync control is supported.
UINT Reserved : 28; ///< Reserved capability bits.
};
UINT Value; ///< Packed representation of the WDDM 2.7 capability flags.
UINT Value;
};
} D3DKMT_WDDM_2_7_CAPS; ///< Alias for D3 DKMT WDDM 2 7 CAPS.
src/platform/windows/input.cpp +2 −2
@@ -98,8 +98,8 @@
feedback_queue_t feedback_queue; ///< Feedback queue.
union {
XUSB_REPORT x360;
DS4_REPORT_EX ds4;
XUSB_REPORT x360; ///< Current Xbox 360 controller report.
DS4_REPORT_EX ds4; ///< Current DualShock 4 controller report.
} report; ///< Current HID report for the virtual controller..
// Map from pointer ID to pointer index
src/stream.cpp +28 −28
@@ -503,51 +503,51 @@
boost::asio::ip::address localAddress; ///< Local address.
struct {
std::string ping_payload;
std::string ping_payload; ///< Payload expected from video-channel ping packets.
int lowseq; ///< Next base sequence number for video RTP packets.
udp::endpoint peer; ///< Client UDP endpoint for the video channel.
int lowseq;
udp::endpoint peer;
std::optional<crypto::cipher::gcm_t> cipher;
std::uint64_t gcm_iv_counter;
std::optional<crypto::cipher::gcm_t> cipher; ///< Optional AES-GCM cipher for encrypted video packets.
std::uint64_t gcm_iv_counter; ///< Counter incorporated into video AES-GCM initialization vectors.
safe::mail_raw_t::event_t<bool> idr_events; ///< Event requesting an instantaneous decoder refresh frame.
safe::mail_raw_t::event_t<bool> idr_events;
safe::mail_raw_t::event_t<std::pair<int64_t, int64_t>> invalidate_ref_frames_events;
safe::mail_raw_t::event_t<std::pair<int64_t, int64_t>> invalidate_ref_frames_events; ///< Event carrying the reference-frame range to invalidate.
std::unique_ptr<platf::deinit_t> qos;
std::unique_ptr<platf::deinit_t> qos; ///< Lifetime guard for video-socket QoS configuration.
} video; ///< Video worker thread state for the active stream.
struct {
crypto::cipher::cbc_t cipher; ///< AES-CBC cipher used for audio-packet encryption.
std::string ping_payload; ///< Payload expected from audio-channel ping packets.
crypto::cipher::cbc_t cipher;
std::string ping_payload;
std::uint16_t sequenceNumber;
std::uint16_t sequenceNumber; ///< Next RTP sequence number for an audio packet.
// avRiKeyId == util::endian::big(First (sizeof(avRiKeyId)) bytes of launch_session->iv)
std::uint32_t avRiKeyId; ///< Base audio encryption key identifier derived from the launch-session IV.
std::uint32_t timestamp; ///< RTP timestamp for the next audio packet.
udp::endpoint peer; ///< Client UDP endpoint for the audio channel.
std::uint32_t avRiKeyId;
std::uint32_t timestamp;
udp::endpoint peer;
util::buffer_t<char> shards;
util::buffer_t<uint8_t *> shards_p;
util::buffer_t<char> shards; ///< Backing storage for audio data and parity shards.
util::buffer_t<uint8_t *> shards_p; ///< Pointer table passed to the audio Reed-Solomon encoder.
audio_fec_packet_t fec_packet;
std::unique_ptr<platf::deinit_t> qos;
audio_fec_packet_t fec_packet; ///< Reusable packet header for audio forward-error-correction shards.
std::unique_ptr<platf::deinit_t> qos; ///< Lifetime guard for audio-socket QoS configuration.
} audio; ///< Audio capture configuration for the stream..
struct {
crypto::cipher::gcm_t cipher;
crypto::cipher::gcm_t cipher; ///< AES-GCM cipher for the encrypted control channel.
crypto::aes_t legacy_input_enc_iv; ///< Input IV used when full control-stream encryption is unavailable.
crypto::aes_t legacy_input_enc_iv; // Only used when the client doesn't support full control stream encryption
crypto::aes_t incoming_iv;
crypto::aes_t outgoing_iv;
crypto::aes_t incoming_iv; ///< Initialization vector for incoming control messages.
crypto::aes_t outgoing_iv; ///< Initialization vector for outgoing control messages.
std::uint32_t connect_data; ///< Session identifier used by clients supporting `ML_FF_SESSION_ID_V1`.
std::string expected_peer_address; ///< Expected address for legacy clients without `ML_FF_SESSION_ID_V1`.
std::uint32_t connect_data; // Used for new clients with ML_FF_SESSION_ID_V1
std::string expected_peer_address; // Only used for legacy clients without ML_FF_SESSION_ID_V1
net::peer_t peer;
std::uint32_t seq;
net::peer_t peer; ///< Connected ENet peer for the control channel.
std::uint32_t seq; ///< Sequence number for the next encrypted control message.
platf::feedback_queue_t feedback_queue; ///< Queue of controller feedback awaiting control-channel delivery.
platf::feedback_queue_t feedback_queue;
safe::mail_raw_t::event_t<video::hdr_info_t> hdr_queue;
safe::mail_raw_t::event_t<video::hdr_info_t> hdr_queue; ///< Queue of HDR metadata awaiting control-channel delivery.
} control; ///< Runtime state for the encrypted GameStream control channel.
std::uint32_t launch_session_id; ///< RTSP launch-session ID associated with this stream.
src/upnp.cpp +3 −3
@@ -30,9 +30,9 @@
*/
struct mapping_t {
struct {
std::string wan;
std::string lan;
std::string proto;
std::string wan; ///< External gateway port exposed by the mapping.
std::string lan; ///< Internal Sunshine port targeted by the mapping.
std::string proto; ///< Transport protocol used by the mapping.
} port; ///< WAN/LAN/protocol tuple for the mapped port.
std::string description; ///< Human-readable UPnP lease description advertised to the gateway.
tests/certificate_test_utils.h +1 −1
@@ -25,7 +25,7 @@
auto certificate = crypto::x509(credentials.x509);
auto private_key = crypto::pkey(credentials.pkey);
util::safe_ptr<BASIC_CONSTRAINTS, BASIC_CONSTRAINTS_free> constraints {BASIC_CONSTRAINTS_new()};
util::safe_ptr<BASIC_CONSTRAINTS, &BASIC_CONSTRAINTS_free> constraints {BASIC_CONSTRAINTS_new()};
constraints->ca = 1;
X509_add1_ext_i2d(certificate.get(), NID_basic_constraints, constraints.get(), 1, X509V3_ADD_DEFAULT);
X509_sign(certificate.get(), private_key.get(), EVP_sha256());
tests/unit/test_http_pairing.cpp +258 −0
@@ -9,10 +9,18 @@
// standard includes
#include <atomic>
#include <chrono>
#include <condition_variable>
#include <format>
#include <future>
#include <mutex>
#include <sstream>
#include <thread>
#include <vector>
// lib includes
#include <Simple-Web-Server/client_http.hpp>
#include <Simple-Web-Server/server_http.hpp>
// local includes
#include <src/config.h>
#include <src/nvhttp.h>
@@ -288,6 +296,80 @@
namespace {
/**
* @brief Stream buffer that holds console input until a pairing test releases it.
*/
class gated_input_buffer: public std::streambuf {
public:
/**
* @brief Make the buffered input available to its reader.
*/
void release() {
{
std::lock_guard lock {mutex_};
released_ = true;
}
condition_.notify_all();
}
protected:
/**
* @brief Wait for release and expose the buffered PIN to the input stream.
*
* @return The first buffered character, or end-of-file after it is consumed.
*/
int_type underflow() override {
std::unique_lock lock {mutex_};
condition_.wait(lock, [this]() {
return released_;
});
if (gptr() == nullptr) {
setg(input_.data(), input_.data(), input_.data() + input_.size());
}
return gptr() == egptr() ? traits_type::eof() : traits_type::to_int_type(*gptr());
}
private:
std::string input_ = "1234\n"; ///< PIN text released to the pairing handler.
std::mutex mutex_; ///< Protects the release flag.
std::condition_variable condition_; ///< Wakes the blocked input reader.
bool released_ = false; ///< Whether the reader may consume the PIN.
};
/**
* @brief Restore the process input stream after a console-PIN test.
*/
class cin_buffer_guard {
public:
/**
* @brief Redirect standard input to a test buffer.
*
* @param replacement Stream buffer to read during the test.
*/
explicit cin_buffer_guard(std::streambuf *replacement):
original_(std::cin.rdbuf(replacement)) {
std::cin.clear();
}
/**
* @brief Restore the original standard-input buffer and stream state.
*/
~cin_buffer_guard() noexcept {
try {
std::cin.rdbuf(original_);
std::cin.clear();
} catch (const std::ios_base::failure &stream_error) {
ADD_FAILURE() << "Failed to restore the standard-input buffer: " << stream_error.what();
}
}
cin_buffer_guard(const cin_buffer_guard &) = delete;
cin_buffer_guard &operator=(const cin_buffer_guard &) = delete;
private:
std::streambuf *original_; ///< Process input buffer restored at scope exit.
};
/**
* @brief Build a pairing session that is awaiting operator approval.
*
* @param unique_id Client-controlled GameStream unique identifier.
@@ -321,8 +403,173 @@
expire_pair_sessions(std::chrono::steady_clock::time_point::max());
}
};
/**
* @brief Exercise the production pairing dispatcher through a local HTTP server.
*/
class PairingHttpHandlerTest: public BaseTest {
protected:
/**
* @brief Start an isolated plain-HTTP pairing endpoint.
*/
void SetUp() override {
BaseTest::SetUp();
expire_pair_sessions(std::chrono::steady_clock::time_point::max());
original_pin_stdin_ = config::sunshine.flags[config::flag::PIN_STDIN];
config::sunshine.flags[config::flag::PIN_STDIN] = false;
server_ = std::make_unique<SimpleWeb::Server<SimpleWeb::HTTP>>();
server_->config.port = 0;
server_->config.reuse_address = true;
server_->config.timeout_request = 5;
server_->config.timeout_content = 5;
server_->resource["^/pair$"]["GET"] = nvhttp::test_support::pair_http;
server_thread_ = std::jthread([this]() {
server_->start([this](const unsigned short assigned_port) {
port_ = assigned_port;
});
});
for (int attempt = 0; attempt < 100 && port_ == 0; ++attempt) {
std::this_thread::sleep_for(std::chrono::milliseconds {10});
}
ASSERT_NE(port_, 0) << "Pairing test server failed to start";
client_ = std::make_unique<SimpleWeb::Client<SimpleWeb::HTTP>>(std::format("localhost:{}", port_.load()));
client_->config.timeout = 5;
}
/**
* @brief Stop the endpoint and restore pairing configuration.
*/
void TearDown() override {
expire_pair_sessions(std::chrono::steady_clock::time_point::max());
if (server_) {
server_->stop();
}
if (server_thread_.joinable()) {
server_thread_.join();
}
client_.reset();
server_.reset();
config::sunshine.flags[config::flag::PIN_STDIN] = original_pin_stdin_;
BaseTest::TearDown();
}
/**
* @brief Build the query for the initial certificate phase.
*
* @param unique_id Client pairing identifier.
* @param salt Hexadecimal PIN salt.
* @return Request target for the local pairing endpoint.
*/
static std::string server_certificate_target(const std::string_view unique_id, const std::string_view salt = "ff5dc6eda99339a8a0793e216c4257c4") {
return std::format(
"/pair?uniqueid={}&phrase=getservercert&clientcert={}&salt={}&devicename=TestClient",
unique_id,
util::hex_vec(PUBLIC_CERT, true),
salt
);
}
/**
* @brief Read the response body from the local pairing endpoint.
*
* @param target Request target including query parameters.
* @return XML response body.
*/
std::string request(const std::string_view target) {
return client_->request("GET", std::string {target})->content.string();
}
/**
* @brief Wait for a request to enter the pending pairing registry.
*
* @return The operator-facing pairing identifier, or an empty string on timeout.
*/
static std::string wait_for_pending_pairing() {
for (int attempt = 0; attempt < 100; ++attempt) {
if (const auto pending = get_pending_pairings(); !pending.empty()) {
return pending.front().id;
}
std::this_thread::sleep_for(std::chrono::milliseconds {10});
}
return {};
}
private:
std::unique_ptr<SimpleWeb::Server<SimpleWeb::HTTP>> server_; ///< Local pairing HTTP server.
std::unique_ptr<SimpleWeb::Client<SimpleWeb::HTTP>> client_; ///< Client connected to the local server.
std::jthread server_thread_; ///< Thread running the local server event loop.
std::atomic<unsigned short> port_ {0}; ///< Ephemeral port assigned to the local server.
bool original_pin_stdin_; ///< Console-PIN flag restored after each test.
};
} // namespace
TEST_F(PairingHttpHandlerTest, ImmediateRequestsReturnExpectedStatus) {
EXPECT_NE(request("/pair"sv).find("status_code=\"400\""), std::string::npos);
EXPECT_NE(request("/pair?uniqueid=challenge&phrase=pairchallenge"sv).find("status_code=\"200\""), std::string::npos);
EXPECT_NE(request("/pair?uniqueid=missing"sv).find("Invalid uniqueid"), std::string::npos);
std::string pairing_id;
ASSERT_EQ(insert_pair_session(pending_session("out-of-order", "Client", "192.0.2.10"), pairing_id), pair_session_insert_e::ADDED);
EXPECT_NE(request("/pair?uniqueid=out-of-order&clientchallenge=00"sv).find("Out of order"), std::string::npos);
}
TEST_F(PairingHttpHandlerTest, WebApprovalRequestRemainsPendingUntilCancelled) {
std::packaged_task<std::string()> request_task {[this]() {
return request(server_certificate_target("pending"));
}};
auto response = request_task.get_future();
std::jthread request_thread {std::move(request_task)};
const auto pairing_id = wait_for_pending_pairing();
ASSERT_FALSE(pairing_id.empty());
EXPECT_TRUE(cancel_pairing(pairing_id));
EXPECT_NE(response.get().find("cancelled by operator"), std::string::npos);
}
TEST_F(PairingHttpHandlerTest, DuplicateAndCapacityErrorsReturnImmediately) {
std::string pairing_id;
ASSERT_EQ(insert_pair_session(pending_session("duplicate", "Original", "192.0.2.10"), pairing_id), pair_session_insert_e::ADDED);
EXPECT_NE(request(server_certificate_target("duplicate")).find("status_code=\"409\""), std::string::npos);
expire_pair_sessions(std::chrono::steady_clock::time_point::max());
for (std::size_t index = 0; index < MAX_PENDING_PAIRING_SESSIONS; ++index) {
ASSERT_EQ(
insert_pair_session(pending_session(std::format("full-{}", index), "Client", "192.0.2.10"), pairing_id),
pair_session_insert_e::ADDED
);
}
EXPECT_NE(request(server_certificate_target("overflow")).find("status_code=\"503\""), std::string::npos);
}
TEST_F(PairingHttpHandlerTest, ConsolePinCompletesOrRejectsTheCertificatePhase) {
setup(PRIVATE_KEY, PUBLIC_CERT);
config::sunshine.flags[config::flag::PIN_STDIN] = true;
std::istringstream input {"5338\n0000\n"};
const cin_buffer_guard input_guard {input.rdbuf()};
EXPECT_NE(request(server_certificate_target("console-success")).find("status_code=\"200\""), std::string::npos);
EXPECT_NE(request(server_certificate_target("console-failure", "00")).find("status_code=\"400\""), std::string::npos);
}
TEST_F(PairingHttpHandlerTest, ConsolePinReportsSessionExpiration) {
config::sunshine.flags[config::flag::PIN_STDIN] = true;
gated_input_buffer input;
const cin_buffer_guard input_guard {&input};
std::packaged_task<std::string()> request_task {[this]() {
return request(server_certificate_target("expired"));
}};
auto response = request_task.get_future();
std::jthread request_thread {std::move(request_task)};
ASSERT_FALSE(wait_for_pending_pairing().empty());
expire_pair_sessions(std::chrono::steady_clock::time_point::max());
input.release();
EXPECT_NE(response.get().find("status_code=\"408\""), std::string::npos);
}
TEST_F(PairingSessionRegistryTest, PinApprovalTargetsExplicitPairingId) {
std::string legitimate_pairing_id;
ASSERT_EQ(
@@ -460,6 +707,17 @@
const auto pending = get_pending_pairings();
ASSERT_EQ(pending.size(), 1);
EXPECT_EQ(pending.front().id, second_pairing_id);
}
TEST_F(PairingSessionRegistryTest, RemoveSessionErasesItsUniqueId) {
auto session = pending_session("removable", "Client", "192.0.2.10");
pair_session_t session_key;
session_key.client.uniqueID = session.client.uniqueID;
std::string pairing_id;
ASSERT_EQ(insert_pair_session(std::move(session), pairing_id), pair_session_insert_e::ADDED);
remove_session(session_key);
EXPECT_TRUE(get_pending_pairings().empty());
}
TEST_F(PairingSessionRegistryTest, ConcurrentInsertionAndCancellationLeaveConsistentState) {
tests/unit/test_nvhttp_client_auth.cpp +6 −4
@@ -36,8 +36,8 @@
config::sunshine.flags[config::flag::FRESH_STATE] = false;
nvhttp::test_support::reset_client_state();
std::error_code error;
std::error_code remove_error;
fs::remove(state_file, remove_error);
fs::remove(state_file, error);
}
/**
@@ -45,14 +45,15 @@
*/
void TearDown() override {
nvhttp::test_support::reset_client_state();
std::error_code error;
fs::remove(state_file, error);
std::error_code remove_error;
fs::remove(state_file, remove_error);
config::nvhttp.file_state = original_state_file;
config::sunshine.flags[config::flag::FRESH_STATE] = original_fresh_state;
BaseTest::TearDown();
}
private:
fs::path state_file; ///< Task-specific persisted state fixture.
std::string original_state_file; ///< State-file setting restored after each test.
bool original_fresh_state; ///< Fresh-state flag restored after each test.
@@ -104,6 +105,7 @@
ASSERT_FALSE(enabled_uuid.empty());
ASSERT_FALSE(disabled_uuid.empty());
ASSERT_FALSE(expired_uuid.empty());
EXPECT_EQ(nvhttp::get_all_clients().size(), 3);
EXPECT_TRUE(nvhttp::test_support::authorize_client_certificate(enabled_credentials.x509));
EXPECT_FALSE(nvhttp::test_support::authorize_client_certificate(disabled_credentials.x509));