diff --git a/src/confighttp.cpp b/src/confighttp.cpp index 1372a594..599aba0b 100644 --- a/src/confighttp.cpp +++ b/src/confighttp.cpp @@ -961,6 +961,67 @@ namespace confighttp { send_response(response, output_tree); } + void setActiveSessionPolicy(resp_https_t response, req_https_t request) { + if (!check_content_type(response, request, "application/json")) { + return; + } + if (!authenticate(response, request)) { + return; + } + + print_req(request); + + std::stringstream ss; + ss << request->content.rdbuf(); + + try { + nlohmann::json output_tree; + const nlohmann::json input_tree = nlohmann::json::parse(ss); + + if (!input_tree.contains("session_id") || + !input_tree.contains("allow_keyboard") || + !input_tree.contains("allow_mouse") || + !input_tree.contains("allow_gamepad")) { + bad_request(response, request, "Missing required session policy fields"); + return; + } + + const uint32_t session_id = input_tree.at("session_id").get(); + const bool allow_keyboard = input_tree.at("allow_keyboard").get(); + const bool allow_mouse = input_tree.at("allow_mouse").get(); + const bool allow_gamepad = input_tree.at("allow_gamepad").get(); + + bool effective_keyboard; + bool effective_mouse; + bool effective_gamepad; + const bool updated = stream::set_active_session_input_policy( + session_id, + allow_keyboard, + allow_mouse, + allow_gamepad, + &effective_keyboard, + &effective_mouse, + &effective_gamepad); + + output_tree["status"] = updated; + if (!updated) { + output_tree["error"] = "Active session not found"; + } else { + output_tree["session_id"] = session_id; + output_tree["policy"] = { + {"allow_keyboard", effective_keyboard}, + {"allow_mouse", effective_mouse}, + {"allow_gamepad", effective_gamepad}, + }; + } + + send_response(response, output_tree); + } catch (std::exception &e) { + BOOST_LOG(warning) << "SetActiveSessionPolicy: "sv << e.what(); + bad_request(response, request, e.what()); + } + } + /** * @brief Unpair a client. * @param response The HTTP response object. @@ -1612,6 +1673,7 @@ namespace confighttp { server.resource["^/api/clients/unpair$"]["POST"] = unpair; server.resource["^/api/sessions/active$"]["GET"] = getActiveSessions; server.resource["^/api/sessions/ws-token$"]["GET"] = getActiveSessionsWsToken; + server.resource["^/api/sessions/policy$"]["POST"] = setActiveSessionPolicy; server.resource["^/api/apps/close$"]["POST"] = closeApp; server.resource["^/api/covers/upload$"]["POST"] = uploadCover; server.resource["^/api/covers/([0-9]+)$"]["GET"] = getCover; diff --git a/src/stream.cpp b/src/stream.cpp index 29132eba..d8c29690 100644 --- a/src/stream.cpp +++ b/src/stream.cpp @@ -2079,6 +2079,55 @@ namespace stream { return result; } + bool set_active_session_input_policy( + uint32_t session_id, + bool allow_keyboard, + bool allow_mouse, + bool allow_gamepad, + bool *effective_allow_keyboard, + bool *effective_allow_mouse, + bool *effective_allow_gamepad) { + auto ref = broadcast.ref(); + if (!ref) { + return false; + } + + auto lg = ref->control_server._sessions.lock(); + for (auto *session : *ref->control_server._sessions) { + if (session->launch_session_id != session_id || session->state.load(std::memory_order_relaxed) != session::state_e::RUNNING) { + continue; + } + + apply_session_input_policy(session, allow_keyboard, allow_mouse, allow_gamepad, INPUT_POLICY_REASON_HOST_OVERRIDE); + + if (send_session_input_policy(session, INPUT_POLICY_REASON_HOST_OVERRIDE)) { + BOOST_LOG(warning) << "Unable to send host policy override to client"sv; + } + + auto final_allow_keyboard = session->input_policy.allow_keyboard.load(std::memory_order_relaxed); + auto final_allow_mouse = session->input_policy.allow_mouse.load(std::memory_order_relaxed); + auto final_allow_gamepad = session->input_policy.allow_gamepad.load(std::memory_order_relaxed); + + if (effective_allow_keyboard) { + *effective_allow_keyboard = final_allow_keyboard; + } + if (effective_allow_mouse) { + *effective_allow_mouse = final_allow_mouse; + } + if (effective_allow_gamepad) { + *effective_allow_gamepad = final_allow_gamepad; + } + + BOOST_LOG(info) + << "Host input policy override applied [session="sv << session_id << "] kb="sv << final_allow_keyboard + << " mouse="sv << final_allow_mouse << " gamepad="sv << final_allow_gamepad; + + return true; + } + + return false; + } + namespace session { std::atomic_uint running_sessions; diff --git a/src/stream.h b/src/stream.h index 3335ea78..ccbb0351 100644 --- a/src/stream.h +++ b/src/stream.h @@ -5,6 +5,7 @@ #pragma once // standard includes +#include #include #include #include @@ -31,6 +32,15 @@ namespace stream { nlohmann::json get_active_sessions_info(); + bool set_active_session_input_policy( + uint32_t session_id, + bool allow_keyboard, + bool allow_mouse, + bool allow_gamepad, + bool *effective_allow_keyboard = nullptr, + bool *effective_allow_mouse = nullptr, + bool *effective_allow_gamepad = nullptr); + struct config_t { audio::config_t audio; video::config_t monitor; diff --git a/src_assets/common/assets/web/troubleshooting.html b/src_assets/common/assets/web/troubleshooting.html index 94f3b5f9..13987a9b 100644 --- a/src_assets/common/assets/web/troubleshooting.html +++ b/src_assets/common/assets/web/troubleshooting.html @@ -151,7 +151,7 @@

Live Input Status

-

Per-session input activity and policy. Indicators update twice per second.

+

Per-session input activity and policy. Use the checkboxes to override keyboard, mouse, and gamepad permissions for each active client.

No active streaming sessions. @@ -187,6 +187,41 @@
+
+ Host override: + + + + Saving... + {{ sessionPolicyError[s.session_id] }} +
@@ -284,6 +319,8 @@ sessionInterval: null, sessionSocket: null, sessionReconnectTimer: null, + sessionPolicyBusy: {}, + sessionPolicyError: {}, restartPressed: false, showApplyMessage: false, platform: "", @@ -455,6 +492,58 @@ } }, methods: { + isSessionPolicyBusy(sessionId) { + return this.sessionPolicyBusy[sessionId] === true; + }, + updateSessionPolicy(session, field, value) { + if (!session || !session.policy || session.session_id === undefined || session.session_id === null) { + return; + } + + if (!["allow_keyboard", "allow_mouse", "allow_gamepad"].includes(field)) { + return; + } + + const sessionId = session.session_id; + const nextPolicy = { + allow_keyboard: field === "allow_keyboard" ? value : !!session.policy.allow_keyboard, + allow_mouse: field === "allow_mouse" ? value : !!session.policy.allow_mouse, + allow_gamepad: field === "allow_gamepad" ? value : !!session.policy.allow_gamepad, + }; + + this.sessionPolicyBusy[sessionId] = true; + this.sessionPolicyError[sessionId] = ""; + + fetch("./api/sessions/policy", { + method: "POST", + headers: { + "Content-Type": "application/json" + }, + body: JSON.stringify({ + session_id: sessionId, + allow_keyboard: nextPolicy.allow_keyboard, + allow_mouse: nextPolicy.allow_mouse, + allow_gamepad: nextPolicy.allow_gamepad, + }) + }) + .then((r) => r.json()) + .then((r) => { + if (!r || r.status !== true || !r.policy) { + throw new Error((r && r.error) ? r.error : "Failed to update session input policy"); + } + + session.policy.allow_keyboard = !!r.policy.allow_keyboard; + session.policy.allow_mouse = !!r.policy.allow_mouse; + session.policy.allow_gamepad = !!r.policy.allow_gamepad; + }) + .catch((e) => { + this.sessionPolicyError[sessionId] = (e && e.message) ? e.message : "Failed to update session input policy"; + this.refreshActiveSessions(); + }) + .finally(() => { + this.sessionPolicyBusy[sessionId] = false; + }); + }, connectActiveSessionsSocket() { fetch("./api/sessions/ws-token") .then((r) => r.json())