From b29ed4c67e360ede467ec84d069da97ba7693c2d Mon Sep 17 00:00:00 2001 From: Yukino Song Date: Sat, 14 Sep 2024 05:20:52 +0800 Subject: [PATCH 1/9] Prepare for adding permission system --- src/crypto.cpp | 11 ++-- src/crypto.h | 14 ++++- src/nvhttp.cpp | 155 +++++++++++++++++++++++++++---------------------- 3 files changed, 104 insertions(+), 76 deletions(-) diff --git a/src/crypto.cpp b/src/crypto.cpp index b16ab08a..7f4417da 100644 --- a/src/crypto.cpp +++ b/src/crypto.cpp @@ -11,11 +11,11 @@ namespace crypto { cert_chain_t::cert_chain_t(): _certs {}, _cert_ctx { X509_STORE_CTX_new() } {} void - cert_chain_t::add(x509_t &&cert) { + cert_chain_t::add(p_named_cert_t& named_cert_p) { x509_store_t x509_store { X509_STORE_new() }; - X509_STORE_add_cert(x509_store.get(), cert.get()); - _certs.emplace_back(std::make_pair(std::move(cert), std::move(x509_store))); + X509_STORE_add_cert(x509_store.get(), x509(named_cert_p->cert).get()); + _certs.emplace_back(std::make_pair(named_cert_p, std::move(x509_store))); } void cert_chain_t::clear() { @@ -52,9 +52,9 @@ namespace crypto { * @return nullptr if the certificate is valid, otherwise an error string. */ const char * - cert_chain_t::verify(x509_t::element_type *cert) { + cert_chain_t::verify(x509_t::element_type *cert, p_named_cert_t& named_cert_out) { int err_code = 0; - for (auto &[_, x509_store] : _certs) { + for (auto &[named_cert_p, x509_store] : _certs) { auto fg = util::fail_guard([this]() { X509_STORE_CTX_cleanup(_cert_ctx.get()); }); @@ -70,6 +70,7 @@ namespace crypto { auto err = X509_verify_cert(_cert_ctx.get()); if (err == 1) { + named_cert_out = named_cert_p; return nullptr; } diff --git a/src/crypto.h b/src/crypto.h index 859c6675..45c876f6 100644 --- a/src/crypto.h +++ b/src/crypto.h @@ -34,6 +34,14 @@ namespace crypto { using pkey_ctx_t = util::safe_ptr; using bignum_t = util::safe_ptr; + struct named_cert_t { + std::string name; + std::string uuid; + std::string cert; + }; + + using p_named_cert_t = std::shared_ptr; + /** * @brief Hashes the given plaintext using SHA-256. * @param plaintext @@ -76,16 +84,16 @@ namespace crypto { KITTY_DECL_CONSTR(cert_chain_t) void - add(x509_t &&cert); + add(p_named_cert_t& named_cert_p); void clear(); const char * - verify(x509_t::element_type *cert); + verify(x509_t::element_type *cert, p_named_cert_t& named_cert_out); private: - std::vector> _certs; + std::vector> _certs; x509_store_ctx_t _cert_ctx; }; diff --git a/src/nvhttp.cpp b/src/nvhttp.cpp index d27d66af..3e5d29e4 100644 --- a/src/nvhttp.cpp +++ b/src/nvhttp.cpp @@ -46,6 +46,14 @@ namespace nvhttp { namespace fs = std::filesystem; namespace pt = boost::property_tree; + using p_named_cert_t = crypto::p_named_cert_t; + + struct client_t { + std::vector named_devices; + }; + + struct pair_session_t; + crypto::cert_chain_t cert_chain; static std::string one_time_pin; static std::string otp_passphrase; @@ -66,6 +74,15 @@ namespace nvhttp { class SunshineHTTPSServer: public SimpleWeb::ServerBase { public: + class ApolloSession: public Session { + public: + bool verified = false; + crypto::named_cert_t* named_cert = nullptr; + void* userp = nullptr; + + template + ApolloSession(Args&&... args): Session(std::forward(args)...) {} + }; SunshineHTTPSServer(const std::string &certification_file, const std::string &private_key_file): ServerBase::ServerBase(443), context(boost::asio::ssl::context::tls_server) { @@ -76,7 +93,7 @@ namespace nvhttp { context.use_private_key_file(private_key_file, boost::asio::ssl::context::pem); } - std::function verify; + std::function verify; std::function, std::shared_ptr)> on_verify_failed; protected: @@ -106,7 +123,7 @@ namespace nvhttp { if (ec != SimpleWeb::error::operation_aborted) this->accept(); - auto session = std::make_shared(config.max_request_streambuf_size, connection); + auto session = std::make_shared(config.max_request_streambuf_size, connection); if (!ec) { boost::asio::ip::tcp::no_delay option(true); @@ -120,7 +137,7 @@ namespace nvhttp { if (!lock) return; if (!ec) { - if (verify && !verify(session->connection->socket->native_handle())) + if (verify && !verify(session.get(), session->connection->socket->native_handle())) this->write(session, on_verify_failed); else this->read(session); @@ -137,22 +154,13 @@ namespace nvhttp { using https_server_t = SunshineHTTPSServer; using http_server_t = SimpleWeb::Server; + using https_session_t = SunshineHTTPSServer::ApolloSession; 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_devices; - }; - struct pair_session_t { struct { std::string uniqueID; @@ -225,12 +233,27 @@ namespace nvhttp { 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)); + std::unordered_set unique_certs; + std::unordered_map name_counts; + for (auto &named_cert_p : client.named_devices) { + if (unique_certs.insert(named_cert_p->cert).second) { + pt::ptree named_cert_node; + std::string base_name = named_cert_p->name; + // Remove existing pending id if present + size_t pos = base_name.find(" ("); + if (pos != std::string::npos) { + base_name = base_name.substr(0, pos); + } + int count = name_counts[base_name]++; + std::string final_name = base_name; + if (count > 0) { + final_name += " (" + std::to_string(count + 1) + ")"; + } + named_cert_node.put("name"s, final_name); + named_cert_node.put("cert"s, named_cert_p->cert); + named_cert_node.put("uuid"s, named_cert_p->uuid); + named_cert_nodes.push_back(std::make_pair(""s, named_cert_node)); + } } root.add_child("root.named_devices"s, named_cert_nodes); @@ -282,11 +305,11 @@ namespace nvhttp { 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(); - named_cert.uuid = uuid_util::uuid_t::generate().string(); - client.named_devices.emplace_back(named_cert); + auto named_cert_p = std::make_shared(); + named_cert_p->name = ""s; + named_cert_p->cert = el.get_value(); + named_cert_p->uuid = uuid_util::uuid_t::generate().string(); + client.named_devices.emplace_back(named_cert_p); } } } @@ -294,34 +317,31 @@ namespace nvhttp { 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(); - named_cert.cert = el.get_child("cert").get_value(); - named_cert.uuid = el.get_child("uuid").get_value(); - client.named_devices.emplace_back(named_cert); + auto named_cert_p = std::make_shared(); + named_cert_p->name = el.get_child("name").get_value(); + named_cert_p->cert = el.get_child("cert").get_value(); + named_cert_p->uuid = el.get_child("uuid").get_value(); + client.named_devices.emplace_back(named_cert_p); } } // 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)); + cert_chain.add(named_cert); } client_root = client; } void - add_authorized_client(const std::string &name, std::string &&cert) { + add_authorized_client(const p_named_cert_t& named_cert_p) { 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); + client.named_devices.emplace_back(named_cert_p); if (!config::sunshine.flags[config::flag::FRESH_STATE]) { save_state(); + load_state(); } } @@ -458,7 +478,7 @@ namespace nvhttp { } void - clientpairingsecret(std::shared_ptr> &add_cert, pair_session_t &sess, pt::ptree &tree, const args_t &args) { + clientpairingsecret(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); @@ -487,11 +507,20 @@ namespace nvhttp { // 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)); + + auto named_cert_p = std::make_shared(); + named_cert_p->name = client.name; + for (char& c : named_cert_p->name) { + if (c == '(') c = '['; + else if (c == ')') c = ']'; + } + named_cert_p->cert = std::move(client.cert); + named_cert_p->uuid = uuid_util::uuid_t::generate().string(); auto it = map_id_sess.find(client.uniqueID); - add_authorized_client(client.name, std::move(client.cert)); map_id_sess.erase(it); + + add_authorized_client(named_cert_p); } else { map_id_sess.erase(client.uniqueID); @@ -557,7 +586,7 @@ namespace nvhttp { template void - pair(std::shared_ptr> &add_cert, std::shared_ptr::Response> response, std::shared_ptr::Request> request) { + pair(std::shared_ptr::Response> response, std::shared_ptr::Request> request) { print_req(request); pt::ptree tree; @@ -673,7 +702,7 @@ namespace nvhttp { serverchallengeresp(sess_it->second, tree, args); } else if (it = args.find("clientpairingsecret"); it != std::end(args)) { - clientpairingsecret(add_cert, sess_it->second, tree, args); + clientpairingsecret(sess_it->second, tree, args); } else { tree.put("root..status_code", 404); @@ -849,10 +878,10 @@ namespace nvhttp { get_all_clients() { pt::ptree named_cert_nodes; client_t &client = client_root; - for (auto &named_cert : client.named_devices) { + for (auto &named_cert_p : 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_node.put("name"s, named_cert_p->name); + named_cert_node.put("uuid"s, named_cert_p->uuid); named_cert_nodes.push_back(std::make_pair(""s, named_cert_node)); } @@ -1133,8 +1162,6 @@ namespace nvhttp { 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>(30); - // resume doesn't always get the parameter "localAudioPlayMode" // launch will store it in host_audio bool host_audio {}; @@ -1143,43 +1170,34 @@ namespace nvhttp { http_server_t http_server; // Verify certificates after establishing connection - https_server.verify = [add_cert](SSL *ssl) { + https_server.verify = [](https_session_t* session, SSL *ssl) { crypto::x509_t x509 { SSL_get_peer_certificate(ssl) }; if (!x509) { BOOST_LOG(info) << "unknown -- denied"sv; - return 0; + return false; } - 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); + BOOST_LOG(debug) << subject_name << " -- "sv << (session->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()); + p_named_cert_t named_cert_p; + auto err_str = cert_chain.verify(x509.get(), named_cert_p); if (err_str) { BOOST_LOG(warning) << "SSL Verification error :: "sv << err_str; - return verified; + return session->verified; } - verified = 1; + session->verified = true; - return verified; + BOOST_LOG(info) << "Device " << named_cert_p->name << " verified!"; + + return session->verified; }; https_server.on_verify_failed = [](resp_https_t resp, req_https_t req) { @@ -1199,7 +1217,7 @@ namespace nvhttp { https_server.default_resource["GET"] = not_found; https_server.resource["^/serverinfo$"]["GET"] = serverinfo; - https_server.resource["^/pair$"]["GET"] = [&add_cert](auto resp, auto req) { pair(add_cert, resp, req); }; + https_server.resource["^/pair$"]["GET"] = pair; 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); }; @@ -1212,7 +1230,7 @@ namespace nvhttp { http_server.default_resource["GET"] = not_found; http_server.resource["^/serverinfo$"]["GET"] = serverinfo; - http_server.resource["^/pair$"]["GET"] = [&add_cert](auto resp, auto req) { pair(add_cert, resp, req); }; + http_server.resource["^/pair$"]["GET"] = pair; http_server.config.reuse_address = true; http_server.config.address = net::af_to_any_address_string(address_family); @@ -1265,6 +1283,7 @@ namespace nvhttp { client_root = client; cert_chain.clear(); save_state(); + load_state(); } int @@ -1272,7 +1291,7 @@ namespace nvhttp { int removed = 0; client_t &client = client_root; for (auto it = client.named_devices.begin(); it != client.named_devices.end();) { - if ((*it).uuid == uuid) { + if ((*it)->uuid == uuid) { it = client.named_devices.erase(it); removed++; } From aac75c08d89ca0cfec1269d2c6bd97d798d72c5c Mon Sep 17 00:00:00 2001 From: Yukino Song Date: Sun, 15 Sep 2024 07:14:58 +0800 Subject: [PATCH 2/9] Prepare for adding permission system --- .gitmodules | 2 +- src/nvhttp.cpp | 33 ++++++++++++++------------------- third-party/Simple-Web-Server | 2 +- 3 files changed, 16 insertions(+), 21 deletions(-) diff --git a/.gitmodules b/.gitmodules index b667a8d0..d373dbce 100644 --- a/.gitmodules +++ b/.gitmodules @@ -32,7 +32,7 @@ branch = sdk [submodule "third-party/Simple-Web-Server"] path = third-party/Simple-Web-Server - url = https://gitlab.com/eidheim/Simple-Web-Server.git + url = https://github.com/ClassicOldSong/Simple-Web-Server branch = master [submodule "third-party/TPCircularBuffer"] path = third-party/TPCircularBuffer diff --git a/src/nvhttp.cpp b/src/nvhttp.cpp index 3e5d29e4..868a0f7c 100644 --- a/src/nvhttp.cpp +++ b/src/nvhttp.cpp @@ -74,15 +74,6 @@ namespace nvhttp { class SunshineHTTPSServer: public SimpleWeb::ServerBase { public: - class ApolloSession: public Session { - public: - bool verified = false; - crypto::named_cert_t* named_cert = nullptr; - void* userp = nullptr; - - template - ApolloSession(Args&&... args): Session(std::forward(args)...) {} - }; SunshineHTTPSServer(const std::string &certification_file, const std::string &private_key_file): ServerBase::ServerBase(443), context(boost::asio::ssl::context::tls_server) { @@ -93,7 +84,7 @@ namespace nvhttp { context.use_private_key_file(private_key_file, boost::asio::ssl::context::pem); } - std::function verify; + std::function, SSL*)> verify; std::function, std::shared_ptr)> on_verify_failed; protected: @@ -123,7 +114,7 @@ namespace nvhttp { if (ec != SimpleWeb::error::operation_aborted) this->accept(); - auto session = std::make_shared(config.max_request_streambuf_size, connection); + auto session = std::make_shared(config.max_request_streambuf_size, connection); if (!ec) { boost::asio::ip::tcp::no_delay option(true); @@ -137,7 +128,7 @@ namespace nvhttp { if (!lock) return; if (!ec) { - if (verify && !verify(session.get(), session->connection->socket->native_handle())) + if (verify && !verify(session->request, session->connection->socket->native_handle())) this->write(session, on_verify_failed); else this->read(session); @@ -154,7 +145,6 @@ namespace nvhttp { using https_server_t = SunshineHTTPSServer; using http_server_t = SimpleWeb::Server; - using https_session_t = SunshineHTTPSServer::ApolloSession; struct conf_intern_t { std::string servercert; @@ -767,8 +757,11 @@ namespace nvhttp { serverinfo(std::shared_ptr::Response> response, std::shared_ptr::Request> request) { print_req(request); + int pair_status = 0; if constexpr (std::is_same_v) { + BOOST_LOG(info) << "Device " << ((crypto::named_cert_t*)request->userp.get())->name << " getting server info!!!"; + auto args = request->parse_query_string(); auto clientID = args.find("uniqueid"s); @@ -1170,34 +1163,36 @@ namespace nvhttp { http_server_t http_server; // Verify certificates after establishing connection - https_server.verify = [](https_session_t* session, SSL *ssl) { + https_server.verify = [](req_https_t req, SSL *ssl) { crypto::x509_t x509 { SSL_get_peer_certificate(ssl) }; if (!x509) { BOOST_LOG(info) << "unknown -- denied"sv; return false; } + bool verified = false; + 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 << (session->verified ? "verified"sv : "denied"sv); + BOOST_LOG(debug) << subject_name << " -- "sv << (verified ? "verified"sv : "denied"sv); }); p_named_cert_t named_cert_p; auto err_str = cert_chain.verify(x509.get(), named_cert_p); if (err_str) { BOOST_LOG(warning) << "SSL Verification error :: "sv << err_str; - - return session->verified; + return verified; } - session->verified = true; + verified = true; + req->userp = named_cert_p; BOOST_LOG(info) << "Device " << named_cert_p->name << " verified!"; - return session->verified; + return true; }; https_server.on_verify_failed = [](resp_https_t resp, req_https_t req) { diff --git a/third-party/Simple-Web-Server b/third-party/Simple-Web-Server index 4abe3491..ab36f157 160000 --- a/third-party/Simple-Web-Server +++ b/third-party/Simple-Web-Server @@ -1 +1 @@ -Subproject commit 4abe3491582b56c0b18fc35278aad33b2db29cdc +Subproject commit ab36f1576bdc05ec0c017a74c4edd7f48109b302 From 2f105081b8d22cab1a83083bc0cc7fb34a1efe81 Mon Sep 17 00:00:00 2001 From: Reimu NotMoe Date: Sun, 15 Sep 2024 11:47:08 +0800 Subject: [PATCH 3/9] Fix typo in locale --- src_assets/common/assets/web/public/assets/locale/en.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src_assets/common/assets/web/public/assets/locale/en.json b/src_assets/common/assets/web/public/assets/locale/en.json index 634665d1..8e5194fb 100644 --- a/src_assets/common/assets/web/public/assets/locale/en.json +++ b/src_assets/common/assets/web/public/assets/locale/en.json @@ -93,7 +93,7 @@ "client_card": { "clients": "Clients", "clients_desc": "Clients that are specifically tuned to work the best with Apollo", - "generic_moonlight_clients_desc": "Gereric Moonlight clients are still usable with Apollo." + "generic_moonlight_clients_desc": "Generic Moonlight clients are still usable with Apollo." }, "config": { "adapter_name": "Adapter Name", From 4a0d425b7c29a6b72d918d94d540509eb50e666c Mon Sep 17 00:00:00 2001 From: Yukino Song Date: Sun, 15 Sep 2024 13:02:10 +0800 Subject: [PATCH 4/9] Get device UUID from local assigned value --- src/nvhttp.cpp | 20 ++++++++++++++------ src/platform/windows/virtual_display.cpp | 12 +----------- src/platform/windows/virtual_display.h | 2 +- src/process.cpp | 5 ++++- 4 files changed, 20 insertions(+), 19 deletions(-) diff --git a/src/nvhttp.cpp b/src/nvhttp.cpp index 868a0f7c..9ee522f8 100644 --- a/src/nvhttp.cpp +++ b/src/nvhttp.cpp @@ -336,7 +336,7 @@ namespace nvhttp { } std::shared_ptr - make_launch_session(bool host_audio, const args_t &args) { + make_launch_session(bool host_audio, const args_t &args, const std::string& uuid) { auto launch_session = std::make_shared(); launch_session->id = ++session_id_counter; @@ -356,7 +356,7 @@ namespace nvhttp { x++; } launch_session->device_name = (get_arg(args, "devicename", "unknown")); - launch_session->unique_id = (get_arg(args, "uniqueid", "unknown")); + launch_session->unique_id = uuid; 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")); @@ -533,6 +533,11 @@ namespace nvhttp { static auto constexpr to_string = "NONE"sv; }; + inline crypto::named_cert_t* + get_verified_cert(req_https_t request) { + return (crypto::named_cert_t*)request->userp.get(); + } + template void print_req(std::shared_ptr::Request> request) { @@ -757,10 +762,9 @@ namespace nvhttp { serverinfo(std::shared_ptr::Response> response, std::shared_ptr::Request> request) { print_req(request); - int pair_status = 0; if constexpr (std::is_same_v) { - BOOST_LOG(info) << "Device " << ((crypto::named_cert_t*)request->userp.get())->name << " getting server info!!!"; + BOOST_LOG(info) << "Device " << get_verified_cert(request)->name << " getting server info!!!"; auto args = request->parse_query_string(); auto clientID = args.find("uniqueid"s); @@ -914,6 +918,8 @@ namespace nvhttp { launch(bool &host_audio, resp_https_t response, req_https_t request) { print_req(request); + auto named_cert_p = get_verified_cert(request); + pt::ptree tree; auto g = util::fail_guard([&]() { std::ostringstream data; @@ -954,7 +960,7 @@ namespace nvhttp { } host_audio = util::from_view(get_arg(args, "localAudioPlayMode")); - auto launch_session = make_launch_session(host_audio, args); + auto launch_session = make_launch_session(host_audio, args, named_cert_p->uuid); auto encryption_mode = net::encryption_mode_for_address(request->remote_endpoint().address()); if (!launch_session->rtsp_cipher && encryption_mode == config::ENCRYPTION_MODE_MANDATORY) { @@ -1011,6 +1017,8 @@ namespace nvhttp { resume(bool &host_audio, resp_https_t response, req_https_t request) { print_req(request); + auto named_cert_p = get_verified_cert(request); + pt::ptree tree; auto g = util::fail_guard([&]() { std::ostringstream data; @@ -1071,7 +1079,7 @@ namespace nvhttp { } } - auto launch_session = make_launch_session(host_audio, args); + auto launch_session = make_launch_session(host_audio, args, named_cert_p->uuid); auto encryption_mode = net::encryption_mode_for_address(request->remote_endpoint().address()); if (!launch_session->rtsp_cipher && encryption_mode == config::ENCRYPTION_MODE_MANDATORY) { diff --git a/src/platform/windows/virtual_display.cpp b/src/platform/windows/virtual_display.cpp index 4c77790c..6036e170 100644 --- a/src/platform/windows/virtual_display.cpp +++ b/src/platform/windows/virtual_display.cpp @@ -294,7 +294,7 @@ std::wstring createVirtualDisplay( uint32_t width, uint32_t height, uint32_t fps, - GUID& guid + const GUID& guid ) { if (SUDOVDA_DRIVER_HANDLE == INVALID_HANDLE_VALUE) { return std::wstring(); @@ -308,16 +308,6 @@ std::wstring createVirtualDisplay( s_client_name = s_app_name; } - if (s_client_uid && strcmp(s_client_uid, "unknown")) { - size_t len = strlen(s_client_uid); - if (len > sizeof(GUID)) { - len = sizeof(GUID); - } - memcpy((void*)&guid, (void*)s_client_uid, len); - } else { - s_client_uid = "unknown"; - } - VIRTUAL_DISPLAY_ADD_OUT output; if (!AddVirtualDisplay(SUDOVDA_DRIVER_HANDLE, width, height, fps, guid, s_client_name, s_client_uid, output)) { printf("[SUDOVDA] Failed to add virtual display.\n"); diff --git a/src/platform/windows/virtual_display.h b/src/platform/windows/virtual_display.h index b4d726be..9311d5fc 100644 --- a/src/platform/windows/virtual_display.h +++ b/src/platform/windows/virtual_display.h @@ -39,7 +39,7 @@ namespace VDISPLAY { uint32_t width, uint32_t height, uint32_t fps, - GUID& guid + const GUID& guid ); bool removeVirtualDisplay(const GUID& guid); } diff --git a/src/process.cpp b/src/process.cpp index 3d076fc2..c4c43bd4 100644 --- a/src/process.cpp +++ b/src/process.cpp @@ -29,6 +29,7 @@ #include "system_tray.h" #include "utility.h" #include "video.h" +#include "uuid.h" #ifdef _WIN32 // from_utf8() string conversion function @@ -205,7 +206,9 @@ namespace proc { VDISPLAY::setRenderAdapterByName(platf::from_utf8(config::video.adapter_name)); } - memcpy(&launch_session->display_guid, &http::uuid, sizeof(GUID)); + auto device_uuid = uuid_util::uuid_t::parse(launch_session->unique_id); + + memcpy(&launch_session->display_guid, &device_uuid, sizeof(GUID)); std::wstring vdisplayName = VDISPLAY::createVirtualDisplay( launch_session->unique_id.c_str(), From 038464ad45bff545c7293b03015d5deabc4f21f1 Mon Sep 17 00:00:00 2001 From: Yukino Song Date: Sun, 15 Sep 2024 13:06:35 +0800 Subject: [PATCH 5/9] Redirect to login after applied configs --- src_assets/common/assets/web/config.html | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src_assets/common/assets/web/config.html b/src_assets/common/assets/web/config.html index 9ee8117e..4a9a4f3f 100644 --- a/src_assets/common/assets/web/config.html +++ b/src_assets/common/assets/web/config.html @@ -405,6 +405,17 @@ }, 5000); fetch("/api/restart", { method: "POST" + }) + .then((resp) => { + if (resp.status !== 200) { + location.href = './login' + return + } + }) + .catch((e) => { + location.href = './login' + console.error(e) + return }); } }); From 306b252205fd006fb5ad546a6ce79d12cf115532 Mon Sep 17 00:00:00 2001 From: Yukino Song Date: Sun, 15 Sep 2024 14:24:55 +0800 Subject: [PATCH 6/9] Get device name from local value for vdisplay --- src/nvhttp.cpp | 9 +++++---- src/platform/windows/virtual_display.cpp | 9 --------- src/platform/windows/virtual_display.h | 1 - src/process.cpp | 1 - 4 files changed, 5 insertions(+), 15 deletions(-) diff --git a/src/nvhttp.cpp b/src/nvhttp.cpp index 9ee522f8..7981d8d9 100644 --- a/src/nvhttp.cpp +++ b/src/nvhttp.cpp @@ -336,7 +336,7 @@ namespace nvhttp { } std::shared_ptr - make_launch_session(bool host_audio, const args_t &args, const std::string& uuid) { + make_launch_session(bool host_audio, const args_t &args, const std::string& device_name, const std::string& uuid) { auto launch_session = std::make_shared(); launch_session->id = ++session_id_counter; @@ -355,7 +355,8 @@ namespace nvhttp { if (x == 2) launch_session->fps = atoi(segment.c_str()); x++; } - launch_session->device_name = (get_arg(args, "devicename", "unknown")); + + launch_session->device_name = device_name.empty() ? "ApolloDisplay"s : device_name; launch_session->unique_id = uuid; launch_session->appid = util::from_view(get_arg(args, "appid", "unknown")); launch_session->enable_sops = util::from_view(get_arg(args, "sops", "0")); @@ -960,7 +961,7 @@ namespace nvhttp { } host_audio = util::from_view(get_arg(args, "localAudioPlayMode")); - auto launch_session = make_launch_session(host_audio, args, named_cert_p->uuid); + auto launch_session = make_launch_session(host_audio, args, named_cert_p->name, named_cert_p->uuid); auto encryption_mode = net::encryption_mode_for_address(request->remote_endpoint().address()); if (!launch_session->rtsp_cipher && encryption_mode == config::ENCRYPTION_MODE_MANDATORY) { @@ -1079,7 +1080,7 @@ namespace nvhttp { } } - auto launch_session = make_launch_session(host_audio, args, named_cert_p->uuid); + auto launch_session = make_launch_session(host_audio, args, named_cert_p->name, named_cert_p->uuid); auto encryption_mode = net::encryption_mode_for_address(request->remote_endpoint().address()); if (!launch_session->rtsp_cipher && encryption_mode == config::ENCRYPTION_MODE_MANDATORY) { diff --git a/src/platform/windows/virtual_display.cpp b/src/platform/windows/virtual_display.cpp index 6036e170..f2b77cec 100644 --- a/src/platform/windows/virtual_display.cpp +++ b/src/platform/windows/virtual_display.cpp @@ -290,7 +290,6 @@ bool setRenderAdapterByName(const std::wstring& adapterName) { std::wstring createVirtualDisplay( const char* s_client_uid, const char* s_client_name, - const char* s_app_name, uint32_t width, uint32_t height, uint32_t fps, @@ -300,14 +299,6 @@ std::wstring createVirtualDisplay( return std::wstring(); } - if (!s_app_name || !strlen(s_app_name) || !strcmp(s_app_name, "unknown")) { - s_app_name = "ApolloVDisp"; - } - - if (!s_client_name || !strlen(s_client_name) || !strcmp(s_client_name, "unknown")) { - s_client_name = s_app_name; - } - VIRTUAL_DISPLAY_ADD_OUT output; if (!AddVirtualDisplay(SUDOVDA_DRIVER_HANDLE, width, height, fps, guid, s_client_name, s_client_uid, output)) { printf("[SUDOVDA] Failed to add virtual display.\n"); diff --git a/src/platform/windows/virtual_display.h b/src/platform/windows/virtual_display.h index 9311d5fc..0d384428 100644 --- a/src/platform/windows/virtual_display.h +++ b/src/platform/windows/virtual_display.h @@ -35,7 +35,6 @@ namespace VDISPLAY { std::wstring createVirtualDisplay( const char* s_client_uid, const char* s_client_name, - const char* s_app_name, uint32_t width, uint32_t height, uint32_t fps, diff --git a/src/process.cpp b/src/process.cpp index c4c43bd4..610aa681 100644 --- a/src/process.cpp +++ b/src/process.cpp @@ -213,7 +213,6 @@ namespace proc { std::wstring vdisplayName = VDISPLAY::createVirtualDisplay( launch_session->unique_id.c_str(), launch_session->device_name.c_str(), - _app.name.c_str(), render_width, render_height, launch_session->fps, From aa4d38e99201e6173be9f7ed1eec77129d23e18b Mon Sep 17 00:00:00 2001 From: Yukino Song Date: Sun, 15 Sep 2024 18:42:54 +0800 Subject: [PATCH 7/9] Implement permission management for nvhttp --- src/crypto.h | 15 +++++ src/nvhttp.cpp | 148 +++++++++++++++++++++++++++++++++++++++---------- 2 files changed, 135 insertions(+), 28 deletions(-) diff --git a/src/crypto.h b/src/crypto.h index 45c876f6..98e6d3a2 100644 --- a/src/crypto.h +++ b/src/crypto.h @@ -34,10 +34,25 @@ namespace crypto { using pkey_ctx_t = util::safe_ptr; using bignum_t = util::safe_ptr; + /** + * @brief The permissions of a client. + */ + enum class PERM: uint32_t { + list = 1 << 0, // Allow list apps + view = 1 << 3, // Allow view streams + input = 1 << 7, // Allow any input: kbd/mouse, controller + server_cmd = 1 << 10, // Allow execute server cmd + launch = 1 << 15, // Allow launch apps + _default = view | list, // Default permissions for new clients + _all = launch | server_cmd | input | view | list, // All current permissions + _no = 0, // No permissions are granted + }; + struct named_cert_t { std::string name; std::string uuid; std::string cert; + PERM perm; }; using p_named_cert_t = std::shared_ptr; diff --git a/src/nvhttp.cpp b/src/nvhttp.cpp index 7981d8d9..c9d369c0 100644 --- a/src/nvhttp.cpp +++ b/src/nvhttp.cpp @@ -47,6 +47,7 @@ namespace nvhttp { namespace pt = boost::property_tree; using p_named_cert_t = crypto::p_named_cert_t; + using PERM = crypto::PERM; struct client_t { std::vector named_devices; @@ -242,6 +243,7 @@ namespace nvhttp { named_cert_node.put("name"s, final_name); named_cert_node.put("cert"s, named_cert_p->cert); named_cert_node.put("uuid"s, named_cert_p->uuid); + named_cert_node.put("perm"s, (uint32_t)named_cert_p->perm); named_cert_nodes.push_back(std::make_pair(""s, named_cert_node)); } } @@ -299,6 +301,7 @@ namespace nvhttp { named_cert_p->name = ""s; named_cert_p->cert = el.get_value(); named_cert_p->uuid = uuid_util::uuid_t::generate().string(); + named_cert_p->perm = PERM::_all; client.named_devices.emplace_back(named_cert_p); } } @@ -308,9 +311,10 @@ namespace nvhttp { if (root.count("named_devices")) { for (auto &[_, el] : root.get_child("named_devices")) { auto named_cert_p = std::make_shared(); - named_cert_p->name = el.get_child("name").get_value(); - named_cert_p->cert = el.get_child("cert").get_value(); - named_cert_p->uuid = el.get_child("uuid").get_value(); + named_cert_p->name = el.get("name"); + named_cert_p->cert = el.get("cert"); + named_cert_p->uuid = el.get("uuid"); + named_cert_p->perm = (PERM)el.get("perm", (uint32_t)PERM::_all); client.named_devices.emplace_back(named_cert_p); } } @@ -507,6 +511,12 @@ namespace nvhttp { } named_cert_p->cert = std::move(client.cert); named_cert_p->uuid = uuid_util::uuid_t::generate().string(); + // If the device is the first one paired with the server, assign full permission. + if (client_root.named_devices.empty()) { + named_cert_p->perm = PERM::_all; + } else { + named_cert_p->perm = PERM::_default; + } auto it = map_id_sess.find(client.uniqueID); map_id_sess.erase(it); @@ -765,8 +775,6 @@ namespace nvhttp { int pair_status = 0; if constexpr (std::is_same_v) { - BOOST_LOG(info) << "Device " << get_verified_cert(request)->name << " getting server info!!!"; - auto args = request->parse_query_string(); auto clientID = args.find("uniqueid"s); @@ -789,25 +797,36 @@ namespace nvhttp { tree.put("root.ExternalPort", net::map_port(PORT_HTTP)); tree.put("root.MaxLumaPixelsHEVC", video::active_hevc_mode > 1 ? "1869449984" : "0"); + crypto::named_cert_t* named_cert_p; + // 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) { tree.put("root.mac", platf::get_mac_address(net::addr_to_normalized_string(local_endpoint.address()))); - pt::ptree& root_node = tree.get_child("root"); + named_cert_p = get_verified_cert(request); + if (named_cert_p->perm >= PERM::server_cmd) { + pt::ptree& root_node = tree.get_child("root"); - if (config::sunshine.server_cmds.size() > 0) { - // Broadcast server_cmds - for (const auto& cmd : config::sunshine.server_cmds) { - pt::ptree cmd_node; - cmd_node.put_value(cmd.cmd_name); - root_node.push_back(std::make_pair("ServerCommand", cmd_node)); + if (config::sunshine.server_cmds.size() > 0) { + // Broadcast server_cmds + for (const auto& cmd : config::sunshine.server_cmds) { + pt::ptree cmd_node; + cmd_node.put_value(cmd.cmd_name); + root_node.push_back(std::make_pair("ServerCommand", cmd_node)); + } } + } else { + BOOST_LOG(debug) << "Permission Get ServerCommand denied for [" << named_cert_p->name << "] (" << (uint32_t)named_cert_p->perm << ")"; } #ifdef _WIN32 tree.put("root.VirtualDisplayCapable", true); - tree.put("root.VirtualDisplayDriverReady", proc::vDisplayDriverStatus == VDISPLAY::DRIVER_STATUS::OK); + if (named_cert_p->perm < PERM::list) { + tree.put("root.VirtualDisplayDriverReady", true); + } else { + tree.put("root.VirtualDisplayDriverReady", proc::vDisplayDriverStatus == VDISPLAY::DRIVER_STATUS::OK); + } #endif } else { @@ -860,10 +879,16 @@ namespace nvhttp { } 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"); + + if constexpr (std::is_same_v) { + auto current_appid = proc::proc.running(); + tree.put("root.currentgame", current_appid); + tree.put("root.state", current_appid > 0 ? "SUNSHINE_SERVER_BUSY" : "SUNSHINE_SERVER_FREE"); + } else { + tree.put("root.currentgame", 0); + tree.put("root.state", "SUNSHINE_SERVER_FREE"); + } std::ostringstream data; @@ -880,6 +905,7 @@ namespace nvhttp { pt::ptree named_cert_node; named_cert_node.put("name"s, named_cert_p->name); named_cert_node.put("uuid"s, named_cert_p->uuid); + named_cert_node.put("perm", (uint32_t)named_cert_p->perm); named_cert_nodes.push_back(std::make_pair(""s, named_cert_node)); } @@ -904,23 +930,37 @@ namespace nvhttp { apps.put(".status_code", 200); - for (auto &proc : proc::proc.get_apps()) { + auto named_cert_p = get_verified_cert(request); + if (named_cert_p->perm < PERM::list) { + BOOST_LOG(debug) << "Permission ListApp denied for [" << named_cert_p->name << "] (" << (uint32_t)named_cert_p->perm << ")"; + 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); + app.put("IsHdrSupported"s, 0); + app.put("AppTitle"s, "Permission Denied"); + app.put("ID", "PERMISSION_DENIED"); apps.push_back(std::make_pair("App", std::move(app))); + + return; + } else { + 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(request); - auto named_cert_p = get_verified_cert(request); - pt::ptree tree; auto g = util::fail_guard([&]() { std::ostringstream data; @@ -930,6 +970,17 @@ namespace nvhttp { response->close_connection_after_response = true; }); + auto named_cert_p = get_verified_cert(request); + if (named_cert_p->perm < PERM::launch) { + BOOST_LOG(debug) << "Permission LaunchApp denied for [" << named_cert_p->name << "] (" << (uint32_t)named_cert_p->perm << ")"; + + tree.put("root.resume", 0); + tree.put("root..status_code", 503); + tree.put("root..status_message", "Permission denied"); + + return; + } + if (rtsp_stream::session_count() == config::stream.channels) { tree.put("root.resume", 0); tree.put("root..status_code", 503); @@ -1018,8 +1069,6 @@ namespace nvhttp { resume(bool &host_audio, resp_https_t response, req_https_t request) { print_req(request); - auto named_cert_p = get_verified_cert(request); - pt::ptree tree; auto g = util::fail_guard([&]() { std::ostringstream data; @@ -1029,6 +1078,17 @@ namespace nvhttp { response->close_connection_after_response = true; }); + auto named_cert_p = get_verified_cert(request); + if (named_cert_p->perm < PERM::view) { + BOOST_LOG(debug) << "Permission ViewApp denied for [" << named_cert_p->name << "] (" << (uint32_t)named_cert_p->perm << ")"; + + tree.put("root.resume", 0); + tree.put("root..status_code", 503); + tree.put("root..status_message", "Permission denied"); + + return; + } + // 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) { @@ -1115,6 +1175,17 @@ namespace nvhttp { response->close_connection_after_response = true; }); + auto named_cert_p = get_verified_cert(request); + if (named_cert_p->perm < PERM::launch) { + BOOST_LOG(debug) << "Permission CancelApp denied for [" << named_cert_p->name << "] (" << (uint32_t)named_cert_p->perm << ")"; + + tree.put("root.resume", 0); + tree.put("root..status_code", 503); + tree.put("root..status_message", "Permission denied"); + + return; + } + // 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) { @@ -1137,9 +1208,27 @@ namespace nvhttp { appasset(resp_https_t response, req_https_t request) { print_req(request); + auto fg = util::fail_guard([&]() { + response->write(SimpleWeb::StatusCode::server_error_internal_server_error); + response->close_connection_after_response = true; + }); + + auto named_cert_p = get_verified_cert(request); + + if (named_cert_p->perm < PERM::list) { + BOOST_LOG(debug) << "Permission Get AppAsset denied for [" << named_cert_p->name << "] (" << (uint32_t)named_cert_p->perm << ")"; + + fg.disable(); + response->write(SimpleWeb::StatusCode::client_error_unauthorized); + response->close_connection_after_response = true; + return; + } + auto args = request->parse_query_string(); auto app_image = proc::proc.get_app_image(util::from_view(get_arg(args, "appid"))); + fg.disable(); + std::ifstream in(app_image, std::ios::binary); SimpleWeb::CaseInsensitiveMultimap headers; headers.emplace("Content-Type", "image/png"); @@ -1180,16 +1269,21 @@ namespace nvhttp { } bool verified = false; + p_named_cert_t named_cert_p; 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); + if (verified) { + BOOST_LOG(debug) << subject_name << " -- "sv << "verified, device name: "sv << named_cert_p->name; + } else { + BOOST_LOG(debug) << subject_name << " -- "sv << "denied"sv; + } + }); - p_named_cert_t named_cert_p; auto err_str = cert_chain.verify(x509.get(), named_cert_p); if (err_str) { BOOST_LOG(warning) << "SSL Verification error :: "sv << err_str; @@ -1199,8 +1293,6 @@ namespace nvhttp { verified = true; req->userp = named_cert_p; - BOOST_LOG(info) << "Device " << named_cert_p->name << " verified!"; - return true; }; From 2e7bde8958e715aea181f80f38c2f22a9940eb3b Mon Sep 17 00:00:00 2001 From: Yukino Song Date: Sun, 15 Sep 2024 22:40:38 +0800 Subject: [PATCH 8/9] Implement permission for stream --- src/crypto.h | 44 +++++++++++++++++++++++++++++++------- src/input.cpp | 56 ++++++++++++++++++++++++++++++++++++++++++++++++- src/input.h | 3 ++- src/nvhttp.cpp | 57 +++++++++++++++++++++++++------------------------- src/rtsp.h | 4 +++- src/stream.cpp | 27 ++++++++++++++++++++++-- 6 files changed, 150 insertions(+), 41 deletions(-) diff --git a/src/crypto.h b/src/crypto.h index 98e6d3a2..fe642bd5 100644 --- a/src/crypto.h +++ b/src/crypto.h @@ -38,16 +38,44 @@ namespace crypto { * @brief The permissions of a client. */ enum class PERM: uint32_t { - list = 1 << 0, // Allow list apps - view = 1 << 3, // Allow view streams - input = 1 << 7, // Allow any input: kbd/mouse, controller - server_cmd = 1 << 10, // Allow execute server cmd - launch = 1 << 15, // Allow launch apps - _default = view | list, // Default permissions for new clients - _all = launch | server_cmd | input | view | list, // All current permissions - _no = 0, // No permissions are granted + _reserved = 1, + + _input = _reserved << 8, // Input permission group + input_controller = _input << 0, // Allow controller input + input_touch = _input << 1, // Allow touch input + input_pen = _input << 2, // Allow pen input + input_kbdm = _input << 3, // Allow kbd/mouse input + _all_inputs = input_controller | input_touch | input_pen | input_kbdm, + + _operation = _input << 8, // Operation permission group + clipboard_set = _operation << 0, // Allow set clipboard from client + clipboard_read = _operation << 1, // Allow read clipboard from host + file_upload = _operation << 2, // Allow upload files to host + file_dwnload = _operation << 3, // Allow download files from host + server_cmd = _operation << 4, // Allow execute server cmd + _all_opeiations = clipboard_set | clipboard_read | file_upload | file_dwnload | server_cmd, + + _action = _operation << 8, // Action permission group + list = _action << 0, // Allow list apps + view = _action << 1, // Allow view streams + launch = _action << 2, // Allow launch apps + _all_actions = list | view | launch, + + _default = view | list, // Default permissions for new clients + _no = 0, // No permissions are granted + _all = _all_inputs | _all_opeiations | _all_actions, // All current permissions }; + inline constexpr PERM + operator&(PERM x, PERM y) { + return static_cast(static_cast(x) & static_cast(y)); + } + + inline constexpr bool + operator!(PERM p) { + return static_cast(p) == 0; + } + struct named_cert_t { std::string name; std::string uuid; diff --git a/src/input.cpp b/src/input.cpp index bf275467..aafad577 100644 --- a/src/input.cpp +++ b/src/input.cpp @@ -1628,7 +1628,61 @@ namespace input { * @param input_data The input message. */ void - passthrough(std::shared_ptr &input, std::vector &&input_data) { + passthrough(std::shared_ptr &input, std::vector &&input_data, const crypto::PERM& permission) { + // No input permissions at all + if (!(permission & crypto::PERM::_all_inputs)) { + return; + } + + // Have some input permission + // Otherwise have all input permission + if ((permission & crypto::PERM::_all_inputs) != crypto::PERM::_all_inputs) { + PNV_INPUT_HEADER payload = (PNV_INPUT_HEADER)input_data.data(); + + // Check permission + switch (util::endian::little(payload->magic)) { + case MULTI_CONTROLLER_MAGIC_GEN5: + case SS_CONTROLLER_ARRIVAL_MAGIC: + case SS_CONTROLLER_TOUCH_MAGIC: + case SS_CONTROLLER_MOTION_MAGIC: + case SS_CONTROLLER_BATTERY_MAGIC: + if (!(permission & crypto::PERM::input_controller)) { + return; + } else { + break; + } + case MOUSE_MOVE_REL_MAGIC_GEN5: + case MOUSE_MOVE_ABS_MAGIC: + case MOUSE_BUTTON_DOWN_EVENT_MAGIC_GEN5: + case MOUSE_BUTTON_UP_EVENT_MAGIC_GEN5: + case SCROLL_MAGIC_GEN5: + case SS_HSCROLL_MAGIC: + case KEY_DOWN_EVENT_MAGIC: + case KEY_UP_EVENT_MAGIC: + case UTF8_TEXT_EVENT_MAGIC: + if (!(permission & crypto::PERM::input_kbdm)) { + return; + } else { + break; + } + case SS_TOUCH_MAGIC: + if (!(permission & crypto::PERM::input_touch)) { + return; + } else { + break; + } + case SS_PEN_MAGIC: + if (!(permission & crypto::PERM::input_pen)) { + return; + } else { + break; + } + default: + // Unknown input event + return; + } + } + { std::lock_guard lg(input->input_queue_lock); input->input_queue.push_back(std::move(input_data)); diff --git a/src/input.h b/src/input.h index a20c9c30..c1225482 100644 --- a/src/input.h +++ b/src/input.h @@ -8,6 +8,7 @@ #include "platform/common.h" #include "thread_safe.h" +#include "crypto.h" namespace input { struct input_t; @@ -17,7 +18,7 @@ namespace input { void reset(std::shared_ptr &input); void - passthrough(std::shared_ptr &input, std::vector &&input_data); + passthrough(std::shared_ptr &input, std::vector &&input_data, const crypto::PERM& permission); [[nodiscard]] std::unique_ptr init(); diff --git a/src/nvhttp.cpp b/src/nvhttp.cpp index c9d369c0..4a9e2baf 100644 --- a/src/nvhttp.cpp +++ b/src/nvhttp.cpp @@ -314,7 +314,7 @@ namespace nvhttp { named_cert_p->name = el.get("name"); named_cert_p->cert = el.get("cert"); named_cert_p->uuid = el.get("uuid"); - named_cert_p->perm = (PERM)el.get("perm", (uint32_t)PERM::_all); + named_cert_p->perm = (PERM)el.get("perm", (uint32_t)PERM::_all) & PERM::_all; client.named_devices.emplace_back(named_cert_p); } } @@ -340,7 +340,7 @@ namespace nvhttp { } std::shared_ptr - make_launch_session(bool host_audio, const args_t &args, const std::string& device_name, const std::string& uuid) { + make_launch_session(bool host_audio, const args_t &args, const crypto::named_cert_t* named_cert_p) { auto launch_session = std::make_shared(); launch_session->id = ++session_id_counter; @@ -360,8 +360,9 @@ namespace nvhttp { x++; } - launch_session->device_name = device_name.empty() ? "ApolloDisplay"s : device_name; - launch_session->unique_id = uuid; + launch_session->device_name = named_cert_p->name.empty() ? "ApolloDisplay"s : named_cert_p->name; + launch_session->unique_id = named_cert_p->uuid; + launch_session->perm = named_cert_p->perm; 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")); @@ -797,15 +798,13 @@ namespace nvhttp { tree.put("root.ExternalPort", net::map_port(PORT_HTTP)); tree.put("root.MaxLumaPixelsHEVC", video::active_hevc_mode > 1 ? "1869449984" : "0"); - crypto::named_cert_t* named_cert_p; - // 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) { tree.put("root.mac", platf::get_mac_address(net::addr_to_normalized_string(local_endpoint.address()))); - named_cert_p = get_verified_cert(request); - if (named_cert_p->perm >= PERM::server_cmd) { + auto named_cert_p = get_verified_cert(request); + if (!!(named_cert_p->perm & PERM::server_cmd)) { pt::ptree& root_node = tree.get_child("root"); if (config::sunshine.server_cmds.size() > 0) { @@ -820,12 +819,14 @@ namespace nvhttp { BOOST_LOG(debug) << "Permission Get ServerCommand denied for [" << named_cert_p->name << "] (" << (uint32_t)named_cert_p->perm << ")"; } + tree.put("root.Permission", std::to_string((uint32_t)named_cert_p->perm)); + #ifdef _WIN32 tree.put("root.VirtualDisplayCapable", true); - if (named_cert_p->perm < PERM::list) { - tree.put("root.VirtualDisplayDriverReady", true); - } else { + if (!!(named_cert_p->perm & PERM::list)) { tree.put("root.VirtualDisplayDriverReady", proc::vDisplayDriverStatus == VDISPLAY::DRIVER_STATUS::OK); + } else { + tree.put("root.VirtualDisplayDriverReady", true); } #endif } @@ -931,7 +932,17 @@ namespace nvhttp { apps.put(".status_code", 200); auto named_cert_p = get_verified_cert(request); - if (named_cert_p->perm < PERM::list) { + if (!!(named_cert_p->perm & PERM::list)) { + 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))); + } + } else { BOOST_LOG(debug) << "Permission ListApp denied for [" << named_cert_p->name << "] (" << (uint32_t)named_cert_p->perm << ")"; pt::ptree app; @@ -943,16 +954,6 @@ namespace nvhttp { apps.push_back(std::make_pair("App", std::move(app))); return; - } else { - 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))); - } } } @@ -971,7 +972,7 @@ namespace nvhttp { }); auto named_cert_p = get_verified_cert(request); - if (named_cert_p->perm < PERM::launch) { + if (!(named_cert_p->perm & PERM::launch)) { BOOST_LOG(debug) << "Permission LaunchApp denied for [" << named_cert_p->name << "] (" << (uint32_t)named_cert_p->perm << ")"; tree.put("root.resume", 0); @@ -1012,7 +1013,7 @@ namespace nvhttp { } host_audio = util::from_view(get_arg(args, "localAudioPlayMode")); - auto launch_session = make_launch_session(host_audio, args, named_cert_p->name, named_cert_p->uuid); + auto launch_session = make_launch_session(host_audio, args, named_cert_p); auto encryption_mode = net::encryption_mode_for_address(request->remote_endpoint().address()); if (!launch_session->rtsp_cipher && encryption_mode == config::ENCRYPTION_MODE_MANDATORY) { @@ -1079,7 +1080,7 @@ namespace nvhttp { }); auto named_cert_p = get_verified_cert(request); - if (named_cert_p->perm < PERM::view) { + if (!(named_cert_p->perm & PERM::view)) { BOOST_LOG(debug) << "Permission ViewApp denied for [" << named_cert_p->name << "] (" << (uint32_t)named_cert_p->perm << ")"; tree.put("root.resume", 0); @@ -1140,7 +1141,7 @@ namespace nvhttp { } } - auto launch_session = make_launch_session(host_audio, args, named_cert_p->name, named_cert_p->uuid); + auto launch_session = make_launch_session(host_audio, args, named_cert_p); auto encryption_mode = net::encryption_mode_for_address(request->remote_endpoint().address()); if (!launch_session->rtsp_cipher && encryption_mode == config::ENCRYPTION_MODE_MANDATORY) { @@ -1176,7 +1177,7 @@ namespace nvhttp { }); auto named_cert_p = get_verified_cert(request); - if (named_cert_p->perm < PERM::launch) { + if (!(named_cert_p->perm & PERM::launch)) { BOOST_LOG(debug) << "Permission CancelApp denied for [" << named_cert_p->name << "] (" << (uint32_t)named_cert_p->perm << ")"; tree.put("root.resume", 0); @@ -1215,7 +1216,7 @@ namespace nvhttp { auto named_cert_p = get_verified_cert(request); - if (named_cert_p->perm < PERM::list) { + if (!(named_cert_p->perm & PERM::list)) { BOOST_LOG(debug) << "Permission Get AppAsset denied for [" << named_cert_p->name << "] (" << (uint32_t)named_cert_p->perm << ")"; fg.disable(); diff --git a/src/rtsp.h b/src/rtsp.h index 928ee67c..598d842b 100644 --- a/src/rtsp.h +++ b/src/rtsp.h @@ -21,9 +21,11 @@ namespace rtsp_stream { std::string av_ping_payload; uint32_t control_connect_data; - bool host_audio; std::string device_name; std::string unique_id; + crypto::PERM perm; + + bool host_audio; int width; int height; int fps; diff --git a/src/stream.cpp b/src/stream.cpp index 47925653..7519e754 100644 --- a/src/stream.cpp +++ b/src/stream.cpp @@ -20,6 +20,7 @@ extern "C" { } #include "config.h" +#include "crypto.h" #include "globals.h" #include "input.h" #include "logging.h" @@ -403,6 +404,9 @@ namespace stream { } control; std::uint32_t launch_session_id; + std::string device_name; + std::string device_uuid; + crypto::PERM permission; safe::mail_raw_t::event_t shutdown_event; safe::signal_t controlEnd; @@ -992,11 +996,17 @@ namespace stream { std::copy(payload.end() - 16, payload.end(), std::begin(iv)); } - input::passthrough(session->input, std::move(plaintext)); + input::passthrough(session->input, std::move(plaintext), session->permission); }); server->map(packetTypes[IDX_EXEC_SERVER_CMD], [server](session_t *session, const std::string_view &payload) { BOOST_LOG(debug) << "type [IDX_EXEC_SERVER_CMD]"sv; + + if (!(session->permission & crypto::PERM::server_cmd)) { + BOOST_LOG(debug) << "Permission Exec Server Cmd deined for [" << session->device_name << "]"; + return; + } + uint8_t cmdIndex = *(uint8_t*)payload.data(); if (cmdIndex < config::sunshine.server_cmds.size()) { @@ -1024,10 +1034,20 @@ namespace stream { server->map(packetTypes[IDX_SET_CLIPBOARD], [server](session_t *session, const std::string_view &payload) { BOOST_LOG(info) << "type [IDX_SET_CLIPBOARD]: "sv << payload << " size: " << payload.size(); + + if (!(session->permission & crypto::PERM::clipboard_set)) { + BOOST_LOG(debug) << "Permission Clipboard Set deined for [" << session->device_name << "]"; + return; + } }); server->map(packetTypes[IDX_FILE_TRANSFER_NONCE_REQUEST], [server](session_t *session, const std::string_view &payload) { BOOST_LOG(info) << "type [IDX_FILE_TRANSFER_NONCE_REQUEST]: "sv << payload << " size: " << payload.size(); + + if (!(session->permission & crypto::PERM::file_upload)) { + BOOST_LOG(debug) << "Permission File Upload deined for [" << session->device_name << "]"; + return; + } }); server->map(packetTypes[IDX_ENCRYPTED], [server](session_t *session, const std::string_view &payload) { @@ -1091,7 +1111,7 @@ namespace stream { // IDX_INPUT_DATA callback will attempt to decrypt unencrypted data, therefore we need pass it directly if (type == packetTypes[IDX_INPUT_DATA]) { plaintext.erase(std::begin(plaintext), std::begin(plaintext) + 4); - input::passthrough(session->input, std::move(plaintext)); + input::passthrough(session->input, std::move(plaintext), session->permission); } else { server->call(type, session, next_payload, true); @@ -2051,6 +2071,9 @@ namespace stream { session->shutdown_event = mail->event(mail::shutdown); session->launch_session_id = launch_session.id; + session->device_name = launch_session.device_name; + session->device_uuid = launch_session.unique_id; + session->permission = launch_session.perm; session->config = config; From db5790b3747a2fcf5a282d1475b8ca18c889b7d3 Mon Sep 17 00:00:00 2001 From: Yukino Song Date: Mon, 16 Sep 2024 23:10:19 +0800 Subject: [PATCH 9/9] Implement permission manage web UI --- src/confighttp.cpp | 63 ++++ src/crypto.h | 1 + src/nvhttp.cpp | 104 +++++- src/nvhttp.h | 45 +++ src/rtsp.cpp | 35 ++ src/rtsp.h | 12 + src/stream.cpp | 68 ++++ src/stream.h | 10 + src/system_tray.cpp | 44 ++- src/system_tray.h | 5 +- src_assets/common/assets/web/pin.html | 303 +++++++++++++++++- .../assets/web/public/assets/locale/en.json | 34 +- .../assets/web/public/assets/locale/zh.json | 34 +- .../common/assets/web/troubleshooting.html | 81 +---- 14 files changed, 714 insertions(+), 125 deletions(-) diff --git a/src/confighttp.cpp b/src/confighttp.cpp index 29474148..9c87307a 100644 --- a/src/confighttp.cpp +++ b/src/confighttp.cpp @@ -866,6 +866,38 @@ namespace confighttp { outputTree.put("status", true); } + void + updateClient(resp_https_t response, req_https_t request) { + if (!authenticate(response, request)) return; + + print_req(request); + + std::stringstream ss; + ss << request->content.rdbuf(); + + pt::ptree inputTree, outputTree; + + auto g = util::fail_guard([&]() { + std::ostringstream data; + pt::write_json(data, outputTree); + response->write(data.str()); + }); + + try { + pt::read_json(ss, inputTree); + std::string uuid = inputTree.get("uuid"); + std::string name = inputTree.get("name"); + auto perm = (crypto::PERM)inputTree.get("perm") & crypto::PERM::_all; + outputTree.put("status", nvhttp::update_device_info(uuid, name, perm)); + } + catch (std::exception &e) { + BOOST_LOG(warning) << "Update Client: "sv << e.what(); + outputTree.put("status", false); + outputTree.put("error", e.what()); + return; + } + } + void unpair(resp_https_t response, req_https_t request) { if (!authenticate(response, request)) return; @@ -897,6 +929,35 @@ namespace confighttp { } } + void + disconnect(resp_https_t response, req_https_t request) { + if (!authenticate(response, request)) return; + + print_req(request); + + std::stringstream ss; + ss << request->content.rdbuf(); + + pt::ptree inputTree, outputTree; + + auto g = util::fail_guard([&]() { + std::ostringstream data; + pt::write_json(data, outputTree); + response->write(data.str()); + }); + + try { + pt::read_json(ss, inputTree); + std::string uuid = inputTree.get("uuid"); + outputTree.put("status", nvhttp::find_and_stop_session(uuid, true)); + } + catch (std::exception &e) { + BOOST_LOG(warning) << "Disconnect: "sv << e.what(); + outputTree.put("status", false); + outputTree.put("error", e.what()); + } + } + void listClients(resp_https_t response, req_https_t request) { if (!authenticate(response, request)) return; @@ -969,7 +1030,9 @@ namespace confighttp { server.resource["^/api/apps/([0-9]+)$"]["DELETE"] = deleteApp; server.resource["^/api/clients/unpair-all$"]["POST"] = unpairAll; server.resource["^/api/clients/list$"]["GET"] = listClients; + server.resource["^/api/clients/update$"]["POST"] = updateClient; server.resource["^/api/clients/unpair$"]["POST"] = unpair; + server.resource["^/api/clients/disconnect$"]["POST"] = disconnect; server.resource["^/api/apps/close$"]["POST"] = closeApp; server.resource["^/api/covers/upload$"]["POST"] = uploadCover; server.resource["^/images/apollo.ico$"]["GET"] = getFaviconImage; diff --git a/src/crypto.h b/src/crypto.h index fe642bd5..8bc8f6a0 100644 --- a/src/crypto.h +++ b/src/crypto.h @@ -59,6 +59,7 @@ namespace crypto { list = _action << 0, // Allow list apps view = _action << 1, // Allow view streams launch = _action << 2, // Allow launch apps + _allow_view = view | launch, // If no view permission is granted, disconnect the device upon permission update _all_actions = list | view | launch, _default = view | list, // Default permissions for new clients diff --git a/src/nvhttp.cpp b/src/nvhttp.cpp index 4a9e2baf..2cb194a3 100644 --- a/src/nvhttp.cpp +++ b/src/nvhttp.cpp @@ -31,6 +31,7 @@ #include "platform/common.h" #include "process.h" #include "rtsp.h" +#include "stream.h" #include "system_tray.h" #include "utility.h" #include "uuid.h" @@ -331,7 +332,12 @@ namespace nvhttp { void add_authorized_client(const p_named_cert_t& named_cert_p) { client_t &client = client_root; - client.named_devices.emplace_back(named_cert_p); + client.named_devices.push_back(named_cert_p); + + +#if defined SUNSHINE_TRAY && SUNSHINE_TRAY >= 1 + system_tray::update_tray_paired(named_cert_p->name); +#endif if (!config::sunshine.flags[config::flag::FRESH_STATE]) { save_state(); @@ -657,10 +663,6 @@ namespace nvhttp { if (hash.to_string_view() == it->second) { -#if defined SUNSHINE_TRAY && SUNSHINE_TRAY >= 1 - system_tray::update_tray_otp_pair(ptr->second.client.name); -#endif - if (!otp_device_name.empty()) { ptr->second.client.name = std::move(otp_device_name); } @@ -823,7 +825,7 @@ namespace nvhttp { #ifdef _WIN32 tree.put("root.VirtualDisplayCapable", true); - if (!!(named_cert_p->perm & PERM::list)) { + if (!!(named_cert_p->perm & PERM::_all_actions)) { tree.put("root.VirtualDisplayDriverReady", proc::vDisplayDriverStatus == VDISPLAY::DRIVER_STATUS::OK); } else { tree.put("root.VirtualDisplayDriverReady", true); @@ -902,11 +904,29 @@ namespace nvhttp { get_all_clients() { pt::ptree named_cert_nodes; client_t &client = client_root; + + std::list connected_uuids = rtsp_stream::get_all_session_uuids(); + for (auto &named_cert_p : client.named_devices) { pt::ptree named_cert_node; named_cert_node.put("name"s, named_cert_p->name); named_cert_node.put("uuid"s, named_cert_p->uuid); named_cert_node.put("perm", (uint32_t)named_cert_p->perm); + + if (connected_uuids.empty()) { + named_cert_node.put("connected"s, false); + } else { + bool connected = false; + for (auto it = connected_uuids.begin(); it != connected_uuids.end(); ++it) { + if (*it == named_cert_p->uuid) { + connected = true; + connected_uuids.erase(it); + break; + } + } + named_cert_node.put("connected"s, connected); + } + named_cert_nodes.push_back(std::make_pair(""s, named_cert_node)); } @@ -932,7 +952,7 @@ namespace nvhttp { apps.put(".status_code", 200); auto named_cert_p = get_verified_cert(request); - if (!!(named_cert_p->perm & PERM::list)) { + if (!!(named_cert_p->perm & PERM::_all_actions)) { for (auto &proc : proc::proc.get_apps()) { pt::ptree app; @@ -949,7 +969,7 @@ namespace nvhttp { app.put("IsHdrSupported"s, 0); app.put("AppTitle"s, "Permission Denied"); - app.put("ID", "PERMISSION_DENIED"); + app.put("ID", "1145141919810"); apps.push_back(std::make_pair("App", std::move(app))); @@ -1080,7 +1100,7 @@ namespace nvhttp { }); auto named_cert_p = get_verified_cert(request); - if (!(named_cert_p->perm & PERM::view)) { + if (!(named_cert_p->perm & PERM::_allow_view)) { BOOST_LOG(debug) << "Permission ViewApp denied for [" << named_cert_p->name << "] (" << (uint32_t)named_cert_p->perm << ")"; tree.put("root.resume", 0); @@ -1161,6 +1181,10 @@ namespace nvhttp { tree.put("root.resume", 1); rtsp_stream::launch_session_raise(launch_session); + +#if defined SUNSHINE_TRAY && SUNSHINE_TRAY >= 1 + system_tray::update_tray_client_connected(named_cert_p->name); +#endif } void @@ -1216,7 +1240,7 @@ namespace nvhttp { auto named_cert_p = get_verified_cert(request); - if (!(named_cert_p->perm & PERM::list)) { + if (!(named_cert_p->perm & PERM::_all_actions)) { BOOST_LOG(debug) << "Permission Get AppAsset denied for [" << named_cert_p->name << "] (" << (uint32_t)named_cert_p->perm << ")"; fg.disable(); @@ -1383,6 +1407,54 @@ namespace nvhttp { load_state(); } + void stop_session(stream::session_t& session, bool graceful) { + if (graceful) { + stream::session::graceful_stop(session); + } else { + stream::session::stop(session); + } + } + + bool find_and_stop_session(const std::string& uuid, bool graceful) { + auto session = rtsp_stream::find_session(uuid); + if (session) { + stop_session(*session, graceful); + return true; + } + return false; + } + + void update_session_info(stream::session_t& session, const std::string& name, const crypto::PERM newPerm) { + stream::session::update_device_info(session, name, newPerm); + } + + bool find_and_udpate_session_info(const std::string& uuid, const std::string& name, const crypto::PERM newPerm) { + auto session = rtsp_stream::find_session(uuid); + if (session) { + update_session_info(*session, name, newPerm); + return true; + } + return false; + } + + bool update_device_info(const std::string& uuid, const std::string& name, const crypto::PERM newPerm) { + find_and_udpate_session_info(uuid, name, newPerm); + + client_t &client = client_root; + auto it = client.named_devices.begin(); + for (; it != client.named_devices.end(); ++it) { + auto named_cert_p = *it; + if (named_cert_p->uuid == uuid) { + named_cert_p->name = name; + named_cert_p->perm = newPerm; + save_state(); + return true; + } + } + + return false; + } + int unpair_client(std::string uuid) { int removed = 0; @@ -1399,6 +1471,18 @@ namespace nvhttp { save_state(); load_state(); + + if (removed) { + auto session = rtsp_stream::find_session(uuid); + if (session) { + stop_session(*session, true); + } + + if (client.named_devices.empty()) { + proc::proc.terminate(); + } + } + return removed; } } // namespace nvhttp diff --git a/src/nvhttp.h b/src/nvhttp.h index 757957ff..10391b46 100644 --- a/src/nvhttp.h +++ b/src/nvhttp.h @@ -13,6 +13,8 @@ #include // local includes +#include "crypto.h" +#include "rtsp.h" #include "thread_safe.h" using namespace std::chrono_literals; @@ -96,4 +98,47 @@ namespace nvhttp { */ void 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] newPerm New permission + */ + bool update_device_info(const std::string& uuid, const std::string& name, const crypto::PERM newPerm); } // namespace nvhttp diff --git a/src/rtsp.cpp b/src/rtsp.cpp index 3f146937..c6e1ccfd 100644 --- a/src/rtsp.cpp +++ b/src/rtsp.cpp @@ -618,6 +618,31 @@ namespace rtsp_stream { return nullptr; } + std::shared_ptr + find_session(const std::string& uuid) { + auto lg = _session_slots.lock(); + + for (auto &slot : *_session_slots) { + if (slot && stream::session::uuid_match(*slot, uuid)) { + return slot; + } + } + + return nullptr; + } + + std::list + get_all_session_uuids() { + std::list uuids; + auto lg = _session_slots.lock(); + for (auto &slot : *_session_slots) { + if (slot) { + uuids.push_back(stream::session::uuid(*slot)); + } + } + return uuids; + } + private: std::unordered_map _map_cmd_cb; @@ -652,6 +677,16 @@ namespace rtsp_stream { return server.session_count(); } + std::shared_ptr + find_session(const std::string& uuid) { + return server.find_session(uuid); + } + + std::list + get_all_session_uuids() { + return server.get_all_session_uuids(); + } + int send(tcp::socket &sock, const std::string_view &sv) { std::size_t bytes_send = 0; diff --git a/src/rtsp.h b/src/rtsp.h index 598d842b..0792756c 100644 --- a/src/rtsp.h +++ b/src/rtsp.h @@ -5,10 +5,16 @@ #pragma once #include +#include #include "crypto.h" #include "thread_safe.h" +// Resolve circular dependencies +namespace stream { + struct session_t; +} + namespace rtsp_stream { constexpr auto RTSP_SETUP_PORT = 21; @@ -60,6 +66,12 @@ namespace rtsp_stream { int session_count(); + std::shared_ptr + find_session(const std::string& uuid); + + std::list + get_all_session_uuids(); + void rtpThread(); diff --git a/src/stream.cpp b/src/stream.cpp index 7519e754..4507cdfd 100644 --- a/src/stream.cpp +++ b/src/stream.cpp @@ -1969,6 +1969,40 @@ namespace stream { return session.state.load(std::memory_order_relaxed); } + inline bool + send(session_t& session, const std::string_view &payload) { + return session.broadcast_ref->control_server.send(payload, session.control.peer); + } + + std::string + uuid(const session_t& session) { + return session.device_uuid; + } + + bool + uuid_match(const session_t &session, const std::string& uuid) { + return session.device_uuid == uuid; + } + + bool + update_device_info(session_t& session, const std::string& name, const crypto::PERM& newPerm) { + session.permission = newPerm; + if (!(newPerm & crypto::PERM::_allow_view)) { + BOOST_LOG(debug) << "Session: View permission revoked for [" << session.device_name << "], disconnecting..."; + graceful_stop(session); + return true; + } + + BOOST_LOG(debug) << "Session: Permission updated for [" << session.device_name << "]"; + + if (session.device_name != name) { + BOOST_LOG(debug) << "Session: Device name changed from [" << session.device_name << "] to [" << name << "]"; + session.device_name = name; + } + + return false; + } + void stop(session_t &session) { while_starting_do_nothing(session.state); @@ -1981,6 +2015,40 @@ namespace stream { session.shutdown_event->raise(true); } + void + graceful_stop(session_t& session) { + while_starting_do_nothing(session.state); + auto expected = state_e::RUNNING; + auto already_stopping = !session.state.compare_exchange_strong(expected, state_e::STOPPING); + if (already_stopping) { + return; + } + + // reason: graceful termination + std::uint32_t reason = 0x80030023; + + control_terminate_t plaintext; + plaintext.header.type = packetTypes[IDX_TERMINATION]; + plaintext.header.payloadLength = sizeof(plaintext.ec); + plaintext.ec = util::endian::big(reason); + + // We may not have gotten far enough to have an ENet connection yet + if (session.control.peer) { + std::array + encrypted_payload; + auto payload = stream::encode_control(&session, util::view(plaintext), encrypted_payload); + + if (send(session, payload)) { + TUPLE_2D(port, addr, platf::from_sockaddr_ex((sockaddr *) &session.control.peer->address.address)); + BOOST_LOG(warning) << "Couldn't send termination code to ["sv << addr << ':' << port << ']'; + } + } + + session.shutdown_event->raise(true); + session.controlEnd.raise(true); + } + void join(session_t &session) { // Current Nvidia drivers have a bug where NVENC can deadlock the encoder thread with hardware-accelerated diff --git a/src/stream.h b/src/stream.h index 95d1e2d4..ecc32554 100644 --- a/src/stream.h +++ b/src/stream.h @@ -43,13 +43,23 @@ namespace stream { std::shared_ptr alloc(config_t &config, rtsp_stream::launch_session_t &launch_session); + std::string + uuid(const session_t& session); + bool + uuid_match(const session_t& session, const std::string& uuid); + bool + update_device_info(session_t& session, const std::string& name, const crypto::PERM& newPerm); int start(session_t &session, const std::string &addr_string); void stop(session_t &session); void + graceful_stop(session_t& session); + void join(session_t &session); state_e state(session_t &session); + inline bool + send(session_t& session); } // namespace session } // namespace stream diff --git a/src/system_tray.cpp b/src/system_tray.cpp index d39ab1c0..e4af8780 100644 --- a/src/system_tray.cpp +++ b/src/system_tray.cpp @@ -268,8 +268,8 @@ namespace system_tray { snprintf(msg, std::size(msg), "Streaming started for %s", app_name.c_str()); snprintf(force_close_msg, std::size(force_close_msg), "Force close [%s]", app_name.c_str()); #ifdef _WIN32 - strcpy(msg, convertUtf8ToCurrentCodepage(msg).c_str()); - strcpy(force_close_msg, convertUtf8ToCurrentCodepage(force_close_msg).c_str()); + strncpy(msg, convertUtf8ToCurrentCodepage(msg).c_str(), std::size(msg) - 1); + strncpy(force_close_msg, convertUtf8ToCurrentCodepage(force_close_msg).c_str(), std::size(force_close_msg) - 1); #endif tray.notification_text = msg; tray.notification_icon = TRAY_ICON_PLAYING; @@ -293,7 +293,7 @@ namespace system_tray { char msg[256]; snprintf(msg, std::size(msg), "Streaming paused for %s", app_name.c_str()); #ifdef _WIN32 - strcpy(msg, convertUtf8ToCurrentCodepage(msg).c_str()); + strncpy(msg, convertUtf8ToCurrentCodepage(msg).c_str(), std::size(msg) - 1); #endif tray.icon = TRAY_ICON_PAUSING; tray.notification_title = "Stream Paused"; @@ -318,7 +318,7 @@ namespace system_tray { char msg[256]; snprintf(msg, std::size(msg), "Streaming stopped for %s", app_name.c_str()); #ifdef _WIN32 - strcpy(msg, convertUtf8ToCurrentCodepage(msg).c_str()); + strncpy(msg, convertUtf8ToCurrentCodepage(msg).c_str(), std::size(msg) - 1); #endif tray.icon = TRAY_ICON; tray.notification_icon = TRAY_ICON; @@ -344,7 +344,7 @@ namespace system_tray { char msg[256]; snprintf(msg, std::size(msg), "Application %s exited too fast with code %d. Click here to terminate the stream.", app_name.c_str(), exit_code); #ifdef _WIN32 - strcpy(msg, convertUtf8ToCurrentCodepage(msg).c_str()); + strncpy(msg, convertUtf8ToCurrentCodepage(msg).c_str(), std::size(msg) - 1); #endif tray.icon = TRAY_ICON; tray.notification_icon = TRAY_ICON; @@ -382,7 +382,30 @@ namespace system_tray { } void - update_tray_otp_pair(std::string device_name) { + update_tray_paired(std::string device_name) { + if (!tray_initialized) { + return; + } + + tray.notification_title = NULL; + tray.notification_text = NULL; + tray.notification_cb = NULL; + tray.notification_icon = NULL; + tray_update(&tray); + char msg[256]; + snprintf(msg, std::size(msg), "Device %s paired Succesfully. Please make sure you have access to the device.", device_name.c_str()); + #ifdef _WIN32 + strncpy(msg, convertUtf8ToCurrentCodepage(msg).c_str(), std::size(msg) - 1 - 1); + #endif + tray.notification_title = "Device Paired Succesfully"; + tray.notification_text = msg; + tray.notification_icon = TRAY_ICON; + tray.tooltip = PROJECT_NAME; + tray_update(&tray); + } + + void + update_tray_client_connected(std::string client_name) { if (!tray_initialized) { return; } @@ -394,14 +417,13 @@ namespace system_tray { tray.icon = TRAY_ICON; tray_update(&tray); char msg[256]; - snprintf(msg, std::size(msg), "OTP Pairing started for device \"%s\". Please make sure you have access to the device initiating the pairing request.", device_name.c_str()); + snprintf(msg, std::size(msg), "%s has connected to the session.", client_name.c_str()); #ifdef _WIN32 - strcpy(msg, convertUtf8ToCurrentCodepage(msg).c_str()); + strncpy(msg, convertUtf8ToCurrentCodepage(msg).c_str(), std::size(msg) - 1); #endif - tray.icon = TRAY_ICON; - tray.notification_title = "Incoming OTP Pairing Request"; + tray.notification_title = "Client Connected"; tray.notification_text = msg; - tray.notification_icon = TRAY_ICON_LOCKED; + tray.notification_icon = TRAY_ICON; tray.tooltip = PROJECT_NAME; tray_update(&tray); } diff --git a/src/system_tray.h b/src/system_tray.h index a1243c63..fae59f05 100644 --- a/src/system_tray.h +++ b/src/system_tray.h @@ -86,5 +86,8 @@ namespace system_tray { update_tray_require_pin(); void - update_tray_otp_pair(std::string device_name); + update_tray_paired(std::string device_name); + + void + update_tray_client_connected(std::string client_name); } // namespace system_tray diff --git a/src_assets/common/assets/web/pin.html b/src_assets/common/assets/web/pin.html index 5f5b576b..e4f17b0c 100644 --- a/src_assets/common/assets/web/pin.html +++ b/src_assets/common/assets/web/pin.html @@ -57,6 +57,71 @@
{{ $t('_common.warning') }} {{ $t('pin.warning_msg') }}
+ +
+
+
+
+

{{ $t('pin.device_management') }}

+ +
+
+

{{ $t('pin.device_management_desc') }}

+
+
{{ $t('_common.success') }} {{ $t('pin.unpair_single_success') }}
+ +
+
+ {{ $t('pin.unpair_all_success') }} +
+
+ {{ $t('pin.unpair_all_error') }} +
+
+
+
    + +
+
    +
    {{ $t('pin.unpair_single_no_devices') }}
    +
+
@@ -91,6 +156,106 @@ } } + /** + * Permissions: + enum class PERM: uint32_t { + _reserved = 1, + + _input = _reserved << 8, // Input permission group + input_controller = _input << 0, // Allow controller input + input_touch = _input << 1, // Allow touch input + input_pen = _input << 2, // Allow pen input + input_kbdm = _input << 3, // Allow kbd/mouse input + _all_inputs = input_controller | input_touch | input_pen | input_kbdm, + + _operation = _input << 8, // Operation permission group + clipboard_set = _operation << 0, // Allow set clipboard from client + clipboard_read = _operation << 1, // Allow read clipboard from host + file_upload = _operation << 2, // Allow upload files to host + file_dwnload = _operation << 3, // Allow download files from host + server_cmd = _operation << 4, // Allow execute server cmd + _all_opeiations = clipboard_set | clipboard_read | file_upload | file_dwnload | server_cmd, + + _action = _operation << 8, // Action permission group + list = _action << 0, // Allow list apps + view = _action << 1, // Allow view streams + launch = _action << 2, // Allow launch apps + _allow_view = view | launch, // Launch contains view permission + _all_actions = list | view | launch, + + _default = view | list, // Default permissions for new clients + _no = 0, // No permissions are granted + _all = _all_inputs | _all_opeiations | _all_actions, // All current permissions + }; + */ + + const permissionMapping = { + // Input permission group + input_controller: 0x00000100, + input_touch: 0x00000200, + input_pen: 0x00000400, + input_kbdm: 0x00000800, + _all_inputs: 0x00000F00, + + // Operation permission group + clipboard_set: 0x00010000, + clipboard_read: 0x00020000, + file_upload: 0x00040000, + file_dwnload: 0x00080000, + server_cmd: 0x00100000, + _all_operations: 0x001F0000, + + // Action permission group + list: 0x01000000, + view: 0x02000000, + launch: 0x04000000, + _allow_view: 0x06000000, + _all_actions: 0x07000000, + + // Special permissions + _default: 0x03000000, + _no: 0x00000000, + _all: 0x071F0F00 + }; + + const permissionGroups = [ + { name: 'Action', permissions: [ + { + name: 'list', + supressed_by: ['view', 'launch'] + }, { + name: 'view', + supressed_by: ['launch'] + }, { + name: 'launch', + supressed_by: [] + } + ] }, + { name: 'Operation', permissions: [ + { + name: 'server_cmd', + supressed_by: [] + } + ] }, + { name: 'Input', permissions: [ + { + name: 'input_controller', + supressed_by: [] + }, { + name: 'input_touch', + supressed_by: [] + }, { + name: 'input_pen', + supressed_by: [] + }, { + name: 'input_kbdm', + supressed_by: [] + } + ] }, + ]; + + let currentEditingClient = null; + const data = () => { return { editingHost: false, @@ -102,7 +267,12 @@ deviceName: '', hostAddr: '', hostPort: '', - hostName: '' + hostName: '', + permissionGroups, + clients: [], + showApplyMessage: false, + unpairAllPressed: false, + unpairAllStatus: null } } @@ -120,10 +290,14 @@ return !!(this.hostAddr && this.hostPort); } }, + created() { + this.refreshClients(); + }, methods: { switchTab(currentTab) { location.hash = currentTab; - Object.assign(this, data()); + const clients = this.clients; + Object.assign(this, data(), { clients }); hostInfoCache = null; clearTimeout(resetOTPTimeout); }, @@ -155,6 +329,8 @@ ).innerHTML = ``; document.querySelector("#pin-input").value = ""; document.querySelector("#name-input").value = ""; + + setTimeout(() => this.refreshClients(), 1000); } else { document.querySelector( "#status" @@ -217,7 +393,128 @@ } } }) - } + }, + clickedApplyBanner() { + this.showApplyMessage = false; + }, + editClient(client) { + if (currentEditingClient) { + this.cancelEdit(currentEditingClient); + } + currentEditingClient = client; + client.editing = true; + client.editPerm = client.perm; + client.editName = client.name; + }, + cancelEdit(client) { + client.editing = false; + client.editPerm = client.perm; + client.editName = client.name; + currentEditingClient = null; + }, + saveClient(client) { + client.editing = false; + currentEditingClient = null; + const editedClient = { + uuid: client.uuid, + name: client.editName, + perm: client.editPerm & permissionMapping._all + } + fetch("/api/clients/update", { + credentials: 'include', + method: "POST", + body: JSON.stringify(editedClient) + }).finally(() => { + setTimeout(() => { + this.refreshClients(); + }, 1000); + }); + }, + permToStr(perm) { + const permSegments = []; + permSegments.push((perm >> 24) & 0xFF); + permSegments.push((perm >> 16) & 0xFF); + permSegments.push((perm >> 8) & 0xFF); + return permSegments.map(seg => seg.toString(16).toUpperCase().padStart(2, '0')).join(' '); + }, + checkPermission(perm, permission) { + return (perm & permissionMapping[permission]) !== 0; + }, + isSupressed(perm, permission, supressed_by) { + for (const supressed of supressed_by) { + if (this.checkPermission(perm, supressed)) { + return true; + } + } + return false; + }, + togglePermission(client, permission) { + client.editPerm ^= permissionMapping[permission]; + }, + disconnectClient(uuid) { + fetch("/api/clients/disconnect", { + credentials: 'include', + method: "POST", + body: JSON.stringify({ uuid }) + }).finally(() => { + setTimeout(() => { + this.refreshClients(); + }, 1000); + }); + }, + unpairAll() { + this.unpairAllPressed = true; + fetch("/api/clients/unpair-all", { + credentials: 'include', + method: "POST" + }) + .then((r) => r.json()) + .then((r) => { + this.unpairAllPressed = false; + this.unpairAllStatus = r.status.toString() === "true"; + setTimeout(() => { + this.unpairAllStatus = null; + }, 5000); + this.refreshClients(); + }); + }, + unpairSingle(uuid) { + fetch("/api/clients/unpair", { + credentials: 'include', + method: "POST", + body: JSON.stringify({ uuid }) + }).then(() => { + this.showApplyMessage = true; + this.refreshClients(); + }); + }, + refreshClients() { + if (currentEditingClient) { + this.cancelEdit(currentEditingClient); + } + fetch("/api/clients/list", { credentials: 'include' }) + .then((response) => response.json()) + .then((response) => { + const clientList = document.querySelector("#client-list"); + if (response.status === 'true' && response.named_certs && response.named_certs.length) { + this.clients = response.named_certs.map(({name, uuid, perm, connected}) => { + const permInt = parseInt(perm, 10); + return { + name, + uuid, + perm: permInt, + connected: connected === 'true', + editing: false, + editPerm: permInt, + editName: name + } + }) + currentEditingClient = null; + } else { + this.clients = []; + } + }); + }, } }); diff --git a/src_assets/common/assets/web/public/assets/locale/en.json b/src_assets/common/assets/web/public/assets/locale/en.json index 8e5194fb..90a2e44a 100644 --- a/src_assets/common/assets/web/public/assets/locale/en.json +++ b/src_assets/common/assets/web/public/assets/locale/en.json @@ -388,6 +388,20 @@ "password_change": "Password Change", "success_msg": "Password has been changed successfully! This page will reload soon, your browser will ask you for the new credentials." }, + "permissions": { + "input_controller": "Controller Input", + "input_touch": "Touch Input", + "input_pen": "Pen Input", + "input_kbdm": "Keyboard & Mouse Input", + "clipboard_set": "Clipboard Set", + "clipboard_read": "Clipboard Read", + "file_upload": "File Upload", + "file_dwnload": "File Download", + "server_cmd": "Server Command", + "list": "List Apps", + "view": "View Streams", + "launch": "Launch Apps" + }, "pin": { "device_name": "Optional: Device Name", "pair_failure": "Pairing Failed: Check if the PIN is typed correctly", @@ -402,7 +416,15 @@ "otp_expired_msg": "OTP expired. Please request a new one.", "otp_success": "PIN request success, the PIN is available within 3 minutes.", "otp_msg": "OTP pairing is only available for the latest Artemis clients. Please use legacy pairing method for other clients.", - "otp_pair_now": "PIN generated successfully, do you want to pair now?" + "otp_pair_now": "PIN generated successfully, do you want to pair now?", + "device_management": "Device Management", + "device_management_desc": "Manage your paired devices.", + "unpair_all": "Unpair All", + "unpair_all_success": "All devices unpaired.", + "unpair_all_error": "Error while unpairing", + "unpair_single_no_devices": "There are no paired devices.", + "unpair_single_success": "However, the device(s) may still be in an active session. Use the 'Force Close' button above to end any open sessions.", + "unpair_single_unknown": "Unknown Client" }, "resource_card": { "github_discussions": "GitHub Discussions", @@ -430,15 +452,7 @@ "quit_apollo_success": "Apollo has exited.", "quit_apollo_success_ongoing": "Apollo is quitting...", "quit_apollo_confirm": "Do you really want to quit Apollo? You'll not be able to start Apollo again if you have no other methods to operate your computer.", - "troubleshooting": "Troubleshooting", - "unpair_all": "Unpair All", - "unpair_all_error": "Error while unpairing", - "unpair_all_success": "All devices unpaired.", - "unpair_desc": "Remove your paired devices. Individually unpaired devices with an active session will remain connected, but cannot start or resume a session.", - "unpair_single_no_devices": "There are no paired devices.", - "unpair_single_success": "However, the device(s) may still be in an active session. Use the 'Force Close' button above to end any open sessions.", - "unpair_single_unknown": "Unknown Client", - "unpair_title": "Unpair Devices" + "troubleshooting": "Troubleshooting" }, "welcome": { "confirm_password": "Confirm password", diff --git a/src_assets/common/assets/web/public/assets/locale/zh.json b/src_assets/common/assets/web/public/assets/locale/zh.json index ccde8ac9..22a43f6d 100644 --- a/src_assets/common/assets/web/public/assets/locale/zh.json +++ b/src_assets/common/assets/web/public/assets/locale/zh.json @@ -388,6 +388,20 @@ "password_change": "更改密码", "success_msg": "密码已成功更改!此页面即将重新加载,您的浏览器将要求您输入新的账户信息。" }, + "permissions": { + "input_controller": "手柄输入", + "input_touch": "触摸输入", + "input_pen": "笔输入", + "input_kbdm": "键鼠输入", + "clipboard_set": "上传剪贴板", + "clipboard_read": "获取剪贴板", + "file_upload": "上传文件", + "file_dwnload": "下载文件", + "server_cmd": "服务端命令", + "list": "列出APP", + "view": "查看串流", + "launch": "启动APP" + }, "pin": { "device_name": "设备名称", "pair_failure": "配对失败:请检查 PIN 码是否正确输入", @@ -402,7 +416,15 @@ "otp_expired_msg": "口令已过期,请重新请求。", "otp_success": "一次性 PIN 请求成功,3分钟内有效。", "otp_msg": "一次性口令目前仅支持最新的 Artemis 客户端使用。其他客户端请使用传统配对方式。", - "otp_pair_now": "PIN 请求成功,是否一键配对?" + "otp_pair_now": "PIN 请求成功,是否一键配对?", + "device_management": "设备管理", + "device_management_desc": "管理已配对的设备。", + "unpair_all": "全部取消配对", + "unpair_all_success": "全部取消配对成功!", + "unpair_all_error": "取消配对时出错", + "unpair_single_no_devices": "没有配对的设备。", + "unpair_single_success": "然而,设备可能仍然处于活动会话中,使用上面的“强制关闭”按钮结束任何打开的会话。", + "unpair_single_unknown": "未知客户端" }, "resource_card": { "github_discussions": "Github 讨论区", @@ -430,15 +452,7 @@ "quit_apollo_success": "Apollo 已成功退出。", "quit_apollo_success_ongoing": "Apollo 正在退出...", "quit_apollo_confirm": "确定要退出 Apollo 吗?如果没有其他操作方式,你将无法再次启动 Apollo。", - "troubleshooting": "故障排除", - "unpair_all": "全部取消配对", - "unpair_all_error": "取消配对时出错", - "unpair_all_success": "取消配对成功!", - "unpair_desc": "删除您已配对的设备。未配对的单独设备与活动会话将保持连接,但不能启动或继续会话。", - "unpair_single_no_devices": "没有配对的设备。", - "unpair_single_success": "然而,设备可能仍然处于活动会话中,使用上面的“强制关闭”按钮结束任何打开的会话。", - "unpair_single_unknown": "未知客户端", - "unpair_title": "取消配对设备" + "troubleshooting": "故障排除" }, "welcome": { "confirm_password": "确认密码", diff --git a/src_assets/common/assets/web/troubleshooting.html b/src_assets/common/assets/web/troubleshooting.html index 56ae4b3d..ec1d369a 100644 --- a/src_assets/common/assets/web/troubleshooting.html +++ b/src_assets/common/assets/web/troubleshooting.html @@ -94,40 +94,6 @@ - -
-
-
-
-

{{ $t('troubleshooting.unpair_title') }}

- -
-
-

{{ $t('troubleshooting.unpair_desc') }}

-
-
{{ $t('_common.success') }} {{ $t('troubleshooting.unpair_single_success') }}
- -
-
- {{ $t('troubleshooting.unpair_all_success') }} -
-
- {{ $t('troubleshooting.unpair_all_error') }} -
-
-
-
    -
    -
    {{client.name != "" ? client.name : $t('troubleshooting.unpair_single_unknown')}}
    -
    -
-
    -
    {{ $t('troubleshooting.unpair_single_no_devices') }}
    -
- -
@@ -166,10 +132,7 @@ logInterval: null, serverRestarting: false, serverQuitting: false, - serverQuit: false, - showApplyMessage: false, - unpairAllPressed: false, - unpairAllStatus: null, + serverQuit: false }; }, computed: { @@ -213,48 +176,6 @@ }, 5000); }); }, - unpairAll() { - this.unpairAllPressed = true; - fetch("/api/clients/unpair-all", { - credentials: 'include', - method: "POST" - }) - .then((r) => r.json()) - .then((r) => { - this.unpairAllPressed = false; - this.unpairAllStatus = r.status.toString() === "true"; - setTimeout(() => { - this.unpairAllStatus = null; - }, 5000); - this.refreshClients(); - }); - }, - unpairSingle(uuid) { - fetch("/api/clients/unpair", { credentials: 'include', - method: "POST", - body: JSON.stringify({ uuid }) - }).then(() => { - this.showApplyMessage = true; - this.refreshClients(); - }); - }, - refreshClients() { - fetch("/api/clients/list", { credentials: 'include' }) - .then((response) => response.json()) - .then((response) => { - const clientList = document.querySelector("#client-list"); - if (response.status === 'true' && response.named_certs && response.named_certs.length) { - this.clients = response.named_certs.sort((a, b) => { - return (a.name.toLowerCase() > b.name.toLowerCase() || a.name == "" ? 1 : -1) - }); - } else { - this.clients = []; - } - }); - }, - clickedApplyBanner() { - this.showApplyMessage = false; - }, copyLogs() { navigator.clipboard.writeText(this.actualLogs); },