Compare commits
20 commits
master
...
parsec-fea
| Author | SHA1 | Date | |
|---|---|---|---|
| 5dd6e9214e | |||
| 808d868b8e | |||
| 69c2693984 | |||
| 7e2ef2037c | |||
| 07c8a7b5fb | |||
| 6fdeb1b868 | |||
| 5d0a6106e6 | |||
| e2d3f23575 | |||
| 1981060f09 | |||
| 718c45b76a | |||
| 54afa9bb67 | |||
| d5218297a4 | |||
| 3f19554cfa | |||
| 916eb1a213 | |||
| c4315144b3 | |||
| a271a63602 | |||
| 0f070b144f | |||
| 6c3a91357c | |||
| 0c16e913da | |||
| bbc1f724e1 |
15 changed files with 1120 additions and 49 deletions
|
|
@ -556,8 +556,8 @@ namespace config {
|
|||
true, // client gamepads with touchpads are emulated as DS4
|
||||
true, // ds5_inputtino_randomize_mac
|
||||
|
||||
true, // keyboard enabled
|
||||
true, // mouse enabled
|
||||
false, // keyboard disabled (Parsec-style default)
|
||||
false, // mouse disabled (Parsec-style default)
|
||||
true, // controller enabled
|
||||
true, // always send scancodes
|
||||
true, // high resolution scrolling
|
||||
|
|
@ -1235,6 +1235,8 @@ namespace config {
|
|||
bool_f(vars, "high_resolution_scrolling", input.high_resolution_scrolling);
|
||||
bool_f(vars, "native_pen_touch", input.native_pen_touch);
|
||||
|
||||
list_string_f(vars, "owner_client_uuids", input.owner_client_uuids);
|
||||
|
||||
bool_f(vars, "notify_pre_releases", sunshine.notify_pre_releases);
|
||||
bool_f(vars, "system_tray", sunshine.system_tray);
|
||||
|
||||
|
|
|
|||
|
|
@ -203,6 +203,8 @@ namespace config {
|
|||
|
||||
bool high_resolution_scrolling;
|
||||
bool native_pen_touch;
|
||||
|
||||
std::vector<std::string> owner_client_uuids;
|
||||
};
|
||||
|
||||
namespace flag {
|
||||
|
|
|
|||
|
|
@ -7,10 +7,18 @@
|
|||
#define BOOST_BIND_GLOBAL_PLACEHOLDERS
|
||||
|
||||
// standard includes
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <filesystem>
|
||||
#include <format>
|
||||
#include <fstream>
|
||||
#include <mutex>
|
||||
#include <set>
|
||||
#include <thread>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
// lib includes
|
||||
#include <boost/algorithm/string.hpp>
|
||||
|
|
@ -19,6 +27,8 @@
|
|||
#include <nlohmann/json.hpp>
|
||||
#include <Simple-Web-Server/crypto.hpp>
|
||||
#include <Simple-Web-Server/server_https.hpp>
|
||||
#include <openssl/evp.h>
|
||||
#include <openssl/sha.h>
|
||||
|
||||
#ifdef _WIN32
|
||||
#include "platform/windows/misc.h"
|
||||
|
|
@ -40,6 +50,7 @@
|
|||
#include "nvhttp.h"
|
||||
#include "platform/common.h"
|
||||
#include "process.h"
|
||||
#include "stream.h"
|
||||
#include "utility.h"
|
||||
#include "uuid.h"
|
||||
|
||||
|
|
@ -54,6 +65,119 @@ namespace confighttp {
|
|||
using resp_https_t = std::shared_ptr<typename SimpleWeb::ServerBase<SimpleWeb::HTTPS>::Response>;
|
||||
using req_https_t = std::shared_ptr<typename SimpleWeb::ServerBase<SimpleWeb::HTTPS>::Request>;
|
||||
|
||||
namespace {
|
||||
struct ws_client_t {
|
||||
std::unique_ptr<SimpleWeb::HTTPS> socket;
|
||||
std::mutex write_mutex;
|
||||
std::atomic_bool alive {true};
|
||||
};
|
||||
|
||||
constexpr std::chrono::seconds WS_TOKEN_TTL {30};
|
||||
constexpr std::chrono::milliseconds WS_PUSH_INTERVAL {250};
|
||||
constexpr std::string_view WS_GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
|
||||
|
||||
std::mutex ws_clients_mutex;
|
||||
std::vector<std::shared_ptr<ws_client_t>> ws_clients;
|
||||
|
||||
std::mutex ws_tokens_mutex;
|
||||
std::unordered_map<std::string, std::chrono::steady_clock::time_point> ws_tokens;
|
||||
|
||||
void cleanup_ws_tokens(const std::chrono::steady_clock::time_point &now) {
|
||||
for (auto it = ws_tokens.begin(); it != ws_tokens.end();) {
|
||||
if (it->second <= now) {
|
||||
it = ws_tokens.erase(it);
|
||||
} else {
|
||||
++it;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::string create_ws_token() {
|
||||
auto now = std::chrono::steady_clock::now();
|
||||
auto token = uuid_util::uuid_t::generate().string();
|
||||
|
||||
std::lock_guard lg {ws_tokens_mutex};
|
||||
cleanup_ws_tokens(now);
|
||||
ws_tokens[token] = now + WS_TOKEN_TTL;
|
||||
|
||||
return token;
|
||||
}
|
||||
|
||||
bool consume_ws_token(std::string_view token) {
|
||||
auto now = std::chrono::steady_clock::now();
|
||||
|
||||
std::lock_guard lg {ws_tokens_mutex};
|
||||
cleanup_ws_tokens(now);
|
||||
|
||||
auto it = ws_tokens.find(std::string(token));
|
||||
if (it == ws_tokens.end()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
ws_tokens.erase(it);
|
||||
return true;
|
||||
}
|
||||
|
||||
std::string websocket_accept_key(std::string_view key) {
|
||||
std::string input;
|
||||
input.reserve(key.size() + WS_GUID.size());
|
||||
input.append(key);
|
||||
input.append(WS_GUID);
|
||||
|
||||
std::array<unsigned char, SHA_DIGEST_LENGTH> digest {};
|
||||
SHA1(reinterpret_cast<const unsigned char *>(input.data()), input.size(), digest.data());
|
||||
|
||||
std::array<unsigned char, 4 * ((SHA_DIGEST_LENGTH + 2) / 3) + 1> encoded {};
|
||||
auto len = EVP_EncodeBlock(encoded.data(), digest.data(), digest.size());
|
||||
|
||||
return std::string(reinterpret_cast<const char *>(encoded.data()), len);
|
||||
}
|
||||
|
||||
std::string websocket_text_frame(const std::string &payload) {
|
||||
std::string frame;
|
||||
frame.reserve(payload.size() + 10);
|
||||
frame.push_back(static_cast<char>(0x81));
|
||||
|
||||
auto len = payload.size();
|
||||
if (len <= 125) {
|
||||
frame.push_back(static_cast<char>(len));
|
||||
} else if (len <= 0xFFFF) {
|
||||
frame.push_back(static_cast<char>(126));
|
||||
frame.push_back(static_cast<char>((len >> 8) & 0xFF));
|
||||
frame.push_back(static_cast<char>(len & 0xFF));
|
||||
} else {
|
||||
frame.push_back(static_cast<char>(127));
|
||||
for (int i = 7; i >= 0; --i) {
|
||||
frame.push_back(static_cast<char>((len >> (i * 8)) & 0xFF));
|
||||
}
|
||||
}
|
||||
|
||||
frame.append(payload);
|
||||
return frame;
|
||||
}
|
||||
|
||||
void ws_write_http_response(std::unique_ptr<SimpleWeb::HTTPS> &socket, SimpleWeb::StatusCode code, std::string_view body) {
|
||||
if (!socket) {
|
||||
return;
|
||||
}
|
||||
|
||||
std::string response;
|
||||
response.reserve(256 + body.size());
|
||||
response.append("HTTP/1.1 ");
|
||||
response.append(SimpleWeb::status_code(code));
|
||||
response.append("\r\nContent-Type: application/json\r\n");
|
||||
response.append("Connection: close\r\n");
|
||||
response.append("Content-Length: ");
|
||||
response.append(std::to_string(body.size()));
|
||||
response.append("\r\n\r\n");
|
||||
response.append(body);
|
||||
|
||||
boost::system::error_code ec;
|
||||
boost::asio::write(*socket, boost::asio::buffer(response), ec);
|
||||
socket->lowest_layer().close(ec);
|
||||
}
|
||||
}
|
||||
|
||||
enum class op_e {
|
||||
ADD, ///< Add client
|
||||
REMOVE ///< Remove client
|
||||
|
|
@ -136,6 +260,24 @@ namespace confighttp {
|
|||
response->write(SimpleWeb::StatusCode::redirection_temporary_redirect, headers);
|
||||
}
|
||||
|
||||
bool authenticate_header(std::string_view raw_auth) {
|
||||
if (raw_auth.size() < "Basic "sv.size() || raw_auth.substr(0, "Basic "sv.size()) != "Basic "sv) {
|
||||
return false;
|
||||
}
|
||||
|
||||
auto auth_data = SimpleWeb::Crypto::Base64::decode(std::string(raw_auth.substr("Basic "sv.length())));
|
||||
auto index = (int) auth_data.find(':');
|
||||
if (index >= auth_data.size() - 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
auto username = auth_data.substr(0, index);
|
||||
auto password = auth_data.substr(index + 1);
|
||||
auto hash = util::hex(crypto::hash(password + config::sunshine.salt)).to_string();
|
||||
|
||||
return boost::iequals(username, config::sunshine.username) && hash == config::sunshine.password;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Authenticate the user.
|
||||
* @param response The HTTP response object.
|
||||
|
|
@ -167,19 +309,7 @@ namespace confighttp {
|
|||
return false;
|
||||
}
|
||||
|
||||
auto &rawAuth = auth->second;
|
||||
auto authData = SimpleWeb::Crypto::Base64::decode(rawAuth.substr("Basic "sv.length()));
|
||||
|
||||
auto index = (int) authData.find(':');
|
||||
if (index >= authData.size() - 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
auto username = authData.substr(0, index);
|
||||
auto password = authData.substr(index + 1);
|
||||
auto hash = util::hex(crypto::hash(password + config::sunshine.salt)).to_string();
|
||||
|
||||
if (!boost::iequals(username, config::sunshine.username) || hash != config::sunshine.password) {
|
||||
if (!authenticate_header(auth->second)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
@ -805,6 +935,93 @@ namespace confighttp {
|
|||
send_response(response, output_tree);
|
||||
}
|
||||
|
||||
void getActiveSessions(resp_https_t response, req_https_t request) {
|
||||
if (!authenticate(response, request)) {
|
||||
return;
|
||||
}
|
||||
|
||||
print_req(request);
|
||||
|
||||
nlohmann::json output_tree;
|
||||
output_tree["sessions"] = stream::get_active_sessions_info();
|
||||
output_tree["status"] = true;
|
||||
send_response(response, output_tree);
|
||||
}
|
||||
|
||||
void getActiveSessionsWsToken(resp_https_t response, req_https_t request) {
|
||||
if (!authenticate(response, request)) {
|
||||
return;
|
||||
}
|
||||
|
||||
print_req(request);
|
||||
|
||||
nlohmann::json output_tree;
|
||||
output_tree["token"] = create_ws_token();
|
||||
output_tree["status"] = true;
|
||||
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.
|
||||
* @param response The HTTP response object.
|
||||
|
|
@ -1454,12 +1671,74 @@ namespace confighttp {
|
|||
server.resource["^/api/clients/unpair-all$"]["POST"] = unpairAll;
|
||||
server.resource["^/api/clients/list$"]["GET"] = getClients;
|
||||
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;
|
||||
server.resource["^/images/sunshine.ico$"]["GET"] = getFaviconImage;
|
||||
server.resource["^/images/logo-sunshine-45.png$"]["GET"] = getSunshineLogoImage;
|
||||
server.resource["^/assets\\/.+$"]["GET"] = getNodeModules;
|
||||
|
||||
server.on_upgrade = [](std::unique_ptr<SimpleWeb::HTTPS> &socket, req_https_t request) {
|
||||
if (!socket || request->path != "/api/sessions/active/ws") {
|
||||
ws_write_http_response(socket, SimpleWeb::StatusCode::client_error_not_found, R"({"status":false})");
|
||||
return;
|
||||
}
|
||||
|
||||
auto address = net::addr_to_normalized_string(request->remote_endpoint().address());
|
||||
if (net::from_address(address) > http::origin_web_ui_allowed) {
|
||||
ws_write_http_response(socket, SimpleWeb::StatusCode::client_error_forbidden, R"({"status":false})");
|
||||
return;
|
||||
}
|
||||
|
||||
if (config::sunshine.username.empty()) {
|
||||
ws_write_http_response(socket, SimpleWeb::StatusCode::client_error_unauthorized, R"({"status":false})");
|
||||
return;
|
||||
}
|
||||
|
||||
auto auth = request->header.find("Authorization");
|
||||
if (auth != request->header.end() && !authenticate_header(auth->second)) {
|
||||
ws_write_http_response(socket, SimpleWeb::StatusCode::client_error_unauthorized, R"({"status":false})");
|
||||
return;
|
||||
}
|
||||
|
||||
auto query = request->parse_query_string();
|
||||
auto token_it = query.find("token");
|
||||
if (token_it == query.end() || !consume_ws_token(token_it->second)) {
|
||||
ws_write_http_response(socket, SimpleWeb::StatusCode::client_error_forbidden, R"({"status":false})");
|
||||
return;
|
||||
}
|
||||
|
||||
auto key_it = request->header.find("Sec-WebSocket-Key");
|
||||
if (key_it == request->header.end()) {
|
||||
ws_write_http_response(socket, SimpleWeb::StatusCode::client_error_bad_request, R"({"status":false})");
|
||||
return;
|
||||
}
|
||||
|
||||
std::string response;
|
||||
response.reserve(256);
|
||||
response.append("HTTP/1.1 101 Switching Protocols\r\n");
|
||||
response.append("Upgrade: websocket\r\n");
|
||||
response.append("Connection: Upgrade\r\n");
|
||||
response.append("Sec-WebSocket-Accept: ");
|
||||
response.append(websocket_accept_key(key_it->second));
|
||||
response.append("\r\n\r\n");
|
||||
|
||||
boost::system::error_code ec;
|
||||
boost::asio::write(*socket, boost::asio::buffer(response), ec);
|
||||
if (ec) {
|
||||
socket->lowest_layer().close(ec);
|
||||
return;
|
||||
}
|
||||
|
||||
auto client = std::make_shared<ws_client_t>();
|
||||
client->socket = std::move(socket);
|
||||
|
||||
std::lock_guard lg {ws_clients_mutex};
|
||||
ws_clients.emplace_back(std::move(client));
|
||||
};
|
||||
server.config.reuse_address = true;
|
||||
server.config.address = net::get_bind_address(address_family);
|
||||
server.config.port = port_https;
|
||||
|
|
@ -1483,11 +1762,58 @@ namespace confighttp {
|
|||
};
|
||||
std::thread tcp {accept_and_run, &server};
|
||||
|
||||
std::thread ws_broadcast {[&] {
|
||||
platf::set_thread_name("confighttp::ws");
|
||||
|
||||
while (!shutdown_event->peek()) {
|
||||
std::vector<std::shared_ptr<ws_client_t>> clients;
|
||||
{
|
||||
std::lock_guard lg {ws_clients_mutex};
|
||||
clients = ws_clients;
|
||||
}
|
||||
|
||||
if (!clients.empty()) {
|
||||
nlohmann::json output_tree;
|
||||
output_tree["sessions"] = stream::get_active_sessions_info();
|
||||
output_tree["status"] = true;
|
||||
auto frame = websocket_text_frame(output_tree.dump());
|
||||
|
||||
for (auto &client : clients) {
|
||||
if (!client->alive.load(std::memory_order_relaxed)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
std::lock_guard write_lg {client->write_mutex};
|
||||
if (!client->socket) {
|
||||
client->alive.store(false, std::memory_order_relaxed);
|
||||
continue;
|
||||
}
|
||||
|
||||
boost::system::error_code ec;
|
||||
boost::asio::write(*client->socket, boost::asio::buffer(frame), ec);
|
||||
if (ec) {
|
||||
client->alive.store(false, std::memory_order_relaxed);
|
||||
client->socket->lowest_layer().close(ec);
|
||||
}
|
||||
}
|
||||
|
||||
std::lock_guard lg {ws_clients_mutex};
|
||||
ws_clients.erase(std::remove_if(std::begin(ws_clients), std::end(ws_clients), [](const auto &client) {
|
||||
return !client->alive.load(std::memory_order_relaxed);
|
||||
}), std::end(ws_clients));
|
||||
}
|
||||
|
||||
std::this_thread::sleep_for(WS_PUSH_INTERVAL);
|
||||
}
|
||||
}};
|
||||
|
||||
// Wait for any event
|
||||
shutdown_event->view();
|
||||
|
||||
server.stop();
|
||||
|
||||
ws_broadcast.join();
|
||||
|
||||
tcp.join();
|
||||
}
|
||||
} // namespace confighttp
|
||||
|
|
|
|||
122
src/input.cpp
122
src/input.cpp
|
|
@ -41,6 +41,18 @@ namespace input {
|
|||
#define DISABLE_LEFT_BUTTON_DELAY ((thread_pool_util::ThreadPool::task_id_t) 0x01)
|
||||
#define ENABLE_LEFT_BUTTON_DELAY nullptr
|
||||
|
||||
static uint64_t now_ms() {
|
||||
return std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||
std::chrono::steady_clock::now().time_since_epoch()
|
||||
).count();
|
||||
}
|
||||
|
||||
static inline void touch_activity(std::atomic<uint64_t> *ts) {
|
||||
if (ts) {
|
||||
ts->store(now_ms(), std::memory_order_relaxed);
|
||||
}
|
||||
}
|
||||
|
||||
constexpr auto VKEY_SHIFT = 0x10;
|
||||
constexpr auto VKEY_LSHIFT = 0xA0;
|
||||
constexpr auto VKEY_RSHIFT = 0xA1;
|
||||
|
|
@ -172,7 +184,10 @@ namespace input {
|
|||
mouse_left_button_timeout {},
|
||||
touch_port {{0, 0, 0, 0}, 0, 0, 1.0f, 1.0f, 0, 0},
|
||||
accumulated_vscroll_delta {},
|
||||
accumulated_hscroll_delta {} {
|
||||
accumulated_hscroll_delta {},
|
||||
allow_keyboard {true},
|
||||
allow_mouse {true},
|
||||
allow_gamepad {true} {
|
||||
}
|
||||
|
||||
// Keep track of alt+ctrl+shift key combo
|
||||
|
|
@ -193,8 +208,28 @@ namespace input {
|
|||
|
||||
int32_t accumulated_vscroll_delta;
|
||||
int32_t accumulated_hscroll_delta;
|
||||
|
||||
std::atomic<uint64_t> *activity_keyboard_ms = nullptr;
|
||||
std::atomic<uint64_t> *activity_mouse_ms = nullptr;
|
||||
std::atomic<uint64_t> *activity_gamepad_ms = nullptr;
|
||||
|
||||
std::atomic<bool> allow_keyboard;
|
||||
std::atomic<bool> allow_mouse;
|
||||
std::atomic<bool> allow_gamepad;
|
||||
};
|
||||
|
||||
static inline bool keyboard_allowed(const std::shared_ptr<input_t> &input) {
|
||||
return config::input.keyboard && input->allow_keyboard.load(std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
static inline bool mouse_allowed(const std::shared_ptr<input_t> &input) {
|
||||
return config::input.mouse && input->allow_mouse.load(std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
static inline bool gamepad_allowed(const std::shared_ptr<input_t> &input) {
|
||||
return config::input.controller && input->allow_gamepad.load(std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Apply shortcut based on VKEY
|
||||
* @param keyCode The VKEY code
|
||||
|
|
@ -442,10 +477,12 @@ namespace input {
|
|||
}
|
||||
|
||||
void passthrough(std::shared_ptr<input_t> &input, PNV_REL_MOUSE_MOVE_PACKET packet) {
|
||||
if (!config::input.mouse) {
|
||||
if (!mouse_allowed(input)) {
|
||||
return;
|
||||
}
|
||||
|
||||
touch_activity(input->activity_mouse_ms);
|
||||
|
||||
input->mouse_left_button_timeout = DISABLE_LEFT_BUTTON_DELAY;
|
||||
platf::move_mouse(platf_input, util::endian::big(packet->deltaX), util::endian::big(packet->deltaY));
|
||||
}
|
||||
|
|
@ -539,10 +576,12 @@ namespace input {
|
|||
}
|
||||
|
||||
void passthrough(std::shared_ptr<input_t> &input, PNV_ABS_MOUSE_MOVE_PACKET packet) {
|
||||
if (!config::input.mouse) {
|
||||
if (!mouse_allowed(input)) {
|
||||
return;
|
||||
}
|
||||
|
||||
touch_activity(input->activity_mouse_ms);
|
||||
|
||||
if (input->mouse_left_button_timeout == DISABLE_LEFT_BUTTON_DELAY) {
|
||||
input->mouse_left_button_timeout = ENABLE_LEFT_BUTTON_DELAY;
|
||||
}
|
||||
|
|
@ -589,10 +628,12 @@ namespace input {
|
|||
}
|
||||
|
||||
void passthrough(std::shared_ptr<input_t> &input, PNV_MOUSE_BUTTON_PACKET packet) {
|
||||
if (!config::input.mouse) {
|
||||
if (!mouse_allowed(input)) {
|
||||
return;
|
||||
}
|
||||
|
||||
touch_activity(input->activity_mouse_ms);
|
||||
|
||||
auto release = util::endian::little(packet->header.magic) == MOUSE_BUTTON_UP_EVENT_MAGIC_GEN5;
|
||||
auto button = util::endian::big(packet->button);
|
||||
if (button > 0 && button < mouse_press.size()) {
|
||||
|
|
@ -753,10 +794,12 @@ namespace input {
|
|||
}
|
||||
|
||||
void passthrough(std::shared_ptr<input_t> &input, PNV_KEYBOARD_PACKET packet) {
|
||||
if (!config::input.keyboard) {
|
||||
if (!keyboard_allowed(input)) {
|
||||
return;
|
||||
}
|
||||
|
||||
touch_activity(input->activity_keyboard_ms);
|
||||
|
||||
auto release = util::endian::little(packet->header.magic) == KEY_UP_EVENT_MAGIC;
|
||||
auto keyCode = packet->keyCode & 0x00FF;
|
||||
|
||||
|
|
@ -813,10 +856,12 @@ namespace input {
|
|||
* @param packet The scroll packet.
|
||||
*/
|
||||
void passthrough(std::shared_ptr<input_t> &input, PNV_SCROLL_PACKET packet) {
|
||||
if (!config::input.mouse) {
|
||||
if (!mouse_allowed(input)) {
|
||||
return;
|
||||
}
|
||||
|
||||
touch_activity(input->activity_mouse_ms);
|
||||
|
||||
if (config::input.high_resolution_scrolling) {
|
||||
platf::scroll(platf_input, util::endian::big(packet->scrollAmt1));
|
||||
} else {
|
||||
|
|
@ -836,10 +881,12 @@ namespace input {
|
|||
* @param packet The scroll packet.
|
||||
*/
|
||||
void passthrough(std::shared_ptr<input_t> &input, PSS_HSCROLL_PACKET packet) {
|
||||
if (!config::input.mouse) {
|
||||
if (!mouse_allowed(input)) {
|
||||
return;
|
||||
}
|
||||
|
||||
touch_activity(input->activity_mouse_ms);
|
||||
|
||||
if (config::input.high_resolution_scrolling) {
|
||||
platf::hscroll(platf_input, util::endian::big(packet->scrollAmount));
|
||||
} else {
|
||||
|
|
@ -853,8 +900,8 @@ namespace input {
|
|||
}
|
||||
}
|
||||
|
||||
void passthrough(PNV_UNICODE_PACKET packet) {
|
||||
if (!config::input.keyboard) {
|
||||
void passthrough(std::shared_ptr<input_t> &input, PNV_UNICODE_PACKET packet) {
|
||||
if (!keyboard_allowed(input)) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -868,10 +915,12 @@ namespace input {
|
|||
* @param packet The controller arrival packet.
|
||||
*/
|
||||
void passthrough(std::shared_ptr<input_t> &input, PSS_CONTROLLER_ARRIVAL_PACKET packet) {
|
||||
if (!config::input.controller) {
|
||||
if (!gamepad_allowed(input)) {
|
||||
return;
|
||||
}
|
||||
|
||||
touch_activity(input->activity_gamepad_ms);
|
||||
|
||||
if (packet->controllerNumber < 0 || packet->controllerNumber >= input->gamepads.size()) {
|
||||
BOOST_LOG(warning) << "ControllerNumber out of range ["sv << packet->controllerNumber << ']';
|
||||
return;
|
||||
|
|
@ -908,10 +957,12 @@ namespace input {
|
|||
* @param packet The touch packet.
|
||||
*/
|
||||
void passthrough(std::shared_ptr<input_t> &input, PSS_TOUCH_PACKET packet) {
|
||||
if (!config::input.mouse) {
|
||||
if (!mouse_allowed(input)) {
|
||||
return;
|
||||
}
|
||||
|
||||
touch_activity(input->activity_mouse_ms);
|
||||
|
||||
// Convert the client normalized coordinates to touchport coordinates
|
||||
auto coords = client_to_touchport(input, {from_clamped_netfloat(packet->x, 0.0f, 1.0f) * 65535.f, from_clamped_netfloat(packet->y, 0.0f, 1.0f) * 65535.f}, {65535.f, 65535.f});
|
||||
if (!coords) {
|
||||
|
|
@ -964,10 +1015,12 @@ namespace input {
|
|||
* @param packet The pen packet.
|
||||
*/
|
||||
void passthrough(std::shared_ptr<input_t> &input, PSS_PEN_PACKET packet) {
|
||||
if (!config::input.mouse) {
|
||||
if (!mouse_allowed(input)) {
|
||||
return;
|
||||
}
|
||||
|
||||
touch_activity(input->activity_mouse_ms);
|
||||
|
||||
// Convert the client normalized coordinates to touchport coordinates
|
||||
auto coords = client_to_touchport(input, {from_clamped_netfloat(packet->x, 0.0f, 1.0f) * 65535.f, from_clamped_netfloat(packet->y, 0.0f, 1.0f) * 65535.f}, {65535.f, 65535.f});
|
||||
if (!coords) {
|
||||
|
|
@ -1022,10 +1075,12 @@ namespace input {
|
|||
* @param packet The controller touch packet.
|
||||
*/
|
||||
void passthrough(std::shared_ptr<input_t> &input, PSS_CONTROLLER_TOUCH_PACKET packet) {
|
||||
if (!config::input.controller) {
|
||||
if (!gamepad_allowed(input)) {
|
||||
return;
|
||||
}
|
||||
|
||||
touch_activity(input->activity_gamepad_ms);
|
||||
|
||||
if (packet->controllerNumber < 0 || packet->controllerNumber >= input->gamepads.size()) {
|
||||
BOOST_LOG(warning) << "ControllerNumber out of range ["sv << packet->controllerNumber << ']';
|
||||
return;
|
||||
|
|
@ -1055,10 +1110,12 @@ namespace input {
|
|||
* @param packet The controller motion packet.
|
||||
*/
|
||||
void passthrough(std::shared_ptr<input_t> &input, PSS_CONTROLLER_MOTION_PACKET packet) {
|
||||
if (!config::input.controller) {
|
||||
if (!gamepad_allowed(input)) {
|
||||
return;
|
||||
}
|
||||
|
||||
touch_activity(input->activity_gamepad_ms);
|
||||
|
||||
if (packet->controllerNumber < 0 || packet->controllerNumber >= input->gamepads.size()) {
|
||||
BOOST_LOG(warning) << "ControllerNumber out of range ["sv << packet->controllerNumber << ']';
|
||||
return;
|
||||
|
|
@ -1087,10 +1144,12 @@ namespace input {
|
|||
* @param packet The controller battery packet.
|
||||
*/
|
||||
void passthrough(std::shared_ptr<input_t> &input, PSS_CONTROLLER_BATTERY_PACKET packet) {
|
||||
if (!config::input.controller) {
|
||||
if (!gamepad_allowed(input)) {
|
||||
return;
|
||||
}
|
||||
|
||||
touch_activity(input->activity_gamepad_ms);
|
||||
|
||||
if (packet->controllerNumber < 0 || packet->controllerNumber >= input->gamepads.size()) {
|
||||
BOOST_LOG(warning) << "ControllerNumber out of range ["sv << packet->controllerNumber << ']';
|
||||
return;
|
||||
|
|
@ -1112,10 +1171,12 @@ namespace input {
|
|||
}
|
||||
|
||||
void passthrough(std::shared_ptr<input_t> &input, PNV_MULTI_CONTROLLER_PACKET packet) {
|
||||
if (!config::input.controller) {
|
||||
if (!gamepad_allowed(input)) {
|
||||
return;
|
||||
}
|
||||
|
||||
touch_activity(input->activity_gamepad_ms);
|
||||
|
||||
if (packet->controllerNumber < 0 || packet->controllerNumber >= input->gamepads.size()) {
|
||||
BOOST_LOG(warning) << "ControllerNumber out of range ["sv << packet->controllerNumber << ']';
|
||||
|
||||
|
|
@ -1578,7 +1639,7 @@ namespace input {
|
|||
passthrough(input, (PNV_KEYBOARD_PACKET) payload);
|
||||
break;
|
||||
case UTF8_TEXT_EVENT_MAGIC:
|
||||
passthrough((PNV_UNICODE_PACKET) payload);
|
||||
passthrough(input, (PNV_UNICODE_PACKET) payload);
|
||||
break;
|
||||
case MULTI_CONTROLLER_MAGIC_GEN5:
|
||||
passthrough(input, (PNV_MULTI_CONTROLLER_PACKET) payload);
|
||||
|
|
@ -1626,8 +1687,8 @@ namespace input {
|
|||
for (int x = 0; x < mouse_press.size(); ++x) {
|
||||
if (mouse_press[x]) {
|
||||
platf::button_mouse(platf_input, x, true);
|
||||
mouse_press[x] = false;
|
||||
}
|
||||
mouse_press[x] = false;
|
||||
}
|
||||
|
||||
for (auto &kp : key_press) {
|
||||
|
|
@ -1638,7 +1699,10 @@ namespace input {
|
|||
platf::keyboard_update(platf_input, vk_from_kpid(kp.first) & 0x00FF, true, flags_from_kpid(kp.first));
|
||||
key_press[kp.first] = false;
|
||||
}
|
||||
});
|
||||
|
||||
key_press.clear();
|
||||
key_press_repeat_id = nullptr;
|
||||
}).wait();
|
||||
}
|
||||
|
||||
class deinit_t: public platf::deinit_t {
|
||||
|
|
@ -1680,4 +1744,24 @@ namespace input {
|
|||
|
||||
return input;
|
||||
}
|
||||
|
||||
void set_activity_pointers(
|
||||
std::shared_ptr<input_t> &input,
|
||||
std::atomic<uint64_t> *keyboard_ms,
|
||||
std::atomic<uint64_t> *mouse_ms,
|
||||
std::atomic<uint64_t> *gamepad_ms) {
|
||||
input->activity_keyboard_ms = keyboard_ms;
|
||||
input->activity_mouse_ms = mouse_ms;
|
||||
input->activity_gamepad_ms = gamepad_ms;
|
||||
}
|
||||
|
||||
void set_policy(
|
||||
std::shared_ptr<input_t> &input,
|
||||
bool allow_keyboard,
|
||||
bool allow_mouse,
|
||||
bool allow_gamepad) {
|
||||
input->allow_keyboard.store(allow_keyboard, std::memory_order_relaxed);
|
||||
input->allow_mouse.store(allow_mouse, std::memory_order_relaxed);
|
||||
input->allow_gamepad.store(allow_gamepad, std::memory_order_relaxed);
|
||||
}
|
||||
} // namespace input
|
||||
|
|
|
|||
12
src/input.h
12
src/input.h
|
|
@ -24,6 +24,18 @@ namespace input {
|
|||
|
||||
std::shared_ptr<input_t> alloc(safe::mail_t mail);
|
||||
|
||||
void set_activity_pointers(
|
||||
std::shared_ptr<input_t> &input,
|
||||
std::atomic<uint64_t> *keyboard_ms,
|
||||
std::atomic<uint64_t> *mouse_ms,
|
||||
std::atomic<uint64_t> *gamepad_ms);
|
||||
|
||||
void set_policy(
|
||||
std::shared_ptr<input_t> &input,
|
||||
bool allow_keyboard,
|
||||
bool allow_mouse,
|
||||
bool allow_gamepad);
|
||||
|
||||
struct touch_port_t: public platf::touch_port_t {
|
||||
int env_width, env_height;
|
||||
|
||||
|
|
|
|||
|
|
@ -279,6 +279,53 @@ namespace nvhttp {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Resolve the real paired client UUID from the SSL peer certificate.
|
||||
* @param request The HTTPS request whose peer cert identifies the client.
|
||||
* @return The paired client's UUID, or empty string if not found.
|
||||
*
|
||||
* Moonlight sends a placeholder uniqueid ("0123456789ABCDEF") in the launch
|
||||
* query string. The real identity is the SSL client certificate presented
|
||||
* during the TLS handshake, which we match against our paired device list.
|
||||
*/
|
||||
std::string resolve_client_uuid(req_https_t request) {
|
||||
auto conn = request->connection_shared();
|
||||
if (!conn) {
|
||||
BOOST_LOG(warning) << "resolve_client_uuid: connection expired"sv;
|
||||
return {};
|
||||
}
|
||||
|
||||
auto ssl = conn->socket->native_handle();
|
||||
if (!ssl) {
|
||||
BOOST_LOG(warning) << "resolve_client_uuid: no SSL handle"sv;
|
||||
return {};
|
||||
}
|
||||
|
||||
crypto::x509_t peer_cert {
|
||||
#if OPENSSL_VERSION_MAJOR >= 3
|
||||
SSL_get1_peer_certificate(ssl)
|
||||
#else
|
||||
SSL_get_peer_certificate(ssl)
|
||||
#endif
|
||||
};
|
||||
if (!peer_cert) {
|
||||
BOOST_LOG(warning) << "resolve_client_uuid: no peer certificate"sv;
|
||||
return {};
|
||||
}
|
||||
|
||||
auto peer_pem = crypto::pem(peer_cert);
|
||||
|
||||
for (const auto &device : client_root.named_devices) {
|
||||
if (device.cert == peer_pem) {
|
||||
BOOST_LOG(info) << "resolve_client_uuid: matched paired client '"sv << device.name << "' uuid="sv << device.uuid;
|
||||
return device.uuid;
|
||||
}
|
||||
}
|
||||
|
||||
BOOST_LOG(warning) << "resolve_client_uuid: peer cert did not match any paired device"sv;
|
||||
return {};
|
||||
}
|
||||
|
||||
std::shared_ptr<rtsp_stream::launch_session_t> make_launch_session(bool host_audio, const args_t &args) {
|
||||
auto launch_session = std::make_shared<rtsp_stream::launch_session_t>();
|
||||
|
||||
|
|
@ -782,6 +829,15 @@ namespace nvhttp {
|
|||
return named_cert_nodes;
|
||||
}
|
||||
|
||||
std::string get_client_name(const std::string &uuid) {
|
||||
for (auto &named_cert : client_root.named_devices) {
|
||||
if (named_cert.uuid == uuid) {
|
||||
return named_cert.name;
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
void applist(resp_https_t response, req_https_t request) {
|
||||
print_req<SunshineHTTPS>(request);
|
||||
|
||||
|
|
@ -859,6 +915,11 @@ namespace nvhttp {
|
|||
host_audio = util::from_view(get_arg(args, "localAudioPlayMode"));
|
||||
auto launch_session = make_launch_session(host_audio, args);
|
||||
|
||||
auto resolved_uuid = resolve_client_uuid(request);
|
||||
if (!resolved_uuid.empty()) {
|
||||
launch_session->unique_id = std::move(resolved_uuid);
|
||||
}
|
||||
|
||||
if (rtsp_stream::session_count() == 0) {
|
||||
// The display should be restored in case something fails as there are no other sessions.
|
||||
revert_display_configuration = true;
|
||||
|
|
@ -967,6 +1028,11 @@ namespace nvhttp {
|
|||
}
|
||||
const auto launch_session = make_launch_session(host_audio, args);
|
||||
|
||||
auto resolved_uuid = resolve_client_uuid(request);
|
||||
if (!resolved_uuid.empty()) {
|
||||
launch_session->unique_id = std::move(resolved_uuid);
|
||||
}
|
||||
|
||||
if (no_active_sessions) {
|
||||
// We want to prepare display only if there are no active sessions at
|
||||
// the moment. This should be done before probing encoders as it could
|
||||
|
|
|
|||
|
|
@ -193,6 +193,8 @@ namespace nvhttp {
|
|||
*/
|
||||
nlohmann::json get_all_clients();
|
||||
|
||||
std::string get_client_name(const std::string &uuid);
|
||||
|
||||
/**
|
||||
* @brief Remove all paired clients.
|
||||
* @examples
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
* @brief Definitions for wlgrab capture.
|
||||
*/
|
||||
// standard includes
|
||||
#include <algorithm>
|
||||
#include <thread>
|
||||
|
||||
// local includes
|
||||
|
|
@ -57,6 +58,30 @@ namespace wl {
|
|||
|
||||
if (streamedMonitor >= 0 && streamedMonitor < interface.monitors.size()) {
|
||||
monitor = interface.monitors[streamedMonitor].get();
|
||||
} else {
|
||||
auto by_name = std::find_if(
|
||||
std::begin(interface.monitors),
|
||||
std::end(interface.monitors),
|
||||
[&display_name](const auto &candidate) {
|
||||
return candidate->name == display_name;
|
||||
});
|
||||
|
||||
if (by_name != std::end(interface.monitors)) {
|
||||
monitor = by_name->get();
|
||||
} else {
|
||||
auto by_description = std::find_if(
|
||||
std::begin(interface.monitors),
|
||||
std::end(interface.monitors),
|
||||
[&display_name](const auto &candidate) {
|
||||
return candidate->description == display_name;
|
||||
});
|
||||
|
||||
if (by_description != std::end(interface.monitors)) {
|
||||
monitor = by_description->get();
|
||||
} else {
|
||||
BOOST_LOG(warning) << "No Wayland monitor matched output_name='"sv << display_name << "'; falling back to monitor 0"sv;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -431,8 +456,12 @@ namespace platf {
|
|||
|
||||
BOOST_LOG(info) << "Monitor " << x << " is "sv << monitor->name << ": "sv << monitor->description;
|
||||
|
||||
display_names.emplace_back(std::to_string(x));
|
||||
}
|
||||
if (!monitor->name.empty()) {
|
||||
display_names.emplace_back(monitor->name);
|
||||
} else {
|
||||
display_names.emplace_back(std::to_string(x));
|
||||
}
|
||||
}
|
||||
|
||||
BOOST_LOG(info) << "--------- End of Wayland monitor list ---------"sv;
|
||||
|
||||
|
|
|
|||
274
src/stream.cpp
274
src/stream.cpp
|
|
@ -4,6 +4,7 @@
|
|||
*/
|
||||
|
||||
// standard includes
|
||||
#include <algorithm>
|
||||
#include <fstream>
|
||||
#include <future>
|
||||
#include <queue>
|
||||
|
|
@ -26,6 +27,7 @@ extern "C" {
|
|||
#include "input.h"
|
||||
#include "logging.h"
|
||||
#include "network.h"
|
||||
#include "nvhttp.h"
|
||||
#include "platform/common.h"
|
||||
#include "process.h"
|
||||
#include "stream.h"
|
||||
|
|
@ -49,6 +51,12 @@ extern "C" {
|
|||
#define IDX_SET_MOTION_EVENT 13
|
||||
#define IDX_SET_RGB_LED 14
|
||||
#define IDX_SET_ADAPTIVE_TRIGGERS 15
|
||||
#define IDX_SET_INPUT_POLICY 16
|
||||
|
||||
#define INPUT_POLICY_REASON_STREAM_START 0
|
||||
#define INPUT_POLICY_REASON_USER_TOGGLE 1
|
||||
#define INPUT_POLICY_REASON_HOST_ACK 2
|
||||
#define INPUT_POLICY_REASON_HOST_OVERRIDE 3
|
||||
|
||||
static const short packetTypes[] = {
|
||||
0x0305, // Start A
|
||||
|
|
@ -67,6 +75,7 @@ static const short packetTypes[] = {
|
|||
0x5501, // Set motion event (Sunshine protocol extension)
|
||||
0x5502, // Set RGB LED (Sunshine protocol extension)
|
||||
0x5503, // Set Adaptive triggers (Sunshine protocol extension)
|
||||
0x5504,
|
||||
};
|
||||
|
||||
namespace asio = boost::asio;
|
||||
|
|
@ -203,6 +212,13 @@ namespace stream {
|
|||
std::uint8_t right[DS_EFFECT_PAYLOAD_SIZE];
|
||||
};
|
||||
|
||||
struct control_set_input_policy_t {
|
||||
std::uint8_t allow_keyboard;
|
||||
std::uint8_t allow_mouse;
|
||||
std::uint8_t allow_gamepad;
|
||||
std::uint8_t reason;
|
||||
};
|
||||
|
||||
struct control_hdr_mode_t {
|
||||
control_header_v2 header;
|
||||
|
||||
|
|
@ -340,6 +356,34 @@ namespace stream {
|
|||
control_server_t control_server;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Per-session input policy (which remote input types are allowed).
|
||||
*/
|
||||
struct session_input_policy_t {
|
||||
std::atomic<bool> allow_gamepad {true};
|
||||
std::atomic<bool> allow_keyboard {false};
|
||||
std::atomic<bool> allow_mouse {false};
|
||||
std::atomic<bool> is_owner_session {false};
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Per-session input activity timestamps (monotonic ms).
|
||||
*/
|
||||
struct session_input_activity_t {
|
||||
std::atomic<uint64_t> last_keyboard_ms {0};
|
||||
std::atomic<uint64_t> last_mouse_ms {0};
|
||||
std::atomic<uint64_t> last_gamepad_ms {0};
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Return current monotonic time in milliseconds.
|
||||
*/
|
||||
static uint64_t now_monotonic_ms() {
|
||||
return std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||
std::chrono::steady_clock::now().time_since_epoch()
|
||||
).count();
|
||||
}
|
||||
|
||||
struct session_t {
|
||||
config_t config;
|
||||
|
||||
|
|
@ -347,6 +391,12 @@ namespace stream {
|
|||
|
||||
std::shared_ptr<input::input_t> input;
|
||||
|
||||
// Parsec-style per-session state
|
||||
std::string client_unique_id;
|
||||
std::string client_name;
|
||||
session_input_policy_t input_policy;
|
||||
session_input_activity_t input_activity;
|
||||
|
||||
std::thread audioThread;
|
||||
std::thread videoThread;
|
||||
|
||||
|
|
@ -412,6 +462,55 @@ namespace stream {
|
|||
std::atomic<session::state_e> state;
|
||||
};
|
||||
|
||||
template<std::size_t max_payload_size>
|
||||
static inline std::string_view encode_control(session_t *session, const std::string_view &plaintext, std::array<std::uint8_t, max_payload_size> &tagged_cipher);
|
||||
|
||||
static void apply_session_input_policy(session_t *session, bool allow_keyboard, bool allow_mouse, bool allow_gamepad, uint8_t reason) {
|
||||
auto effective_keyboard = config::input.keyboard && allow_keyboard;
|
||||
auto effective_mouse = config::input.mouse && allow_mouse;
|
||||
auto effective_gamepad = config::input.controller && allow_gamepad;
|
||||
|
||||
session->input_policy.allow_keyboard.store(effective_keyboard, std::memory_order_relaxed);
|
||||
session->input_policy.allow_mouse.store(effective_mouse, std::memory_order_relaxed);
|
||||
session->input_policy.allow_gamepad.store(effective_gamepad, std::memory_order_relaxed);
|
||||
|
||||
if (session->input) {
|
||||
input::set_policy(session->input, effective_keyboard, effective_mouse, effective_gamepad);
|
||||
}
|
||||
|
||||
BOOST_LOG(debug)
|
||||
<< "Session input policy updated [reason="sv << (int) reason << "] kb="sv << effective_keyboard
|
||||
<< " mouse="sv << effective_mouse << " gamepad="sv << effective_gamepad;
|
||||
}
|
||||
|
||||
static int send_session_input_policy(session_t *session, uint8_t reason) {
|
||||
if (!session->control.peer) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
struct control_set_input_policy_msg_t {
|
||||
control_header_v2 header;
|
||||
control_set_input_policy_t payload;
|
||||
} plaintext;
|
||||
|
||||
plaintext.header.type = packetTypes[IDX_SET_INPUT_POLICY];
|
||||
plaintext.header.payloadLength = sizeof(plaintext) - sizeof(control_header_v2);
|
||||
plaintext.payload.allow_keyboard = session->input_policy.allow_keyboard.load(std::memory_order_relaxed) ? 1 : 0;
|
||||
plaintext.payload.allow_mouse = session->input_policy.allow_mouse.load(std::memory_order_relaxed) ? 1 : 0;
|
||||
plaintext.payload.allow_gamepad = session->input_policy.allow_gamepad.load(std::memory_order_relaxed) ? 1 : 0;
|
||||
plaintext.payload.reason = reason;
|
||||
|
||||
std::array<std::uint8_t, sizeof(control_encrypted_t) + crypto::cipher::round_to_pkcs7_padded(sizeof(plaintext)) + crypto::cipher::tag_size>
|
||||
encrypted_payload;
|
||||
|
||||
auto payload = encode_control(session, util::view(plaintext), encrypted_payload);
|
||||
if (session->broadcast_ref->control_server.send(payload, session->control.peer)) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* First part of cipher must be struct of type control_encrypted_t
|
||||
*
|
||||
|
|
@ -930,6 +1029,10 @@ namespace stream {
|
|||
|
||||
server->map(packetTypes[IDX_START_B], [&](session_t *session, const std::string_view &payload) {
|
||||
BOOST_LOG(debug) << "type [IDX_START_B]"sv;
|
||||
|
||||
if (send_session_input_policy(session, INPUT_POLICY_REASON_STREAM_START)) {
|
||||
BOOST_LOG(warning) << "Unable to send initial session input policy"sv;
|
||||
}
|
||||
});
|
||||
|
||||
server->map(packetTypes[IDX_LOSS_STATS], [&](session_t *session, const std::string_view &payload) {
|
||||
|
|
@ -1059,6 +1162,34 @@ namespace stream {
|
|||
}
|
||||
});
|
||||
|
||||
server->map(packetTypes[IDX_SET_INPUT_POLICY], [](session_t *session, const std::string_view &payload) {
|
||||
BOOST_LOG(debug) << "type [IDX_SET_INPUT_POLICY]"sv;
|
||||
|
||||
if (payload.size() < sizeof(control_set_input_policy_t)) {
|
||||
BOOST_LOG(warning) << "Dropping runt input policy payload"sv;
|
||||
return;
|
||||
}
|
||||
|
||||
auto *policy = (const control_set_input_policy_t *) payload.data();
|
||||
bool is_owner_session = session->input_policy.is_owner_session.load(std::memory_order_relaxed);
|
||||
if (!is_owner_session) {
|
||||
BOOST_LOG(info)
|
||||
<< "Ignoring client input policy update from non-owner session [id="sv << session->launch_session_id
|
||||
<< ", client="sv << session->client_unique_id << "]"sv;
|
||||
} else {
|
||||
apply_session_input_policy(
|
||||
session,
|
||||
policy->allow_keyboard != 0,
|
||||
policy->allow_mouse != 0,
|
||||
policy->allow_gamepad != 0,
|
||||
policy->reason);
|
||||
}
|
||||
|
||||
if (send_session_input_policy(session, INPUT_POLICY_REASON_HOST_ACK)) {
|
||||
BOOST_LOG(warning) << "Unable to send input policy acknowledgment"sv;
|
||||
}
|
||||
});
|
||||
|
||||
// This thread handles latency-sensitive control messages
|
||||
platf::set_thread_name("stream::controlBroadcast");
|
||||
platf::adjust_thread_priority(platf::thread_priority_e::critical);
|
||||
|
|
@ -1787,13 +1918,13 @@ namespace stream {
|
|||
audio_packets.reset();
|
||||
|
||||
BOOST_LOG(debug) << "Waiting for main listening thread to end..."sv;
|
||||
ctx.recv_thread.join();
|
||||
if (ctx.recv_thread.joinable()) ctx.recv_thread.join();
|
||||
BOOST_LOG(debug) << "Waiting for main video thread to end..."sv;
|
||||
ctx.video_thread.join();
|
||||
if (ctx.video_thread.joinable()) ctx.video_thread.join();
|
||||
BOOST_LOG(debug) << "Waiting for main audio thread to end..."sv;
|
||||
ctx.audio_thread.join();
|
||||
if (ctx.audio_thread.joinable()) ctx.audio_thread.join();
|
||||
BOOST_LOG(debug) << "Waiting for main control thread to end..."sv;
|
||||
ctx.control_thread.join();
|
||||
if (ctx.control_thread.joinable()) ctx.control_thread.join();
|
||||
BOOST_LOG(debug) << "All broadcasting threads ended"sv;
|
||||
|
||||
broadcast_shutdown_event->reset();
|
||||
|
|
@ -1896,6 +2027,114 @@ namespace stream {
|
|||
audio::capture(session->mail, session->config.audio, session);
|
||||
}
|
||||
|
||||
namespace session {
|
||||
extern std::atomic_uint running_sessions;
|
||||
}
|
||||
|
||||
nlohmann::json get_active_sessions_info() {
|
||||
auto result = nlohmann::json::array();
|
||||
|
||||
if (session::running_sessions.load(std::memory_order_relaxed) == 0) {
|
||||
return result;
|
||||
}
|
||||
|
||||
auto ref = broadcast.ref();
|
||||
if (!ref) {
|
||||
return result;
|
||||
}
|
||||
|
||||
auto now = now_monotonic_ms();
|
||||
|
||||
auto lg = ref->control_server._sessions.lock();
|
||||
for (auto *session : *ref->control_server._sessions) {
|
||||
if (session->state.load(std::memory_order_relaxed) != session::state_e::RUNNING) {
|
||||
continue;
|
||||
}
|
||||
|
||||
auto last_kb = session->input_activity.last_keyboard_ms.load(std::memory_order_relaxed);
|
||||
auto last_mouse = session->input_activity.last_mouse_ms.load(std::memory_order_relaxed);
|
||||
auto last_gp = session->input_activity.last_gamepad_ms.load(std::memory_order_relaxed);
|
||||
|
||||
nlohmann::json entry;
|
||||
entry["session_id"] = session->launch_session_id;
|
||||
entry["client_uuid"] = session->client_unique_id;
|
||||
entry["client_name"] = session->client_name;
|
||||
entry["is_owner_session"] = session->input_policy.is_owner_session.load(std::memory_order_relaxed);
|
||||
|
||||
entry["policy"] = {
|
||||
{"allow_keyboard", session->input_policy.allow_keyboard.load(std::memory_order_relaxed)},
|
||||
{"allow_mouse", session->input_policy.allow_mouse.load(std::memory_order_relaxed)},
|
||||
{"allow_gamepad", session->input_policy.allow_gamepad.load(std::memory_order_relaxed)},
|
||||
};
|
||||
|
||||
auto kb_ago = last_kb > 0 ? now - last_kb : UINT64_MAX;
|
||||
auto mouse_ago = last_mouse > 0 ? now - last_mouse : UINT64_MAX;
|
||||
auto gp_ago = last_gp > 0 ? now - last_gp : UINT64_MAX;
|
||||
|
||||
entry["activity"] = {
|
||||
{"keyboard_active", kb_ago <= KB_ACTIVE_WINDOW_MS},
|
||||
{"mouse_active", mouse_ago <= MOUSE_ACTIVE_WINDOW_MS},
|
||||
{"gamepad_active", gp_ago <= GAMEPAD_ACTIVE_WINDOW_MS},
|
||||
{"last_keyboard_ms_ago", last_kb > 0 ? (int64_t) kb_ago : (int64_t) -1},
|
||||
{"last_mouse_ms_ago", last_mouse > 0 ? (int64_t) mouse_ago : (int64_t) -1},
|
||||
{"last_gamepad_ms_ago", last_gp > 0 ? (int64_t) gp_ago : (int64_t) -1},
|
||||
};
|
||||
|
||||
result.push_back(std::move(entry));
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
|
|
@ -1964,6 +2203,19 @@ namespace stream {
|
|||
int start(session_t &session, const std::string &addr_string) {
|
||||
session.input = input::alloc(session.mail);
|
||||
|
||||
input::set_activity_pointers(
|
||||
session.input,
|
||||
&session.input_activity.last_keyboard_ms,
|
||||
&session.input_activity.last_mouse_ms,
|
||||
&session.input_activity.last_gamepad_ms);
|
||||
|
||||
apply_session_input_policy(
|
||||
&session,
|
||||
session.input_policy.allow_keyboard.load(std::memory_order_relaxed),
|
||||
session.input_policy.allow_mouse.load(std::memory_order_relaxed),
|
||||
session.input_policy.allow_gamepad.load(std::memory_order_relaxed),
|
||||
0);
|
||||
|
||||
session.broadcast_ref = broadcast.ref();
|
||||
if (!session.broadcast_ref) {
|
||||
return -1;
|
||||
|
|
@ -2010,6 +2262,20 @@ namespace stream {
|
|||
|
||||
session->shutdown_event = mail->event<bool>(mail::shutdown);
|
||||
session->launch_session_id = launch_session.id;
|
||||
session->client_unique_id = launch_session.unique_id;
|
||||
session->client_name = nvhttp::get_client_name(launch_session.unique_id);
|
||||
|
||||
auto &owner_uuids = config::input.owner_client_uuids;
|
||||
bool is_owner = std::find(owner_uuids.begin(), owner_uuids.end(), launch_session.unique_id) != owner_uuids.end();
|
||||
|
||||
session->input_policy.is_owner_session.store(is_owner, std::memory_order_relaxed);
|
||||
session->input_policy.allow_gamepad.store(true, std::memory_order_relaxed);
|
||||
session->input_policy.allow_keyboard.store(is_owner, std::memory_order_relaxed);
|
||||
session->input_policy.allow_mouse.store(is_owner, std::memory_order_relaxed);
|
||||
|
||||
if (is_owner) {
|
||||
BOOST_LOG(info) << "Owner session detected for client: "sv << launch_session.unique_id;
|
||||
}
|
||||
|
||||
session->config = config;
|
||||
|
||||
|
|
|
|||
19
src/stream.h
19
src/stream.h
|
|
@ -5,10 +5,14 @@
|
|||
#pragma once
|
||||
|
||||
// standard includes
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
// lib includes
|
||||
#include <boost/asio.hpp>
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
// local includes
|
||||
#include "audio.h"
|
||||
|
|
@ -20,8 +24,23 @@ namespace stream {
|
|||
constexpr auto CONTROL_PORT = 10;
|
||||
constexpr auto AUDIO_STREAM_PORT = 11;
|
||||
|
||||
constexpr uint64_t KB_ACTIVE_WINDOW_MS = 1500;
|
||||
constexpr uint64_t MOUSE_ACTIVE_WINDOW_MS = 1500;
|
||||
constexpr uint64_t GAMEPAD_ACTIVE_WINDOW_MS = 1500;
|
||||
|
||||
struct session_t;
|
||||
|
||||
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;
|
||||
|
|
|
|||
|
|
@ -203,14 +203,15 @@
|
|||
"touchpad_as_ds4": "enabled",
|
||||
"ds5_inputtino_randomize_mac": "enabled",
|
||||
"back_button_timeout": -1,
|
||||
"keyboard": "enabled",
|
||||
"keyboard": "disabled",
|
||||
"key_repeat_delay": 500,
|
||||
"key_repeat_frequency": 24.9,
|
||||
"always_send_scancodes": "enabled",
|
||||
"key_rightalt_to_key_win": "disabled",
|
||||
"mouse": "enabled",
|
||||
"mouse": "disabled",
|
||||
"high_resolution_scrolling": "enabled",
|
||||
"native_pen_touch": "enabled",
|
||||
"owner_client_uuids": "",
|
||||
"keybindings": "[0x10,0xA0,0x11,0xA2,0x12,0xA4]", // todo: add this to UI
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -118,7 +118,7 @@ const config = ref(props.config)
|
|||
id="keyboard"
|
||||
locale-prefix="config"
|
||||
v-model="config.keyboard"
|
||||
default="true"
|
||||
default="false"
|
||||
></Checkbox>
|
||||
|
||||
<!-- Key Repeat Delay-->
|
||||
|
|
@ -161,7 +161,7 @@ const config = ref(props.config)
|
|||
id="mouse"
|
||||
locale-prefix="config"
|
||||
v-model="config.mouse"
|
||||
default="true"
|
||||
default="false"
|
||||
></Checkbox>
|
||||
|
||||
<!-- High resolution scrolling support -->
|
||||
|
|
@ -181,6 +181,15 @@ const config = ref(props.config)
|
|||
v-model="config.native_pen_touch"
|
||||
default="true"
|
||||
></Checkbox>
|
||||
|
||||
<hr>
|
||||
<div class="mb-3">
|
||||
<label for="owner_client_uuids" class="form-label">Owner Client UUIDs</label>
|
||||
<input type="text" class="form-control" id="owner_client_uuids"
|
||||
placeholder=""
|
||||
v-model="config.owner_client_uuids" />
|
||||
<div class="form-text">Comma-separated UUIDs of owner clients. Owner sessions start with keyboard/mouse enabled. Find UUIDs in the Troubleshooting page under paired clients.</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
|
|||
|
|
@ -1202,6 +1202,28 @@ p {
|
|||
margin: 0;
|
||||
}
|
||||
|
||||
.activity-dot {
|
||||
display: inline-block;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
border-radius: 50%;
|
||||
transition: background-color 0.2s, border-color 0.2s, box-shadow 0.2s;
|
||||
flex-shrink: 0;
|
||||
vertical-align: middle;
|
||||
margin-right: 3px;
|
||||
}
|
||||
|
||||
.dot-green {
|
||||
background-color: #22c55e;
|
||||
box-shadow: 0 0 6px #22c55e;
|
||||
border: 1px solid #16a34a;
|
||||
}
|
||||
|
||||
.dot-gray {
|
||||
background-color: #6b7280;
|
||||
border: 1px solid #d1d5db;
|
||||
}
|
||||
|
||||
/* Log level highlighting */
|
||||
.log-line-info {
|
||||
color: var(--color-text-base);
|
||||
|
|
|
|||
|
|
@ -135,10 +135,21 @@
|
|||
</div>
|
||||
<ul class="list-group list-group-flush" v-if="clients && clients.length > 0">
|
||||
<li v-for="client in clients" :key="client.uuid" class="list-group-item d-flex align-items-center">
|
||||
<div class="flex-grow-1">{{ client.name !== "" ? client.name : $t('troubleshooting.unpair_single_unknown') }}</div>
|
||||
<button class="btn btn-danger ms-auto" @click="unpairSingle(client.uuid)">
|
||||
<trash-2 :size="18" class="icon"></trash-2>
|
||||
</button>
|
||||
<div class="flex-grow-1">
|
||||
<div>{{ client.name !== "" ? client.name : $t('troubleshooting.unpair_single_unknown') }}</div>
|
||||
<div class="text-muted" style="font-size: 0.8em">
|
||||
<span class="font-monospace">{{ client.uuid }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex align-items-center gap-2 ms-auto">
|
||||
<button class="btn btn-outline-secondary btn-sm" @click="copyClientUuid(client.uuid)">
|
||||
<copy :size="16" class="icon"></copy>
|
||||
Copy UUID
|
||||
</button>
|
||||
<button class="btn btn-danger" @click="unpairSingle(client.uuid)">
|
||||
<trash-2 :size="18" class="icon"></trash-2>
|
||||
</button>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
<ul v-else class="list-group list-group-flush">
|
||||
|
|
@ -147,6 +158,84 @@
|
|||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<!-- Live Input Status -->
|
||||
<div class="card my-4">
|
||||
<div class="card-body">
|
||||
<h2 id="input_status">Live Input Status</h2>
|
||||
<p>Per-session input activity and policy. Use the checkboxes to override keyboard, mouse, and gamepad permissions for each active client.</p>
|
||||
</div>
|
||||
<div v-if="activeSessions.length === 0" class="card-body pt-0">
|
||||
<em>No active streaming sessions.</em>
|
||||
</div>
|
||||
<ul class="list-group list-group-flush" v-else>
|
||||
<li v-for="s in activeSessions" :key="s.session_id" class="list-group-item">
|
||||
<div class="d-flex align-items-center justify-content-between">
|
||||
<div>
|
||||
<strong>{{ s.client_name || s.client_uuid || 'Unknown' }}</strong>
|
||||
<span v-if="s.is_owner_session" class="badge bg-primary ms-2">Owner</span>
|
||||
</div>
|
||||
<div class="d-flex gap-3 align-items-center">
|
||||
<span class="d-flex align-items-center gap-1" title="Keyboard">
|
||||
<span :class="['activity-dot', s.activity.keyboard_active ? 'dot-green' : 'dot-gray']"></span>
|
||||
KB
|
||||
<span :class="['badge', s.policy.allow_keyboard ? 'bg-success' : 'bg-secondary']" style="font-size: 0.7em">
|
||||
{{ s.policy.allow_keyboard ? 'ON' : 'OFF' }}
|
||||
</span>
|
||||
</span>
|
||||
<span class="d-flex align-items-center gap-1" title="Mouse">
|
||||
<span :class="['activity-dot', s.activity.mouse_active ? 'dot-green' : 'dot-gray']"></span>
|
||||
Mouse
|
||||
<span :class="['badge', s.policy.allow_mouse ? 'bg-success' : 'bg-secondary']" style="font-size: 0.7em">
|
||||
{{ s.policy.allow_mouse ? 'ON' : 'OFF' }}
|
||||
</span>
|
||||
</span>
|
||||
<span class="d-flex align-items-center gap-1" title="Gamepad">
|
||||
<span :class="['activity-dot', s.activity.gamepad_active ? 'dot-green' : 'dot-gray']"></span>
|
||||
Gamepad
|
||||
<span :class="['badge', s.policy.allow_gamepad ? 'bg-success' : 'bg-secondary']" style="font-size: 0.7em">
|
||||
{{ s.policy.allow_gamepad ? 'ON' : 'OFF' }}
|
||||
</span>
|
||||
</span>
|
||||
</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>
|
||||
</ul>
|
||||
</div>
|
||||
<!-- Logs -->
|
||||
<div class="card my-4">
|
||||
<div class="card-body">
|
||||
|
|
@ -229,6 +318,7 @@
|
|||
},
|
||||
data() {
|
||||
return {
|
||||
activeSessions: [],
|
||||
clients: [],
|
||||
closeAppPressed: false,
|
||||
closeAppStatus: null,
|
||||
|
|
@ -237,6 +327,11 @@
|
|||
logs: 'Loading...',
|
||||
logFilter: null,
|
||||
logInterval: null,
|
||||
sessionInterval: null,
|
||||
sessionSocket: null,
|
||||
sessionReconnectTimer: null,
|
||||
sessionPolicyBusy: {},
|
||||
sessionPolicyError: {},
|
||||
restartPressed: false,
|
||||
showApplyMessage: false,
|
||||
platform: "",
|
||||
|
|
@ -389,13 +484,140 @@
|
|||
this.logInterval = setInterval(() => {
|
||||
this.refreshLogs();
|
||||
}, 5000);
|
||||
this.sessionInterval = setInterval(() => {
|
||||
this.refreshActiveSessions();
|
||||
}, 5000);
|
||||
this.refreshLogs();
|
||||
this.refreshClients();
|
||||
this.refreshActiveSessions();
|
||||
this.connectActiveSessionsSocket();
|
||||
},
|
||||
beforeDestroy() {
|
||||
clearInterval(this.logInterval);
|
||||
clearInterval(this.sessionInterval);
|
||||
clearTimeout(this.sessionReconnectTimer);
|
||||
if (this.sessionSocket) {
|
||||
this.sessionSocket.onclose = null;
|
||||
this.sessionSocket.close();
|
||||
this.sessionSocket = null;
|
||||
}
|
||||
},
|
||||
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())
|
||||
.then((r) => {
|
||||
if (!r || r.status !== true || !r.token) {
|
||||
throw new Error("No websocket token");
|
||||
}
|
||||
|
||||
const protocol = window.location.protocol === "https:" ? "wss" : "ws";
|
||||
const wsUrl = `${protocol}://${window.location.host}/api/sessions/active/ws?token=${encodeURIComponent(r.token)}`;
|
||||
|
||||
this.sessionSocket = new WebSocket(wsUrl);
|
||||
|
||||
this.sessionSocket.onmessage = (event) => {
|
||||
try {
|
||||
const payload = JSON.parse(event.data);
|
||||
if (payload && payload.status === true && payload.sessions) {
|
||||
this.activeSessions = payload.sessions;
|
||||
}
|
||||
} catch (_e) {
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
this.sessionSocket.onclose = () => {
|
||||
this.sessionSocket = null;
|
||||
clearTimeout(this.sessionReconnectTimer);
|
||||
this.sessionReconnectTimer = setTimeout(() => {
|
||||
this.connectActiveSessionsSocket();
|
||||
}, 1000);
|
||||
};
|
||||
|
||||
this.sessionSocket.onerror = () => {
|
||||
if (this.sessionSocket) {
|
||||
this.sessionSocket.close();
|
||||
}
|
||||
};
|
||||
})
|
||||
.catch(() => {
|
||||
clearTimeout(this.sessionReconnectTimer);
|
||||
this.sessionReconnectTimer = setTimeout(() => {
|
||||
this.connectActiveSessionsSocket();
|
||||
}, 1000);
|
||||
});
|
||||
},
|
||||
refreshActiveSessions() {
|
||||
if (this.sessionSocket && this.sessionSocket.readyState === WebSocket.OPEN) {
|
||||
return;
|
||||
}
|
||||
|
||||
fetch("./api/sessions/active")
|
||||
.then((r) => r.json())
|
||||
.then((r) => {
|
||||
if (r.status === true && r.sessions) {
|
||||
this.activeSessions = r.sessions;
|
||||
} else {
|
||||
this.activeSessions = [];
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
this.activeSessions = [];
|
||||
});
|
||||
},
|
||||
refreshLogs() {
|
||||
fetch("./api/logs",)
|
||||
.then((r) => r.text())
|
||||
|
|
@ -466,6 +688,15 @@
|
|||
clickedApplyBanner() {
|
||||
this.showApplyMessage = false;
|
||||
},
|
||||
copyClientUuid(uuid) {
|
||||
if (!uuid) {
|
||||
return;
|
||||
}
|
||||
|
||||
navigator.clipboard.writeText(uuid).catch(() => {
|
||||
window.prompt("Copy client UUID", uuid);
|
||||
});
|
||||
},
|
||||
copyLogs() {
|
||||
// Copy the filtered view if a filter is active.
|
||||
navigator.clipboard.writeText(this.actualLogs);
|
||||
|
|
|
|||
2
third-party/Simple-Web-Server
vendored
2
third-party/Simple-Web-Server
vendored
|
|
@ -1 +1 @@
|
|||
Subproject commit 546895a93a29062bb178367b46c7afb72da9881e
|
||||
Subproject commit 99c1f621ebd8d119c5d2dc3a88ecf255058acec0
|
||||
Loading…
Add table
Add a link
Reference in a new issue