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); },