Add host-side per-session input policy controls
Some checks failed
ci-bundle.yml / Add host-side per-session input policy controls (push) Failing after 0s
ci-copr.yml / Add host-side per-session input policy controls (push) Failing after 0s
ci-homebrew.yml / Add host-side per-session input policy controls (push) Failing after 0s

This commit is contained in:
Joey Yakimowich-Payne 2026-02-12 08:43:39 -07:00
commit 69c2693984
4 changed files with 211 additions and 1 deletions

View file

@ -961,6 +961,67 @@ namespace confighttp {
send_response(response, output_tree); 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<uint32_t>();
const bool allow_keyboard = input_tree.at("allow_keyboard").get<bool>();
const bool allow_mouse = input_tree.at("allow_mouse").get<bool>();
const bool allow_gamepad = input_tree.at("allow_gamepad").get<bool>();
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. * @brief Unpair a client.
* @param response The HTTP response object. * @param response The HTTP response object.
@ -1612,6 +1673,7 @@ namespace confighttp {
server.resource["^/api/clients/unpair$"]["POST"] = unpair; server.resource["^/api/clients/unpair$"]["POST"] = unpair;
server.resource["^/api/sessions/active$"]["GET"] = getActiveSessions; server.resource["^/api/sessions/active$"]["GET"] = getActiveSessions;
server.resource["^/api/sessions/ws-token$"]["GET"] = getActiveSessionsWsToken; 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/apps/close$"]["POST"] = closeApp;
server.resource["^/api/covers/upload$"]["POST"] = uploadCover; server.resource["^/api/covers/upload$"]["POST"] = uploadCover;
server.resource["^/api/covers/([0-9]+)$"]["GET"] = getCover; server.resource["^/api/covers/([0-9]+)$"]["GET"] = getCover;

View file

@ -2079,6 +2079,55 @@ namespace stream {
return result; 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 { namespace session {
std::atomic_uint running_sessions; std::atomic_uint running_sessions;

View file

@ -5,6 +5,7 @@
#pragma once #pragma once
// standard includes // standard includes
#include <cstdint>
#include <string> #include <string>
#include <utility> #include <utility>
#include <vector> #include <vector>
@ -31,6 +32,15 @@ namespace stream {
nlohmann::json get_active_sessions_info(); 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 { struct config_t {
audio::config_t audio; audio::config_t audio;
video::config_t monitor; video::config_t monitor;

View file

@ -151,7 +151,7 @@
<div class="card my-4"> <div class="card my-4">
<div class="card-body"> <div class="card-body">
<h2 id="input_status">Live Input Status</h2> <h2 id="input_status">Live Input Status</h2>
<p>Per-session input activity and policy. Indicators update twice per second.</p> <p>Per-session input activity and policy. Use the checkboxes to override keyboard, mouse, and gamepad permissions for each active client.</p>
</div> </div>
<div v-if="activeSessions.length === 0" class="card-body pt-0"> <div v-if="activeSessions.length === 0" class="card-body pt-0">
<em>No active streaming sessions.</em> <em>No active streaming sessions.</em>
@ -187,6 +187,41 @@
</span> </span>
</div> </div>
</div> </div>
<div class="d-flex align-items-center flex-wrap gap-3 mt-2">
<span class="text-muted" style="font-size: 0.8em">Host override:</span>
<label class="d-flex align-items-center gap-1" style="font-size: 0.8em">
<input
class="form-check-input mt-0"
type="checkbox"
:checked="s.policy.allow_keyboard"
:disabled="isSessionPolicyBusy(s.session_id)"
@change="updateSessionPolicy(s, 'allow_keyboard', $event.target.checked)"
>
KB
</label>
<label class="d-flex align-items-center gap-1" style="font-size: 0.8em">
<input
class="form-check-input mt-0"
type="checkbox"
:checked="s.policy.allow_mouse"
:disabled="isSessionPolicyBusy(s.session_id)"
@change="updateSessionPolicy(s, 'allow_mouse', $event.target.checked)"
>
Mouse
</label>
<label class="d-flex align-items-center gap-1" style="font-size: 0.8em">
<input
class="form-check-input mt-0"
type="checkbox"
:checked="s.policy.allow_gamepad"
:disabled="isSessionPolicyBusy(s.session_id)"
@change="updateSessionPolicy(s, 'allow_gamepad', $event.target.checked)"
>
Gamepad
</label>
<span v-if="isSessionPolicyBusy(s.session_id)" class="text-muted" style="font-size: 0.8em">Saving...</span>
<span v-else-if="sessionPolicyError[s.session_id]" class="text-danger" style="font-size: 0.8em">{{ sessionPolicyError[s.session_id] }}</span>
</div>
</li> </li>
</ul> </ul>
</div> </div>
@ -284,6 +319,8 @@
sessionInterval: null, sessionInterval: null,
sessionSocket: null, sessionSocket: null,
sessionReconnectTimer: null, sessionReconnectTimer: null,
sessionPolicyBusy: {},
sessionPolicyError: {},
restartPressed: false, restartPressed: false,
showApplyMessage: false, showApplyMessage: false,
platform: "", platform: "",
@ -455,6 +492,58 @@
} }
}, },
methods: { 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() { connectActiveSessionsSocket() {
fetch("./api/sessions/ws-token") fetch("./api/sessions/ws-token")
.then((r) => r.json()) .then((r) => r.json())