/** * @file src/nvhttp.h * @brief Declarations for the nvhttp (GameStream) server. */ // macros #pragma once // standard includes #include #include #include #include #include #include #include #include // lib includes #include #include #include // local includes #include "crypto.h" #include "rtsp.h" #include "thread_safe.h" using namespace std::chrono_literals; /** * @brief Contains all the functions and variables related to the nvhttp (GameStream) server. */ namespace nvhttp { using args_t = SimpleWeb::CaseInsensitiveMultimap; using cmd_list_t = std::list; /** * @brief The protocol version. * @details The version of the GameStream protocol we are mocking. * @note The negative 4th number indicates to Moonlight that this is Sunshine. */ constexpr auto VERSION = "7.1.431.-1"; /** * @brief The GFE version we are replicating. */ constexpr auto GFE_VERSION = "3.23.0.74"; /** * @brief The HTTP port, as a difference from the config port. */ constexpr auto PORT_HTTP = 0; /** * @brief The HTTPS port, as a difference from the config port. */ constexpr auto PORT_HTTPS = -5; constexpr auto OTP_EXPIRE_DURATION = 180s; /** * @brief Start the nvhttp server. * @examples * nvhttp::start(); * @examples_end */ void start(); std::string get_arg(const args_t &args, const char *name, const char *default_value = nullptr); // Helper function to extract command entries cmd_list_t extract_command_entries(const nlohmann::json &j, const std::string &key); struct resolved_client_identity_t; std::shared_ptr make_launch_session(bool host_audio, bool input_only, const args_t &args, const crypto::named_cert_t *named_cert_p, const resolved_client_identity_t *resolved_client_identity = nullptr); /** * @brief Setup the nvhttp server. * @param pkey * @param cert */ void setup(const std::string &pkey, const std::string &cert); // Remote Input has no retained resource; its catalogue ownership ends with // the exact transport generation that created it. void notify_remote_input_transport_lost(std::string_view client_uuid, std::uint64_t generation); void notify_remote_monitor_released(std::string_view client_uuid, std::uint64_t generation); class SunshineHTTPS: public SimpleWeb::HTTPS { public: SunshineHTTPS(boost::asio::io_context &io_context, boost::asio::ssl::context &ctx): SimpleWeb::HTTPS(io_context, ctx) { } virtual ~SunshineHTTPS() { // Gracefully shutdown the TLS connection SimpleWeb::error_code ec; shutdown(ec); } }; enum class PAIR_PHASE { NONE, ///< Sunshine is not in a pairing phase GETSERVERCERT, ///< Sunshine is in the get server certificate phase CLIENTCHALLENGE, ///< Sunshine is in the client challenge phase SERVERCHALLENGERESP, ///< Sunshine is in the server challenge response phase CLIENTPAIRINGSECRET ///< Sunshine is in the client pairing secret phase }; struct pair_session_t { std::chrono::steady_clock::time_point created_at = std::chrono::steady_clock::now(); struct { std::string uniqueID = {}; std::string cert = {}; std::string name = {}; } client; std::unique_ptr cipher_key = {}; std::vector clienthash = {}; std::string serversecret = {}; std::string serverchallenge = {}; struct { util::Either< std::shared_ptr::Response>, std::shared_ptr::Response>> response; std::string salt = {}; } async_insert_pin; /** * @brief used as a security measure to prevent out of order calls */ PAIR_PHASE last_phase = PAIR_PHASE::NONE; }; /** * @brief removes the temporary pairing session * @param sess */ void remove_session(const pair_session_t &sess); /** * @brief Pair, phase 1 * * Moonlight will send a salt and client certificate, we'll also need the user provided pin. * * PIN and SALT will be used to derive a shared AES key that needs to be stored * in order to be used to decrypt_symmetric in the next phases. * * At this stage we only have to send back our public certificate. */ void getservercert(pair_session_t &sess, boost::property_tree::ptree &tree, const std::string &pin); /** * @brief Pair, phase 2 * * Using the AES key that we generated in phase 1 we have to decrypt the client challenge, * * We generate a SHA256 hash with the following: * - Decrypted challenge * - Server certificate signature * - Server secret: a randomly generated secret * * The hash + server_challenge will then be AES encrypted and sent as the `challengeresponse` in the returned XML */ void clientchallenge(pair_session_t &sess, boost::property_tree::ptree &tree, const std::string &challenge); /** * @brief Pair, phase 3 * * Moonlight will send back a `serverchallengeresp`: an AES encrypted client hash, * we have to send back the `pairingsecret`: * using our private key we have to sign the certificate_signature + server_secret (generated in phase 2) */ void serverchallengeresp(pair_session_t &sess, boost::property_tree::ptree &tree, const std::string &encrypted_response); /** * @brief Pair, phase 4 (final) * * We now have to use everything we exchanged before in order to verify and finally pair the clients * * We'll check the client_hash obtained at phase 3, it should contain the following: * - The original server_challenge * - The signature of the X509 client_cert * - The unencrypted client_pairing_secret * We'll check that SHA256(server_challenge + client_public_cert_signature + client_secret) == client_hash * * Then using the client certificate public key we should be able to verify that * the client secret has been signed by Moonlight */ void clientpairingsecret( pair_session_t &sess, const std::shared_ptr> &pending_certs, boost::property_tree::ptree &tree, const std::string &client_pairing_secret ); void clientpairingsecret(pair_session_t &sess, boost::property_tree::ptree &tree, const std::string &client_pairing_secret); /** * @brief Compare the user supplied pin to the Moonlight pin. * @param pin The user supplied pin. * @param name The user supplied name. * @return `true` if the pin is correct, `false` otherwise. * @examples * bool pin_status = nvhttp::pin("1234", "laptop"); * @examples_end */ bool pin(std::string pin, std::string name); /** * @brief Pick the client label used for display-facing behavior. */ std::string display_client_name_for_session(const std::string &paired_name, const std::string &device_name, const std::string &host_name); std::string request_otp(const std::string &passphrase, const std::string &deviceName); /** * @brief Remove single client. * @param uuid The UUID of the client to remove. * @examples * nvhttp::unpair_client("4D7BB2DD-5704-A405-B41C-891A022932E1"); * @examples_end */ bool unpair_client(std::string_view uuid); bool has_client_uuid(std::string_view uuid); bool get_client_always_use_virtual_display(const std::string &uuid); /** * @brief Get all paired clients. * @return The list of all paired clients. * @examples * nlohmann::json clients = nvhttp::get_all_clients(); * @examples_end */ nlohmann::json get_all_clients(); nlohmann::json get_remote_display_layout(); bool set_remote_display_layout(const nlohmann::json &layout, std::string &error); /** * @brief Record a client's last seen time (seconds since epoch). */ void mark_client_last_seen(const std::string &uuid); /** * @brief Update stored settings for a paired client. * @return True if the client was found and updated. */ bool update_device_info( const std::string &uuid, const std::string &name, const std::string &display_mode, const std::string &output_name_override, bool always_use_virtual_display, const std::string &virtual_display_mode, const std::string &virtual_display_layout, std::optional> config_overrides, bool prefer_10bit_sdr, std::optional hdr_profile ); /** * @brief Disconnect any active sessions for a paired client. * @return True if one or more sessions were stopped. */ bool disconnect_client(const std::string &uuid); /** * @brief Whether a paired client is opted into 10-bit SDR instead of HDR. */ bool get_client_prefer_10bit_sdr(const std::string &uuid); /** * @brief Get a copy of a client's runtime config overrides. */ std::unordered_map get_client_config_overrides(const std::string &uuid); /** * @brief Encoder capabilities safe to expose to the browser WebRTC UI. * @details Resolves the same selected-adapter capability view used for HTTP * protocol advertisement. A false `probe_complete` means the host * has not safely verified an encoder for the current capture target. */ struct web_stream_capabilities_t { bool probe_complete {false}; bool h264 {false}; bool hevc {false}; bool av1 {false}; bool hevc_hdr {false}; bool av1_hdr {false}; }; web_stream_capabilities_t get_web_stream_capabilities(); /** * @brief Serialize shared stream start and final teardown across RTSP and WebRTC. * * Acquire after the RTSP launch-request mutex, before protocol capture locks. * Starts hold it from their first idle-state observation through pending-owner * publication; final teardown holds it through its shared cleanup decision. */ std::mutex &stream_lifecycle_mutex(); /** * @brief Persist a per-client HDR color profile selection (Windows only). * @return True if the client was found and updated. */ bool set_client_hdr_profile(const std::string &uuid, const std::string &hdr_profile); /** * @brief Remove all paired clients. * @return True only when the cleared machine pairing state was persisted. * @examples * nvhttp::erase_all_clients(); * @examples_end */ bool erase_all_clients(); /** * @brief Stops a session. * * @param session The session * @param[in] graceful Whether to stop gracefully */ void stop_session(stream::session_t &session, bool graceful); /** * @brief Finds and stop session. * * @param[in] uuid The uuid string * @param[in] graceful Whether to stop gracefully */ bool find_and_stop_session(const std::string &uuid, bool graceful); /** * @brief Update device info associated to the session * * @param session The session * @param[in] name New name * @param[in] newPerm New permission */ void update_session_info(stream::session_t &session, const std::string &name, const crypto::PERM newPerm); /** * @brief Finds and udpate session information. * * @param[in] uuid The uuid string * @param[in] name New name * @param[in] newPerm New permission */ bool find_and_udpate_session_info(const std::string &uuid, const std::string &name, const crypto::PERM newPerm); /** * @brief Update device info * * @param[in] uuid The uuid string * @param[in] name New name * @param[in] do_cmds The do commands * @param[in] undo_cmds The undo commands * @param[in] newPerm New permission * @param[in] enable_legacy_ordering Enable legacy ordering * @param[in] allow_client_commands Allow client commands * @param[in] always_use_virtual_display Always use virtual display * @param[in] virtual_display_mode Virtual display mode override * @param[in] virtual_display_layout Virtual display layout override * * @return Whether the update is successful */ bool update_device_info( const std::string &uuid, const std::string &name, const std::string &display_mode, const std::string &output_name_override, const cmd_list_t &do_cmds, const cmd_list_t &undo_cmds, const crypto::PERM newPerm, const bool enable_legacy_ordering, const bool allow_client_commands, const bool always_use_virtual_display, const std::string &virtual_display_mode, const std::string &virtual_display_layout, const bool prefer_10bit_sdr ); /** * @brief Persist current nvhttp-related state (paired clients, update subsystem markers, etc.). * @note Exposed so subsystems (e.g. update) can trigger a save after mutating persisted fields. */ bool save_state(); } // namespace nvhttp