From 1ae7157bba2fd4990ce59fce5d02c996d2a545d8 Mon Sep 17 00:00:00 2001 From: Yukino Song Date: Sat, 28 Sep 2024 03:31:50 +0800 Subject: [PATCH] Add Windows clipboard support --- src/confighttp.cpp | 5 +- src/nvhttp.cpp | 115 ++++++++++++++++++++++++-- src/platform/common.h | 6 ++ src/platform/linux/misc.cpp | 12 +++ src/platform/macos/misc.mm | 12 +++ src/platform/windows/misc.cpp | 104 +++++++++++++++++++++++ src/platform/windows/utils.cpp | 57 ++++++++++--- src/platform/windows/utils.h | 3 + src_assets/common/assets/web/pin.html | 12 ++- 9 files changed, 304 insertions(+), 22 deletions(-) diff --git a/src/confighttp.cpp b/src/confighttp.cpp index 682d544a..a2e51f44 100644 --- a/src/confighttp.cpp +++ b/src/confighttp.cpp @@ -182,10 +182,7 @@ namespace confighttp { std::ostringstream data; pt::write_xml(data, tree); - response->write(data.str()); - - *response << "HTTP/1.1 404 NOT FOUND\r\n" - << data.str(); + response->write(SimpleWeb::StatusCode::client_error_not_found, data.str()); } void diff --git a/src/nvhttp.cpp b/src/nvhttp.cpp index eb1ce401..7782d81c 100644 --- a/src/nvhttp.cpp +++ b/src/nvhttp.cpp @@ -592,12 +592,7 @@ namespace nvhttp { std::ostringstream data; pt::write_xml(data, tree); - response->write(data.str()); - - *response - << "HTTP/1.1 404 NOT FOUND\r\n" - << data.str(); - + response->write(SimpleWeb::StatusCode::client_error_not_found, data.str()); response->close_connection_after_response = true; } @@ -1268,6 +1263,112 @@ namespace nvhttp { response->close_connection_after_response = true; } + void + getClipboard(resp_https_t response, req_https_t request) { + print_req(request); + + auto named_cert_p = get_verified_cert(request); + + if ( + !(named_cert_p->perm & PERM::_allow_view) + || !(named_cert_p->perm & PERM::clipboard_read) + ) { + BOOST_LOG(debug) << "Permission Read Clipboard denied for [" << named_cert_p->name << "] (" << (uint32_t)named_cert_p->perm << ")"; + + response->write(SimpleWeb::StatusCode::client_error_unauthorized); + response->close_connection_after_response = true; + return; + } + + auto args = request->parse_query_string(); + auto clipboard_type = get_arg(args, "type"); + if (clipboard_type != "text"sv) { + BOOST_LOG(debug) << "Clipboard type [" << clipboard_type << "] is not supported!"; + + response->write(SimpleWeb::StatusCode::client_error_bad_request); + response->close_connection_after_response = true; + return; + } + + std::list connected_uuids = rtsp_stream::get_all_session_uuids(); + + bool found = !connected_uuids.empty(); + + if (found) { + found = (std::find(connected_uuids.begin(), connected_uuids.end(), named_cert_p->uuid) != connected_uuids.end()); + } + + if (!found) { + BOOST_LOG(debug) << "Client ["<< named_cert_p->name << "] trying to get clipboard is not connected to a stream"; + + response->write(SimpleWeb::StatusCode::client_error_forbidden); + response->close_connection_after_response = true; + return; + } + + std::string content = platf::get_clipboard(); + response->write(content); + return; + } + + void + setClipboard(resp_https_t response, req_https_t request) { + print_req(request); + + auto named_cert_p = get_verified_cert(request); + + if ( + !(named_cert_p->perm & PERM::_allow_view) + || !(named_cert_p->perm & PERM::clipboard_set) + ) { + BOOST_LOG(debug) << "Permission Write Clipboard denied for [" << named_cert_p->name << "] (" << (uint32_t)named_cert_p->perm << ")"; + + response->write(SimpleWeb::StatusCode::client_error_unauthorized); + response->close_connection_after_response = true; + return; + } + + auto args = request->parse_query_string(); + auto clipboard_type = get_arg(args, "type"); + if (clipboard_type != "text"sv) { + BOOST_LOG(debug) << "Clipboard type [" << clipboard_type << "] is not supported!"; + + response->write(SimpleWeb::StatusCode::client_error_bad_request); + response->close_connection_after_response = true; + return; + } + + std::list connected_uuids = rtsp_stream::get_all_session_uuids(); + + bool found = !connected_uuids.empty(); + + if (found) { + found = (std::find(connected_uuids.begin(), connected_uuids.end(), named_cert_p->uuid) != connected_uuids.end()); + } + + if (!found) { + BOOST_LOG(debug) << "Client ["<< named_cert_p->name << "] trying to set clipboard is not connected to a stream"; + + response->write(SimpleWeb::StatusCode::client_error_forbidden); + response->close_connection_after_response = true; + return; + } + + std::string content = request->content.string(); + + bool success = platf::set_clipboard(content); + + if (!success) { + BOOST_LOG(debug) << "Setting clipboard failed!"; + + response->write(SimpleWeb::StatusCode::server_error_internal_server_error); + response->close_connection_after_response = true; + } + + response->write(); + return; + } + void start() { auto shutdown_event = mail::man->event(mail::shutdown); @@ -1351,6 +1452,8 @@ namespace nvhttp { https_server.resource["^/launch$"]["GET"] = [&host_audio](auto resp, auto req) { launch(host_audio, resp, req); }; https_server.resource["^/resume$"]["GET"] = [&host_audio](auto resp, auto req) { resume(host_audio, resp, req); }; https_server.resource["^/cancel$"]["GET"] = cancel; + https_server.resource["^/actions/clipboard$"]["GET"] = getClipboard; + https_server.resource["^/actions/clipboard$"]["POST"] = setClipboard; https_server.config.reuse_address = true; https_server.config.address = net::af_to_any_address_string(address_family); diff --git a/src/platform/common.h b/src/platform/common.h index e8051fc2..a306b5e7 100644 --- a/src/platform/common.h +++ b/src/platform/common.h @@ -895,4 +895,10 @@ namespace platf { std::unique_ptr create_high_precision_timer(); + std::string + get_clipboard(); + + bool + set_clipboard(const std::string& content); + } // namespace platf diff --git a/src/platform/linux/misc.cpp b/src/platform/linux/misc.cpp index c274783b..c93b5aff 100644 --- a/src/platform/linux/misc.cpp +++ b/src/platform/linux/misc.cpp @@ -1088,4 +1088,16 @@ get_local_ip_for_gateway() { create_high_precision_timer() { return std::make_unique(); } + + std::string + get_clipboard() { + // Placeholder + return ""; + } + + bool + set_clipboard(const std::string& content) { + // Placeholder + return false; + } } // namespace platf diff --git a/src/platform/macos/misc.mm b/src/platform/macos/misc.mm index cd07c830..499b6bd6 100644 --- a/src/platform/macos/misc.mm +++ b/src/platform/macos/misc.mm @@ -546,6 +546,18 @@ operator bool() override { create_high_precision_timer() { return std::make_unique(); } + + std::string + get_clipboard() { + // Placeholder + return ""; + } + + bool + set_clipboard(const std::string& content) { + // Placeholder + return false; + } } // namespace platf namespace dyn { diff --git a/src/platform/windows/misc.cpp b/src/platform/windows/misc.cpp index 03c3c0e7..9097a6bb 100644 --- a/src/platform/windows/misc.cpp +++ b/src/platform/windows/misc.cpp @@ -35,6 +35,7 @@ #include #include "misc.h" +#include "utils.h" #include "src/entry_handler.h" #include "src/globals.h" @@ -95,6 +96,10 @@ namespace { namespace bp = boost::process; +static std::string ensureCrLf(const std::string& utf8Str); +static std::wstring getClipboardData(); +static int setClipboardData(const std::string& acpStr); + using namespace std::literals; namespace platf { using adapteraddrs_t = util::c_ptr; @@ -1941,4 +1946,103 @@ namespace platf { create_high_precision_timer() { return std::make_unique(); } + + std::string + get_clipboard() { + std::string currentClipboard = to_utf8(getClipboardData()); + return currentClipboard; + } + + bool + set_clipboard(const std::string& content) { + std::string cpContent = convertUtf8ToCurrentCodepage(ensureCrLf(content)); + return !setClipboardData(cpContent); + } } // namespace platf + +static std::string ensureCrLf(const std::string& utf8Str) { + std::string result; + result.reserve(utf8Str.size() + utf8Str.length() / 2); // Reserve extra space + + for (size_t i = 0; i < utf8Str.size(); ++i) { + if (utf8Str[i] == '\n' && (i == 0 || utf8Str[i - 1] != '\r')) { + result += '\r'; // Add \r before \n if not present + } + result += utf8Str[i]; // Always add the current character + } + + return result; +} + +static std::wstring getClipboardData() { + if (!OpenClipboard(nullptr)) { + BOOST_LOG(warning) << "Failed to open clipboard."; + return L""; + } + + HANDLE hData = GetClipboardData(CF_UNICODETEXT); + if (hData == nullptr) { + BOOST_LOG(warning) << "No text data in clipboard or failed to get data."; + CloseClipboard(); + return L""; + } + + wchar_t* pszText = static_cast(GlobalLock(hData)); + if (pszText == nullptr) { + BOOST_LOG(warning) << "Failed to lock clipboard data."; + CloseClipboard(); + return L""; + } + + std::wstring ret = pszText; + + GlobalUnlock(hData); + CloseClipboard(); + + return ret; +} + +static int setClipboardData(const std::string& acpStr) { + if (!OpenClipboard(nullptr)) { + BOOST_LOG(warning) << "Failed to open clipboard."; + return 1; + } + + if (!EmptyClipboard()) { + BOOST_LOG(warning) << "Failed to empty clipboard."; + CloseClipboard(); + return 1; + } + + // Allocate global memory for the clipboard text + HGLOBAL hGlobal = GlobalAlloc(GMEM_MOVEABLE, acpStr.size() + 1); + if (hGlobal == nullptr) { + BOOST_LOG(warning) << "Failed to allocate global memory."; + CloseClipboard(); + return 1; + } + + // Lock the global memory and copy the text + char* pGlobal = static_cast(GlobalLock(hGlobal)); + if (pGlobal == nullptr) { + BOOST_LOG(warning) << "Failed to lock global memory."; + GlobalFree(hGlobal); + CloseClipboard(); + return 1; + } + + memcpy(pGlobal, acpStr.c_str(), acpStr.size() + 1); + GlobalUnlock(hGlobal); + + // Set the clipboard data + if (SetClipboardData(CF_TEXT, hGlobal) == nullptr) { + BOOST_LOG(warning) << "Failed to set clipboard data."; + GlobalFree(hGlobal); + CloseClipboard(); + return 1; + } + + CloseClipboard(); + + return 0; +} diff --git a/src/platform/windows/utils.cpp b/src/platform/windows/utils.cpp index 1ed2bad4..150c72c1 100644 --- a/src/platform/windows/utils.cpp +++ b/src/platform/windows/utils.cpp @@ -1,28 +1,65 @@ #include "utils.h" +std::wstring acpToUtf16(const std::string& origStr) { + auto acp = GetACP(); + + int utf16Len = MultiByteToWideChar(acp, 0, origStr.c_str(), origStr.size(), NULL, 0); + if (utf16Len == 0) { + return L""; + } + + std::wstring utf16Str(utf16Len, L'\0'); + MultiByteToWideChar(acp, 0, origStr.c_str(), origStr.size(), &utf16Str[0], utf16Len); + + return utf16Str; +} + +std::string utf16toAcp(const std::wstring& utf16Str) { + auto acp = GetACP(); + + int codepageLen = WideCharToMultiByte(acp, 0, utf16Str.c_str(), utf16Str.size(), NULL, 0, NULL, NULL); + if (codepageLen == 0) { + return ""; + } + + std::string codepageStr(codepageLen, '\0'); + WideCharToMultiByte(acp, 0, utf16Str.c_str(), utf16Str.size(), &codepageStr[0], codepageLen, NULL, NULL); + + return codepageStr; +} + std::string convertUtf8ToCurrentCodepage(const std::string& utf8Str) { if (GetACP() == CP_UTF8) { return std::string(utf8Str); } - // Step 1: Convert UTF-8 to UTF-16 - int utf16Len = MultiByteToWideChar(CP_UTF8, 0, utf8Str.c_str(), -1, NULL, 0); + + int utf16Len = MultiByteToWideChar(CP_UTF8, 0, utf8Str.c_str(), utf8Str.size(), NULL, 0); if (utf16Len == 0) { return std::string(utf8Str); } std::wstring utf16Str(utf16Len, L'\0'); - MultiByteToWideChar(CP_UTF8, 0, utf8Str.c_str(), -1, &utf16Str[0], utf16Len); + MultiByteToWideChar(CP_UTF8, 0, utf8Str.c_str(), utf8Str.size(), &utf16Str[0], utf16Len); - // Step 2: Convert UTF-16 to the current Windows codepage - int codepageLen = WideCharToMultiByte(GetACP(), 0, utf16Str.c_str(), -1, NULL, 0, NULL, NULL); - if (codepageLen == 0) { - return std::string(utf8Str); + return utf16toAcp(utf16Str); +} + +std::string convertCurrentCodepageToUtf8(const std::string& origStr) { + if (GetACP() == CP_UTF8) { + return std::string(origStr); } - std::string codepageStr(codepageLen, '\0'); - WideCharToMultiByte(GetACP(), 0, utf16Str.c_str(), -1, &codepageStr[0], codepageLen, NULL, NULL); + auto utf16Str = acpToUtf16(origStr); - return codepageStr; + int utf8Len = WideCharToMultiByte(CP_UTF8, 0, utf16Str.c_str(), utf16Str.size(), NULL, 0, NULL, NULL); + if (utf8Len == 0) { + return std::string(origStr); + } + + std::string utf8Str(utf8Len, '\0'); + WideCharToMultiByte(CP_UTF8, 0, utf16Str.c_str(), utf16Str.size(), &utf8Str[0], utf8Len, NULL, NULL); + + return utf8Str; } // Modified from https://github.com/FrogTheFrog/Sunshine/blob/b6f8573d35eff7c55da6965dfa317dc9722bd4ef/src/platform/windows/display_device/windows_utils.cpp diff --git a/src/platform/windows/utils.h b/src/platform/windows/utils.h index ee6f96b8..6f2be42a 100644 --- a/src/platform/windows/utils.h +++ b/src/platform/windows/utils.h @@ -9,7 +9,10 @@ #include "src/utility.h" #include "src/logging.h" +std::wstring acpToUtf16(const std::string& origStr); +std::string utf16toAcp(const std::wstring& utf16Str); std::string convertUtf8ToCurrentCodepage(const std::string& utf8Str); +std::string convertCurrentCodepageToUtf8(const std::string& currentStr); std::string get_error_string(LONG error_code); diff --git a/src_assets/common/assets/web/pin.html b/src_assets/common/assets/web/pin.html index 830c9679..455ef728 100644 --- a/src_assets/common/assets/web/pin.html +++ b/src_assets/common/assets/web/pin.html @@ -68,8 +68,8 @@

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


-

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

-

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

+

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

+

{{ $t('pin.device_management_warning') }} {{ $t('_common.learn_more') }}

{{ $t('_common.success') }} {{ $t('pin.unpair_single_success') }}
@@ -235,6 +235,14 @@

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

} ] }, { name: 'Operation', permissions: [ + { + name: 'clipboard_set', + suppressed_by: [] + }, + { + name: 'clipboard_read', + suppressed_by: [] + }, { name: 'server_cmd', suppressed_by: []