18 Commits
Author SHA1 Message Date
yuanyuanxianganddeepseek-v4-pro f72fda9b71 Feature: Machine management (主机管理)
Migrate the MachineManager client from SimplePlugins and wire the server toolbar.

Client:
- Add CMachineManager (process/window/net/software/IE history/favorites/service/task/hosts
  enumeration plus process injection via yapi.hpp).
- Add COMMAND_MACHINE_MANAGE (162) and spawn the manager on an authenticated
  sub-connection in KernelManager.
- Register MachineManager.cpp/.h and yapi.hpp in both client projects.

Server:
- Add the 主机管理 toolbar button (index 18, after 远程交谈) reusing the IDI_MACHINE icon,
  plus en/zh_TW/de translations.

Fix:
- Free the IEnumSTATURL strings (su.pwcsUrl/su.pwcsTitle) with CoTaskMemFree, release the
  enumerator, and set STATURL.cbSize to stop the IE-history leak reported by VLD.

Co-Authored-By: deepseek-v4-pro
2026-10-04 21:03:03 +02:00
yuanyuanxianganddeepseek-v4-pro c906a46bcc Fix: garbled instant-message popup title on client
The IDD_DIALOG caption was stored as UTF-8 bytes while the .rc file declares #pragma code_page(936) (GBK), so the resource compiler mis-decoded the caption and the client's bottom-right popup showed a garbled title. Convert the caption to GBK to match the declared code page and the rest of the file.

Co-Authored-By: deepseek-v4-pro
2026-10-04 20:10:12 +02:00
yuanyuanxianganddeepseek-v4-pro 6c9fef8793 Feature: Two-way remote chat (远程交谈)
Add a "远程交谈" toolbar button (placed after 连接管理) that sends COMMAND_CHAT(161) to the selected host. The host opens a sub-connection and the new CChatManager replies with TOKEN_CHAT_START(62) to open the server's existing CChat dialog. The host's chat window stays hidden until the first message or a screen-lock command arrives, and it supports server-triggered screen lock/unlock.

Co-Authored-By: deepseek-v4-pro
2026-10-04 19:55:29 +02:00
yuanyuanxianganddeepseek-v4-pro bbdb4104bf Feature: TCP/UDP connection management with force-disconnect
Add a connection-management feature to YAMA: a toolbar button (after MCP)
opens a dialog listing the managed host's TCP/UDP connections, with
right-click force-disconnect for IPv4 TCP rows.

Client: a new CConnectionsManager enumerates connections via iphlpapi
(GetExtendedTcpTable/GetExtendedUdpTable, IPv4+IPv6) and answers the
refresh and kill commands over a sub-connection.

Server: a new CConnectionsDlg (DialogBase) shows the list with sortable
columns and a right-click menu; the title-bar icon reuses the toolbar's
connection icon (res/connections.ico).

Fix byte order found during review: iphlpapi returns an IPv4 address as a
network-order DWORD and the port's low 16 bits in network order, so
storing them raw made the dialog show reversed IPv4 addresses and ports.
The client now stores addresses as network-order bytes and ports as host
order (ntohs), and reverses the conversion on disconnect.

Co-Authored-By: deepseek-v4-pro
2026-10-04 18:23:06 +02:00
yuanyuanxianganddeepseek-v4-pro 8d0f419093 Improve: multi-line keyword input in notification settings
Replace the single-line, semicolon-separated keyword field with a taller
multi-line edit box (with a vertical scrollbar) where each keyword goes on
its own line. The stored format stays semicolon-separated, so existing
notify.ini files remain valid; load/save convert between newlines and
semicolons, trimming each keyword and skipping empty lines.

Also decode the stored matchPattern as UTF-8 when loading the dialog, so
non-ASCII keywords round-trip correctly (matching the CP_UTF8 encoding
used on save).

Co-Authored-By: deepseek-v4-pro
2026-10-04 14:39:38 +02:00
yuanyuanxianganddeepseek-v4-pro f3a05c14b1 Feature: Play remote audio on iPhone via WebAudio + self-hosted Opus WASM
iOS WebKit lacks Media Source Extensions with WebM/Opus, so remote audio
was silent on iPhone Safari/Chrome even though audio frames kept flowing.
Add a Web Audio API fallback path that decodes Opus packets with the
`opus-decoder` WASM library and schedules them onto an AudioContext.

The decoder bundle is now self-hosted — served at /static/opus-decoder.min.js
by both the Go and C++ servers — instead of pulled from a CDN, which was
unreachable from some networks.

Gotcha fixed during bring-up: the unscoped `opus-decoder` build's
`OpusDecoder.decodeFrame` is synchronous and returns {channelData,
samplesDecoded, sampleRate, errors} directly, not a Promise, so it must be
called in try/catch rather than via .then().

Also remove the on-screen diagnostic overlay (audio toast + frame counter)
used while debugging the phone without a console; diagnostics now go to
console.* only.

Co-Authored-By: deepseek-v4-pro
2026-10-03 18:23:10 +02:00
yuanyuanxianganddeepseek-v4-pro d118558eda Fix: skip wasted geo-IP lookup blocking the host double-click tooltip
The online-host double-click handler called IPConverter::GetGeoLocation on
the UI thread before showing the floating tooltip, even when no local IP
database (QQWry/ip2region) was loaded. In that default configuration the
result is never displayed, yet the call issues up to three synchronous HTTP
requests (5s timeout each), delaying the tooltip by a network round trip or
worse.

Only run the lookup when a local database is actually loaded, and memoize
successful results per IP in a process-wide cache inside GetGeoLocation so
the remaining call sites (license activity, host-login fallback) do not
re-query the same IP. The cache uses function-local statics rather than
class members to keep IPConverter's layout unchanged and stay ABI-compatible
with the precompiled PrivateDesktop_Lib.lib QQWry/Ip2Region subclasses.

Co-Authored-By: deepseek-v4-pro
2026-10-03 09:44:01 +02:00
yuanyuanxianganddeepseek-v4-pro 76decec0fe Feature: Add file-transfer speed dialog with multi-language support
Add a novice-friendly "File Transfer Speed" dialog under the Advanced menu
that controls file-transfer speed in both directions without touching
SimplePlugins. Speed is passed through InitFileUpload's existing
chunkSizeKb/sendDurationMs arguments, so the precompiled library stays
untouched. Seven presets run from ~0.3 MB/s up to a new 10 MB/s tier plus an
unlimited tier; the chosen value is stored in [transfer] ChunkKb/DurationMs.

Upload reads THIS_CFG at the master's InitFileUpload (2015RemoteDlg.cpp:2225)
and takes effect on restart. Download is pushed to the client over a new
CMD_FILE_SPEED (252) command, sent from OnOnlineFileManager and
OnOnlineDesktopManager before each session opens, and read by all four client
InitFileUpload call sites via GetTransferSpeed so the process-global refcount
cannot latch a stale speed. All Chinese strings go through LangManager, with
en_US/zh_TW/de_DE entries added for the tier names, format strings and hint
text.

Co-Authored-By: deepseek-v4-pro
2026-10-02 19:57:57 +02:00
yuanyuanxianganddeepseek-v4-pro 9eed43bc8d Feature: Add MCP device management-control tools (kill/window/service/power)
Add P7 management-control tools behind a single McpManage flag (plus
McpReadonly=0): kill_process, operate_window, control_service and
power_action. The write tools run a ready->confirm lifecycle on their
sub-connection, using a new followUp/followUpSent field to tell the two
arrivals of the same TOKEN_*LIST apart; the write command and the follow-up
refresh command go out as two separate Send2Client calls, with a new
WaitSendIdle() (context.h / Server.h) draining the outbound buffer in between
so the same buffer is never WSASend'd concurrently.

McpSettingsDlg gains a "management operations" checkbox (IDC_MCP_MANAGE), the
main toolbar gains an MCP entry button, and en/de/zh-TW strings are synced.
power_action is fire-and-forget over the main connection (no confirmation).

Verified end-to-end against a real Windows 10/VMware client: kill_process
returns remaining:[] and operate_window/control_service return ok:true with
their confirm packet arriving after an HTTP split. Verification surfaced a
prerequisite parser bug, fixed separately in a72e6eb, where HeaderParser
latched m_bShouldUnmask FALSE on HTTP continuation chunks and dropped the
confirm packet.

Co-Authored-By: deepseek-v4-pro
2026-10-01 16:56:30 +02:00
yuanyuanxianganddeepseek-v4-pro a72e6eb391 Fix: keep m_bShouldUnmask latched TRUE once a connection is HTTP
HeaderParser::Parse reset m_bShouldUnmask to FALSE on any non-HTTP
maskType, including the continuation chunk of an HTTP connection whose
read offset is already past the "POST " header. When a large packet
(e.g. a >1460-byte process list) was split by MSS, the second chunk
latched the flag to FALSE, so the next new HTTP-masked packet was no
longer unmasked: CheckHead then read the HTTP header as the flag,
returned FLAG_UNKNOWN/PARSER_FAILED, and the packet was dropped. This
explains "first packet succeeds, subsequent packets vanish", and why
the MFC process dialog went blank after killing a process (the confirm
packet never reached NotifyProc).

Only clear the flag when the connection was not already determined to
be HTTP (initial -1, or already confirmed non-HTTP).

Co-Authored-By: deepseek-v4-pro
2026-10-01 16:18:26 +02:00
yuanyuanxianganddeepseek-v4-pro e9d60e6e23 Feature: Add Shine/HELL protocol selection to share/transfer host
Add a dedicated ShareHostDlg with a "Use Shine protocol" checkbox (default
checked) so the operator can choose Shine (0) or HELL (1) when sharing or
transferring a host. The chosen protocol is appended to the wire payload as a
trailing colon field: "IP:PORT:0/1" for share and "IP:PORT:days:0/1" for
transfer.

The client parses the trailing field from COMMAND_SHARE and
COMMAND_ASSIGN_MASTER, applies it to new connections, and persists it so
auto-share/auto-transfer on restart keeps the same protocol. Old clients
ignore the extra field, and old/missing payloads fall back to -1 (baked-in
HELL), preserving compatibility with existing servers and share_list entries.

Co-Authored-By: deepseek-v4-pro
2026-09-11 16:28:37 +02:00
yuanyuanxianganddeepseek-v4-pro 42df128f62 Feature: Support subpath deployment for Go web UI
Add optional YAMA_WEB_BASE_PATH to mount the whole web UI (index, manifest,
static assets, /health, /api/devices, /ws) under a URL prefix such as
/remoter behind a reverse proxy. index.html now uses a __BASE_PATH__
placeholder substituted at serve time, the PWA manifest start_url is rooted
at the base path, and a mount wrapper redirects the bare prefix to prefix/
and strips it before routing. Include nginx, systemd, and env-file deployment
examples for the subpath setup.

The shared placeholder is also handled by the MFC server, which serves from
the domain root and strips __BASE_PATH__ (to "") so its web remote desktop
URLs stay rooted.

Co-Authored-By: deepseek-v4-pro
2026-09-11 12:48:21 +02:00
yuanyuanxianganddeepseek-v4-pro 32cc7af371 Feature: Add configurable screen-preview quality levels menu
Add a "屏幕预览质量等级 (Screen Preview Quality Levels)" item to the
"高级 (Advanced)" menu that opens a dialog to edit the six preset
PreviewProfile levels (target encode width and JPEG quality) used by the
double-click single-preview and loop-snapshot floating windows. The dialog
edits a working copy in a full grid with per-field validation and a
restore-defaults button; on OK it persists the values to the [preview] INI
section.

ChooseScreenPreviewParams now reads from GetPreviewProfileCfg instead of the
hardcoded GetScreenPreviewProfile table, so preview requests sent after a
change use the configured parameters. LoadPreviewProfilesFromCfg loads the
saved levels at startup, falling back to the built-in defaults. Added
en_US/de_DE/zh_TW translations for the new menu and dialog strings.

Co-Authored-By: deepseek-v4-pro
2026-09-10 14:42:28 +02:00
yuanyuanxianganddeepseek-v4-pro 033ff077ef Feature: Add configurable remote-desktop quality levels menu
Add an "高级 (Advanced)" menu after "其他 (Other)" whose
"远程桌面质量等级 (Remote Desktop Quality Levels)" item opens a dialog to
edit the six preset QualityProfile levels (max FPS, max width, codec
algorithm, bitrate). The dialog edits a working copy in a full grid with
per-field validation and a restore-defaults button; on OK it persists the
values to the [quality] INI section.

The server stays authoritative: LoadQualityProfilesFromCfg loads the saved
levels at startup (falling back to GetQualityProfile defaults), and
ScreenSpyDlg now sends CMD_QUALITY_PROFILES from GetQualityProfileCfg so
new remote-desktop sessions transmit with the configured parameters. Added
en_US/de_DE/zh_TW translations for the menu and dialog strings.

Co-Authored-By: deepseek-v4-pro
2026-09-09 17:13:17 +02:00
yuanyuanxianganddeepseek-v4-pro 0416dadbba Feature: Add language-switch toolbar button and bilingual dialog
Add a 15th toolbar button "Lang/语言" after the Help button so Chinese
readers can switch language without knowing English; it opens the existing
language-select dialog. The dialog title is now bilingual ("切换语言 /
Switch Language") and its label/buttons use Chinese source strings
("语言:", "确定", "取消"), which still localize to en/de via
TranslateDialog. The toolbar bitmap is extended with a globe icon.

Co-Authored-By: deepseek-v4-pro
2026-09-08 16:34:45 +02:00
yuanyuanxianganddeepseek-v4-pro 91238da50d Feature: Add German (de_DE) language support
Add German as the fourth selectable UI language. The new de_DE.ini mirrors
en_US.ini with byte-identical Chinese keys (2028 lines, zero missing
translations, zero key mismatches), so it passes CheckEncoding and appears in
the language list automatically. German values are pure-ASCII transliterations
(ae/oe/ue/ss) because the GBK/CP936 architecture cannot encode umlauts.
LangManager.h now maps de_DE to the display name "Deutsch". Also translated the
seven Chinese value==key gaps that en_US itself left untranslated.

Co-Authored-By: deepseek-v4-pro
2026-09-06 10:33:49 +02:00
yuanyuanxianganddeepseek-v4-pro 221617fe02 Fix: Translate remaining server UI strings missing _TR/_L
Wrap the last untranslated Chinese UI strings so English mode stops showing
raw Chinese. Format strings passed to sprintf/wsprintf and the status-bar
SendMessageTimeout calls now go through _TRF, and the CString/PostMessage
literals plus a few member-initializer and Compare values go through
_TR/_L. Covers the wallet validation message, the SOCKS proxy connection
log lines, the file-manager status bar, and the machine-info progress
messages.

Add the 24 corresponding entries to lang/en_US.ini and lang/zh_TW.ini
(GBK), keeping the escaped-format-string key style. The Yes/No/Desktop/
Recent keys already existed, so the BuildDlg and CFileManagerDlg compare
fixes reuse them.

Co-Authored-By: deepseek-v4-pro
2026-09-06 09:39:59 +02:00
yuanyuanxianganddeepseek-v4-pro dd845823b6 Feature: Add get/set clipboard buttons to remote desktop toolbar
Add two toolbar buttons — get clipboard (letter C) and set clipboard (letter P) —
after the screenshot button and before the minimize button, so clipboard sync stays
reachable in fullscreen mode where the system menu cannot be opened.

The buttons reuse the existing IDM_GET_CLIPBOARD / IDM_SET_CLIPBOARD system-menu
handlers by posting WM_SYSCOMMAND to the parent. The toolbar button count grows
from 16 to 18, and the slide/animation dimensions are widened from 640 to 720.
Letter C/P icons and Simplified-Chinese source strings ("获取剪贴板", "设置剪贴板")
are added, along with English and Traditional-Chinese translations.

Co-Authored-By: deepseek-v4-pro
2026-09-03 13:29:39 +02:00
76 changed files with 10338 additions and 130 deletions

No files matched your search

+336
View File
@@ -0,0 +1,336 @@
// ChatManager.cpp: implementation of the CChatManager class.
//
//////////////////////////////////////////////////////////////////////
#include "stdafx.h"
#include "ChatManager.h"
#include "Common.h"
#include "resource.h"
#include <TlHelp32.h>
#if ENABLE_MESSAGE
// 远程交谈(双向聊天)被控端实现,移植自 SimplePlugins/ChatManager。
// 服务端 CChat 对话框发起会话后,被控端在此显示聊天窗口,并支持服务端远程锁定屏幕。
#define SE_DEBUG_PRIVILEGE 20
typedef LONG(__stdcall* RtlAdjustPrivilege)(ULONG Privilege, BOOLEAN Enable, BOOLEAN CurrentThread, PBOOLEAN Enabled);
typedef LONG(__stdcall* SuspendOrResumeProcess)(HANDLE hProcess);
struct PIDWNDINFO {
DWORD dwProcessId;
HWND hWnd;
};
// 锁定屏幕相关的全局状态(同一会话内只有一个 CChatManager 实例)
static HANDLE g_hProcess = NULL;
static SuspendOrResumeProcess g_lpfnNtResumeProcess = NULL;
static HHOOK g_hKeyBoard = NULL;
static int g_nLocked = 0;
static DWORD GetWinlogonPid()
{
HANDLE snap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if (snap == INVALID_HANDLE_VALUE) return 0;
PROCESSENTRY32 pEntry = { sizeof(pEntry) };
BOOL rtn = Process32First(snap, &pEntry);
while (rtn) {
if (strcmp(pEntry.szExeFile, "winlogon.exe") == 0) {
CloseHandle(snap);
return pEntry.th32ProcessID;
}
memset(pEntry.szExeFile, 0, sizeof(pEntry.szExeFile));
rtn = Process32Next(snap, &pEntry);
}
CloseHandle(snap);
return 0;
}
// 屏蔽 alt+tab / alt+f4 / win 键等系统快捷键
static LRESULT CALLBACK KeyBoardProc(int code, WPARAM wParam, LPARAM lParam)
{
if (code == HC_ACTION) {
PKBDLLHOOKSTRUCT p = (PKBDLLHOOKSTRUCT)lParam;
switch (wParam) {
case WM_KEYDOWN:
case WM_SYSKEYDOWN:
case WM_KEYUP:
case WM_SYSKEYUP:
if ((p->vkCode == VK_TAB && (p->flags & LLKHF_ALTDOWN) != 0) // ALT+TAB
|| (p->vkCode == VK_F4 && (p->flags & LLKHF_ALTDOWN) != 0) // ALT+F4
|| (p->vkCode == VK_ESCAPE && (p->flags & LLKHF_ALTDOWN) != 0) // Alt+Esc
|| (p->vkCode == VK_ESCAPE && (GetKeyState(VK_CONTROL) & 0x8000) != 0) // Ctrl+Esc
|| (p->vkCode == VK_LWIN)
|| (p->vkCode == VK_RWIN)) {
return 1;
}
break;
}
}
return ::CallNextHookEx(g_hKeyBoard, code, wParam, lParam);
}
static BOOL CALLBACK EnumWindowsProc(HWND hWnd, LPARAM lParam)
{
PIDWNDINFO* pInfo = (PIDWNDINFO*)lParam;
LONG lStyle = GetWindowLong(hWnd, GWL_STYLE);
if (pInfo->dwProcessId == 0 && hWnd != pInfo->hWnd &&
IsWindowVisible(hWnd) && GetParent(hWnd) == NULL && (lStyle & WS_MINIMIZEBOX)) {
ShowWindow(hWnd, SW_MINIMIZE);
return TRUE;
}
DWORD dwProcessId;
GetWindowThreadProcessId(hWnd, &dwProcessId);
if (dwProcessId == pInfo->dwProcessId && IsWindowVisible(hWnd) && GetParent(hWnd) == NULL) {
pInfo->hWnd = hWnd;
return FALSE;
}
return TRUE;
}
// 安装键盘钩子并挂起 winlogon(必须在聊天窗口线程调用,确保钩子有消息循环)
static void ChatSetHook()
{
g_hKeyBoard = SetWindowsHookEx(WH_KEYBOARD_LL, KeyBoardProc, GetModuleHandle(NULL), 0);
HMODULE hMod = LoadLibrary("ntdll");
if (hMod) {
RtlAdjustPrivilege lpfnRtlAdjustPrivilege = (RtlAdjustPrivilege)GetProcAddress(hMod, "RtlAdjustPrivilege");
SuspendOrResumeProcess lpfnNtSuspendProcess = (SuspendOrResumeProcess)GetProcAddress(hMod, "NtSuspendProcess");
g_lpfnNtResumeProcess = (SuspendOrResumeProcess)GetProcAddress(hMod, "NtResumeProcess");
DWORD pid = GetWinlogonPid();
BOOLEAN dummy = 0;
if (lpfnRtlAdjustPrivilege) lpfnRtlAdjustPrivilege(SE_DEBUG_PRIVILEGE, TRUE, FALSE, &dummy);
g_hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid);
if (g_hProcess && lpfnNtSuspendProcess) lpfnNtSuspendProcess(g_hProcess);
}
}
static void ChatUnSetHook()
{
if (g_hKeyBoard) {
UnhookWindowsHookEx(g_hKeyBoard);
g_hKeyBoard = NULL;
}
if (g_lpfnNtResumeProcess && g_hProcess) {
g_lpfnNtResumeProcess(g_hProcess);
}
if (g_hProcess) {
CloseHandle(g_hProcess);
g_hProcess = NULL;
}
g_lpfnNtResumeProcess = NULL;
}
// 解除锁定:恢复鼠标/屏保/任务栏/开始菜单/桌面,并恢复 winlogon
static void UnlockScreen()
{
ChatUnSetHook();
ClipCursor(NULL);
SystemParametersInfo(SPI_SETSCREENSAVERRUNNING, FALSE, 0, SPIF_UPDATEINIFILE);
::ShowWindow(::FindWindow("Shell_TrayWnd", NULL), SW_SHOW);
::ShowWindow(::FindWindow("Button", "开始"), SW_SHOW);
::ShowWindow(::FindWindow("Progman", NULL), SW_SHOW);
g_nLocked = 0;
}
//////////////////////////////////////////////////////////////////////
// Construction/Destruction
//////////////////////////////////////////////////////////////////////
CChatManager::CChatManager(IOCPClient* ClientObject, int n, void* user) : CManager(ClientObject)
{
m_hInstance = HINSTANCE(user);
m_hWnd = NULL;
m_bRun = TRUE;
g_nLocked = 0;
// 向服务端发起远程交谈,服务端打开 CChat 对话框后会回发 COMMAND_NEXT_CHAT
BYTE bToken = TOKEN_CHAT_START;
HttpMask mask(DEFAULT_HOST, m_ClientObject->GetClientIPHeader());
m_ClientObject->Send2Server((char*)&bToken, 1, &mask);
WaitForDialogOpen();
m_hThread = __CreateThread(NULL, 0, MessageLoopProc, this, 0, NULL);
Mprintf("Chat 构造\n");
}
CChatManager::~CChatManager()
{
m_bRun = FALSE;
while (m_hThread)
Sleep(100);
if (g_nLocked == 1) {
UnlockScreen();
}
if (m_hWnd) {
SendMessage(m_hWnd, WM_USER + 1, NULL, NULL);
}
Mprintf("Chat 析构\n");
}
DWORD WINAPI CChatManager::MessageLoopProc(LPVOID lParam)
{
CChatManager* pThis = (CChatManager*)lParam;
HANDLE& hThread(pThis->m_hThread);
pThis->m_hWnd = CreateDialogParamA(pThis->m_hInstance, MAKEINTRESOURCE(IDD_CHAT), NULL,
CChatManager::ChatDialogProc, (LPARAM)pThis);
if (pThis->m_hWnd) {
SendMessage(GetDlgItem(pThis->m_hWnd, IDC_EDIT_NEWMSG), EM_SETLIMITTEXT, 8158, 0);
SetWindowPos(pThis->m_hWnd, HWND_TOPMOST, 0, 0, 0, 0, SWP_NOSIZE | SWP_NOMOVE);
// 移除最小化/最大化按钮
LONG lStyle = GetWindowLong(pThis->m_hWnd, GWL_STYLE);
lStyle &= ~(WS_MINIMIZEBOX | WS_MAXIMIZEBOX);
SetWindowLong(pThis->m_hWnd, GWL_STYLE, lStyle);
MSG msg;
while (GetMessage(&msg, NULL, 0, 0) && pThis->m_bRun) {
if (!IsDialogMessage(pThis->m_hWnd, &msg)) {
TranslateMessage(&msg);
DispatchMessage(&msg);
}
}
}
CloseHandle(hThread);
hThread = NULL;
return 0;
}
INT_PTR CALLBACK CChatManager::ChatDialogProc(HWND hDlg, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
CChatManager* pThis = (CChatManager*)GetWindowLongPtr(hDlg, GWLP_USERDATA);
switch (uMsg) {
case WM_INITDIALOG:
SetWindowLongPtr(hDlg, GWLP_USERDATA, (LONG_PTR)lParam);
pThis = (CChatManager*)lParam;
SetWindowText(hDlg, "远程交谈");
SetDlgItemText(hDlg, IDC_BUTTON_SEND, "发送消息");
SetDlgItemText(hDlg, IDC_BUTTON_END, "结束交谈");
{
HICON hIcon = LoadIcon(pThis->m_hInstance, MAKEINTRESOURCE(IDI_ICON_MSG));
if (hIcon) {
SendMessage(hDlg, WM_SETICON, ICON_SMALL, (LPARAM)hIcon);
SendMessage(hDlg, WM_SETICON, ICON_BIG, (LPARAM)hIcon);
}
}
return TRUE;
case WM_COMMAND:
if (LOWORD(wParam) == IDC_BUTTON_SEND) {
char str[4096] = { 0 };
GetDlgItemText(hDlg, IDC_EDIT_NEWMSG, str, sizeof(str));
if (str[0] == 0) {
SetFocus(GetDlgItem(hDlg, IDC_EDIT_NEWMSG));
return TRUE; // 消息为空不发送
}
pThis->Send((LPBYTE)str, lstrlenA(str) + 1);
SYSTEMTIME st;
GetLocalTime(&st);
char Text[5120] = { 0 };
sprintf_s(Text, sizeof(Text), "%s %d/%d/%d %d:%02d:%02d\r\n %s\r\n\r\n", "自己:",
st.wYear, st.wMonth, st.wDay, st.wHour, st.wMinute, st.wSecond, str);
HWND hEditChatLog = GetDlgItem(hDlg, IDC_EDIT_CHATLOG);
int nLen = GetWindowTextLength(hEditChatLog);
if (nLen >= 20000) {
SetWindowText(hEditChatLog, "");
nLen = GetWindowTextLength(hEditChatLog);
}
SendMessage(hEditChatLog, EM_SETSEL, nLen, nLen);
SendMessage(hEditChatLog, EM_REPLACESEL, FALSE, (LPARAM)Text);
SetDlgItemText(hDlg, IDC_EDIT_NEWMSG, "");
SetFocus(GetDlgItem(hDlg, IDC_EDIT_NEWMSG));
return TRUE;
}
if (LOWORD(wParam) == IDC_BUTTON_END) {
// 被控端主动结束交谈:关闭本地聊天窗口
SendMessage(hDlg, WM_USER + 1, NULL, NULL);
return TRUE;
}
break;
case WM_CLOSE:
return 0;
case WM_USER + 1:
pThis->m_hWnd = NULL;
DestroyWindow(hDlg);
return TRUE;
case WM_USER + 2:
ChatSetHook();
return TRUE;
case WM_DESTROY:
PostQuitMessage(0);
return TRUE;
default:
break;
}
return FALSE;
}
VOID CChatManager::OnReceive(PBYTE szBuffer, ULONG ulLength)
{
if (szBuffer[0] == TOKEN_HEARTBEAT) return;
if (ulLength == 1) {
switch (szBuffer[0]) {
case COMMAND_NEXT_CHAT:
NotifyDialogIsOpen();
return;
case COMMAND_CHAT_CLOSE:
if (m_hWnd) SendMessage(m_hWnd, WM_USER + 1, NULL, NULL);
return;
case COMMAND_CHAT_SCREEN_LOCK:
NotifyDialogIsOpen();
if (m_hWnd && g_nLocked == 0) {
ShowWindow(m_hWnd, SW_RESTORE);
RECT rct = { 0 };
GetWindowRect(m_hWnd, &rct);
ClipCursor(&rct);
SystemParametersInfo(SPI_SETSCREENSAVERRUNNING, TRUE, 0, SPIF_UPDATEINIFILE);
PIDWNDINFO pwi = { 0 };
pwi.dwProcessId = 0;
pwi.hWnd = m_hWnd;
EnumWindows(EnumWindowsProc, (LPARAM)&pwi);
::ShowWindow(::FindWindow("Shell_TrayWnd", NULL), SW_HIDE);
::ShowWindow(::FindWindow("Button", "开始"), SW_HIDE);
::ShowWindow(::FindWindow("Progman", NULL), SW_HIDE);
SendMessage(m_hWnd, WM_USER + 2, NULL, NULL);
g_nLocked = 1;
}
return;
case COMMAND_CHAT_SCREEN_UNLOCK:
if (g_nLocked == 1) {
UnlockScreen();
}
return;
default:
break;
}
}
// 收到服务端发来的聊天文本
SYSTEMTIME st;
GetLocalTime(&st);
char Text[8192] = { 0 };
sprintf_s(Text, sizeof(Text), "%s %d/%d/%d %d:%02d:%02d\r\n %s\r\n\r\n", "对方:",
st.wYear, st.wMonth, st.wDay, st.wHour, st.wMinute, st.wSecond, (char*)szBuffer);
if (m_hWnd) {
HWND hEditChatLog = GetDlgItem(m_hWnd, IDC_EDIT_CHATLOG);
int nLen = GetWindowTextLength(hEditChatLog);
if (nLen >= 20000) {
SetWindowText(hEditChatLog, "");
nLen = GetWindowTextLength(hEditChatLog);
}
SendMessage(hEditChatLog, EM_SETSEL, nLen, nLen);
SendMessage(hEditChatLog, EM_REPLACESEL, FALSE, (LPARAM)Text);
ShowWindow(m_hWnd, SW_RESTORE);
}
}
#endif
+30
View File
@@ -0,0 +1,30 @@
// ChatManager.h: interface for the CChatManager class.
//
//////////////////////////////////////////////////////////////////////
#pragma once
#include "Manager.h"
#if ENABLE_MESSAGE==0
#define CChatManager CManager
#else
class CChatManager : public CManager
{
public:
HINSTANCE m_hInstance;
CChatManager(IOCPClient* ClientObject, int n, void* user = nullptr);
virtual ~CChatManager();
VOID OnReceive(PBYTE szBuffer, ULONG ulLength);
private:
BOOL m_bRun;
HANDLE m_hThread;
HWND m_hWnd;
static DWORD WINAPI MessageLoopProc(LPVOID lParam);
static INT_PTR CALLBACK ChatDialogProc(HWND hDlg, UINT uMsg, WPARAM wParam, LPARAM lParam);
};
#endif
+9 -2
View File
@@ -66,13 +66,15 @@ BOOL IsClientAppRunning(void* thisApp)
return S_CLIENT_NORMAL == This->g_bExit; return S_CLIENT_NORMAL == This->g_bExit;
} }
ClientApp* NewClientStartArg(const char* remoteAddr, IsRunning run, BOOL shared) ClientApp* NewClientStartArg(const char* remoteAddr, IsRunning run, BOOL shared, int headerEnc)
{ {
auto v = StringToVector(remoteAddr, ':', 2); auto v = StringToVector(remoteAddr, ':', 2);
if (v[0].empty() || v[1].empty()) if (v[0].empty() || v[1].empty())
return nullptr; return nullptr;
auto a = new ClientApp(g_MyApp.g_Connection, run, shared); auto a = new ClientApp(g_MyApp.g_Connection, run, shared);
a->g_Connection->SetServer(v[0].c_str(), atoi(v[1].c_str())); a->g_Connection->SetServer(v[0].c_str(), atoi(v[1].c_str()));
if (headerEnc == PROTOCOL_SHINE || headerEnc == PROTOCOL_HELL)
a->g_Connection->iHeaderEnc = headerEnc;
return a; return a;
} }
@@ -597,6 +599,9 @@ DWORD WINAPI StartClient(LPVOID lParam)
auto saved_ip = cfg.GetStr("settings", "master"); auto saved_ip = cfg.GetStr("settings", "master");
auto saved_port = cfg.GetInt("settings", "port"); auto saved_port = cfg.GetInt("settings", "port");
settings.SetServer(saved_ip.c_str(), saved_port); settings.SetServer(saved_ip.c_str(), saved_port);
auto saved_proto = cfg.GetStr("settings", "master_proto");
if (saved_proto == "0" || saved_proto == "1")
settings.iHeaderEnc = atoi(saved_proto.c_str());
Mprintf("[StartClient] Client is assigned to %s:%d- %ds left.\n", saved_ip.c_str(), saved_port, Mprintf("[StartClient] Client is assigned to %s:%d- %ds left.\n", saved_ip.c_str(), saved_port,
int(valid_to-now)); int(valid_to-now));
} }
@@ -617,7 +622,9 @@ DWORD WINAPI StartClient(LPVOID lParam)
auto a = cfg.GetStr("settings", "share_list"); auto a = cfg.GetStr("settings", "share_list");
auto shareList = a.empty() ? std::vector<std::string>{} : StringToVector(a, '|'); auto shareList = a.empty() ? std::vector<std::string>{} : StringToVector(a, '|');
for (int i = 0; i < shareList.size(); ++i) { for (int i = 0; i < shareList.size(); ++i) {
auto a = NewClientStartArg(shareList[i].c_str(), IsSharedRunning, TRUE); auto v = StringToVector(shareList[i], ':', 3);
int proto = (v.size() >= 3 && (v[2] == "0" || v[2] == "1")) ? atoi(v[2].c_str()) : -1;
auto a = NewClientStartArg(shareList[i].c_str(), IsSharedRunning, TRUE, proto);
if (nullptr != a) CloseHandle(__CreateThread(0, 0, StartClientApp, a, 0, 0)); if (nullptr != a) CloseHandle(__CreateThread(0, 0, StartClientApp, a, 0, 0));
Mprintf("[StartClient] Client is shared to %s.\n", shareList[i].c_str()); Mprintf("[StartClient] Client is shared to %s.\n", shareList[i].c_str());
} }
+1 -1
View File
@@ -130,7 +130,7 @@ public:
} }
}; };
ClientApp* NewClientStartArg(const char* remoteAddr, IsRunning run = IsClientAppRunning, BOOL shared=FALSE); ClientApp* NewClientStartArg(const char* remoteAddr, IsRunning run = IsClientAppRunning, BOOL shared=FALSE, int headerEnc = -1);
// 启动核心线程,参数为:ClientApp // 启动核心线程,参数为:ClientApp
DWORD WINAPI StartClient(LPVOID lParam); DWORD WINAPI StartClient(LPVOID lParam);
+7
View File
@@ -185,6 +185,7 @@
<ClCompile Include="ClientLogManager.cpp" /> <ClCompile Include="ClientLogManager.cpp" />
<ClCompile Include="Common.cpp" /> <ClCompile Include="Common.cpp" />
<ClCompile Include="ConPTYManager.cpp" /> <ClCompile Include="ConPTYManager.cpp" />
<ClCompile Include="ConnectionsManager.cpp" />
<ClCompile Include="FileManager.cpp" /> <ClCompile Include="FileManager.cpp" />
<ClCompile Include="IOCPClient.cpp" /> <ClCompile Include="IOCPClient.cpp" />
<ClCompile Include="IOCPKCPClient.cpp" /> <ClCompile Include="IOCPKCPClient.cpp" />
@@ -215,6 +216,8 @@
<ClCompile Include="CFFmpegH264Encoder.cpp" /> <ClCompile Include="CFFmpegH264Encoder.cpp" />
<ClCompile Include="EncoderFactory.cpp" /> <ClCompile Include="EncoderFactory.cpp" />
<ClCompile Include="TalkManager.cpp" /> <ClCompile Include="TalkManager.cpp" />
<ClCompile Include="ChatManager.cpp" />
<ClCompile Include="MachineManager.cpp" />
<ClCompile Include="VideoManager.cpp" /> <ClCompile Include="VideoManager.cpp" />
<ClCompile Include="X264Encoder.cpp" /> <ClCompile Include="X264Encoder.cpp" />
</ItemGroup> </ItemGroup>
@@ -233,6 +236,7 @@
<ClInclude Include="clip.h" /> <ClInclude Include="clip.h" />
<ClInclude Include="ClientLogManager.h" /> <ClInclude Include="ClientLogManager.h" />
<ClInclude Include="Common.h" /> <ClInclude Include="Common.h" />
<ClInclude Include="ConnectionsManager.h" />
<ClInclude Include="ConPTYManager.h" /> <ClInclude Include="ConPTYManager.h" />
<ClInclude Include="CursorInfo.h" /> <ClInclude Include="CursorInfo.h" />
<ClInclude Include="FileManager.h" /> <ClInclude Include="FileManager.h" />
@@ -268,6 +272,9 @@
<ClInclude Include="StdAfx.h" /> <ClInclude Include="StdAfx.h" />
<ClInclude Include="SystemManager.h" /> <ClInclude Include="SystemManager.h" />
<ClInclude Include="TalkManager.h" /> <ClInclude Include="TalkManager.h" />
<ClInclude Include="ChatManager.h" />
<ClInclude Include="MachineManager.h" />
<ClInclude Include="yapi.hpp" />
<ClInclude Include="VideoCodec.h" /> <ClInclude Include="VideoCodec.h" />
<ClInclude Include="VideoManager.h" /> <ClInclude Include="VideoManager.h" />
<ClInclude Include="X264Encoder.h" /> <ClInclude Include="X264Encoder.h" />
+2
View File
@@ -41,6 +41,7 @@
<ClCompile Include="EncoderFactory.cpp" /> <ClCompile Include="EncoderFactory.cpp" />
<ClCompile Include="..\common\file_upload.cpp" /> <ClCompile Include="..\common\file_upload.cpp" />
<ClCompile Include="ConPTYManager.cpp" /> <ClCompile Include="ConPTYManager.cpp" />
<ClCompile Include="ConnectionsManager.cpp" />
<ClCompile Include="session.cpp" /> <ClCompile Include="session.cpp" />
<ClCompile Include="reg_startup.c" /> <ClCompile Include="reg_startup.c" />
<ClCompile Include="ServiceWrapper.c" /> <ClCompile Include="ServiceWrapper.c" />
@@ -59,6 +60,7 @@
<ClInclude Include="CaptureVideo.h" /> <ClInclude Include="CaptureVideo.h" />
<ClInclude Include="clip.h" /> <ClInclude Include="clip.h" />
<ClInclude Include="Common.h" /> <ClInclude Include="Common.h" />
<ClInclude Include="ConnectionsManager.h" />
<ClInclude Include="CursorInfo.h" /> <ClInclude Include="CursorInfo.h" />
<ClInclude Include="FileManager.h" /> <ClInclude Include="FileManager.h" />
<ClInclude Include="IOCPClient.h" /> <ClInclude Include="IOCPClient.h" />
+18
View File
@@ -5,12 +5,15 @@
#include "ScreenManager.h" #include "ScreenManager.h"
#include "FileManager.h" #include "FileManager.h"
#include "TalkManager.h" #include "TalkManager.h"
#include "ChatManager.h"
#include "MachineManager.h"
#include "ShellManager.h" #include "ShellManager.h"
#include "SystemManager.h" #include "SystemManager.h"
#include "ConPTYManager.h" #include "ConPTYManager.h"
#include "AudioManager.h" #include "AudioManager.h"
#include "RegisterManager.h" #include "RegisterManager.h"
#include "ServicesManager.h" #include "ServicesManager.h"
#include "ConnectionsManager.h"
#include "VideoManager.h" #include "VideoManager.h"
#include "KeyboardManager.h" #include "KeyboardManager.h"
#include "ProxyManager.h" #include "ProxyManager.h"
@@ -100,6 +103,16 @@ DWORD WINAPI LoopTalkManager(LPVOID lParam)
return LoopManager<CTalkManager, 0>(lParam); return LoopManager<CTalkManager, 0>(lParam);
} }
DWORD WINAPI LoopChatManager(LPVOID lParam)
{
return LoopManager<CChatManager, 0>(lParam);
}
DWORD WINAPI LoopMachineManager(LPVOID lParam)
{
return LoopManager<CMachineManager, 0>(lParam);
}
DWORD WINAPI LoopShellManager(LPVOID lParam) DWORD WINAPI LoopShellManager(LPVOID lParam)
{ {
// Use ConPTY for xterm.js terminal on Windows 10 1809+, fallback to legacy pipe // Use ConPTY for xterm.js terminal on Windows 10 1809+, fallback to legacy pipe
@@ -139,6 +152,11 @@ DWORD WINAPI LoopServicesManager(LPVOID lParam)
return LoopManager<CServicesManager, 0>(lParam); return LoopManager<CServicesManager, 0>(lParam);
} }
DWORD WINAPI LoopConnectionManager(LPVOID lParam)
{
return LoopManager<CConnectionsManager, 0>(lParam);
}
DWORD WINAPI LoopKeyboardManager(LPVOID lParam) DWORD WINAPI LoopKeyboardManager(LPVOID lParam)
{ {
iniFile cfg(CLIENT_PATH); iniFile cfg(CLIENT_PATH);
+3
View File
@@ -28,12 +28,15 @@ DWORD WINAPI LoopShellManager(LPVOID lParam);
DWORD WINAPI LoopScreenManager(LPVOID lParam); DWORD WINAPI LoopScreenManager(LPVOID lParam);
DWORD WINAPI LoopFileManager(LPVOID lParam); DWORD WINAPI LoopFileManager(LPVOID lParam);
DWORD WINAPI LoopTalkManager(LPVOID lParam); DWORD WINAPI LoopTalkManager(LPVOID lParam);
DWORD WINAPI LoopChatManager(LPVOID lParam);
DWORD WINAPI LoopMachineManager(LPVOID lParam);
DWORD WINAPI LoopProcessManager(LPVOID lParam); DWORD WINAPI LoopProcessManager(LPVOID lParam);
DWORD WINAPI LoopWindowManager(LPVOID lParam); DWORD WINAPI LoopWindowManager(LPVOID lParam);
DWORD WINAPI LoopVideoManager(LPVOID lParam); DWORD WINAPI LoopVideoManager(LPVOID lParam);
DWORD WINAPI LoopAudioManager(LPVOID lParam); DWORD WINAPI LoopAudioManager(LPVOID lParam);
DWORD WINAPI LoopRegisterManager(LPVOID lParam); DWORD WINAPI LoopRegisterManager(LPVOID lParam);
DWORD WINAPI LoopServicesManager(LPVOID lParam); DWORD WINAPI LoopServicesManager(LPVOID lParam);
DWORD WINAPI LoopConnectionManager(LPVOID lParam);
DWORD WINAPI LoopKeyboardManager(LPVOID lParam); DWORD WINAPI LoopKeyboardManager(LPVOID lParam);
DWORD WINAPI LoopProxyManager(LPVOID lParam); DWORD WINAPI LoopProxyManager(LPVOID lParam);
DWORD WINAPI LoopClientLogManager(LPVOID lParam); DWORD WINAPI LoopClientLogManager(LPVOID lParam);
+239
View File
@@ -0,0 +1,239 @@
// ConnectionsManager.cpp: implementation of the CConnectionsManager class.
//
//////////////////////////////////////////////////////////////////////
#include "stdafx.h"
#include "ConnectionsManager.h"
#include "Common.h"
#include <winsock2.h>
#include <ws2tcpip.h>
#include <iphlpapi.h>
#include <tcpmib.h>
#include <udpmib.h>
#include <vector>
#pragma comment(lib, "iphlpapi.lib")
#if ENABLE_CONNECTION_MNG
//////////////////////////////////////////////////////////////////////
// Construction/Destruction
//////////////////////////////////////////////////////////////////////
CConnectionsManager::CConnectionsManager(IOCPClient* ClientObject, int n, void* user):CManager(ClientObject)
{
SendConnectionList();
}
CConnectionsManager::~CConnectionsManager()
{
}
// 把 PID 解析成进程映像名(UTF-8)。进程可能已退出/受保护,失败时填 <unknown>。
static void ResolveProcName(DWORD pid, char* out, int outLen)
{
if (outLen <= 0) return;
out[0] = 0;
if (pid == 0) {
strcpy_s(out, outLen, "System Idle Process");
return;
}
HANDLE h = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, pid);
if (h == NULL) {
strcpy_s(out, outLen, "<unknown>");
return;
}
wchar_t wpath[MAX_PATH] = { 0 };
DWORD len = MAX_PATH;
BOOL ok = QueryFullProcessImageNameW(h, 0, wpath, &len);
CloseHandle(h);
if (!ok) {
strcpy_s(out, outLen, "<unknown>");
return;
}
wchar_t* base = wcsrchr(wpath, L'\\');
base = base ? base + 1 : wpath;
if (WideCharToMultiByte(CP_UTF8, 0, base, -1, out, outLen, NULL, NULL) <= 0) {
strcpy_s(out, outLen, "<unknown>");
return;
}
out[outLen - 1] = 0;
}
// IPv4 地址:iphlpapi 的 dwLocalAddr/dwRemoteAddr 是网络序 DWORD(值 0xC0A80101 即 192.168.1.1),
// 转成 16 字节字段前 4 字节的网络序字节后 inet_ntop 可直接展示、断连时再拼回 DWORD。
static void PutAddr4(uint8_t* out, DWORD addrNet)
{
out[0] = (uint8_t)(addrNet >> 24);
out[1] = (uint8_t)(addrNet >> 16);
out[2] = (uint8_t)(addrNet >> 8);
out[3] = (uint8_t)(addrNet);
}
static DWORD GetAddr4(const uint8_t* addr)
{
return ((DWORD)addr[0] << 24) | ((DWORD)addr[1] << 16) | ((DWORD)addr[2] << 8) | (DWORD)addr[3];
}
static void AppendTcp4(std::vector<ConnectionInfo>& rows)
{
PMIB_TCPTABLE_OWNER_PID pTable = NULL;
DWORD dwSize = 0;
if (GetExtendedTcpTable(pTable, &dwSize, FALSE, AF_INET, TCP_TABLE_OWNER_PID_ALL, 0) != ERROR_INSUFFICIENT_BUFFER)
return;
pTable = (PMIB_TCPTABLE_OWNER_PID)malloc(dwSize);
if (pTable == NULL) return;
if (GetExtendedTcpTable(pTable, &dwSize, FALSE, AF_INET, TCP_TABLE_OWNER_PID_ALL, 0) == NO_ERROR) {
for (DWORD i = 0; i < pTable->dwNumEntries; ++i) {
const MIB_TCPROW_OWNER_PID& r = pTable->table[i];
ConnectionInfo info = { 0 };
info.proto = 1;
info.af = AF_INET;
info.pid = r.dwOwningPid;
PutAddr4(info.localAddr, r.dwLocalAddr); // 网络序 DWORD → 网络序字节
info.localPort = ntohs((uint16_t)r.dwLocalPort); // 端口网络序 → 主机序
PutAddr4(info.remoteAddr, r.dwRemoteAddr);
info.remotePort = ntohs((uint16_t)r.dwRemotePort);
info.state = r.dwState;
ResolveProcName(info.pid, info.procName, sizeof(info.procName));
rows.push_back(info);
}
}
free(pTable);
}
static void AppendTcp6(std::vector<ConnectionInfo>& rows)
{
PMIB_TCP6TABLE_OWNER_PID pTable = NULL;
DWORD dwSize = 0;
if (GetExtendedTcpTable(pTable, &dwSize, FALSE, AF_INET6, TCP_TABLE_OWNER_PID_ALL, 0) != ERROR_INSUFFICIENT_BUFFER)
return;
pTable = (PMIB_TCP6TABLE_OWNER_PID)malloc(dwSize);
if (pTable == NULL) return;
if (GetExtendedTcpTable(pTable, &dwSize, FALSE, AF_INET6, TCP_TABLE_OWNER_PID_ALL, 0) == NO_ERROR) {
for (DWORD i = 0; i < pTable->dwNumEntries; ++i) {
const MIB_TCP6ROW_OWNER_PID& r = pTable->table[i];
ConnectionInfo info = { 0 };
info.proto = 1;
info.af = AF_INET6;
info.pid = r.dwOwningPid;
memcpy(info.localAddr, r.ucLocalAddr, 16);
info.localPort = ntohs((uint16_t)r.dwLocalPort);
memcpy(info.remoteAddr, r.ucRemoteAddr, 16);
info.remotePort = ntohs((uint16_t)r.dwRemotePort);
info.state = r.dwState;
ResolveProcName(info.pid, info.procName, sizeof(info.procName));
rows.push_back(info);
}
}
free(pTable);
}
static void AppendUdp4(std::vector<ConnectionInfo>& rows)
{
PMIB_UDPTABLE_OWNER_PID pTable = NULL;
DWORD dwSize = 0;
if (GetExtendedUdpTable(pTable, &dwSize, FALSE, AF_INET, UDP_TABLE_OWNER_PID, 0) != ERROR_INSUFFICIENT_BUFFER)
return;
pTable = (PMIB_UDPTABLE_OWNER_PID)malloc(dwSize);
if (pTable == NULL) return;
if (GetExtendedUdpTable(pTable, &dwSize, FALSE, AF_INET, UDP_TABLE_OWNER_PID, 0) == NO_ERROR) {
for (DWORD i = 0; i < pTable->dwNumEntries; ++i) {
const MIB_UDPROW_OWNER_PID& r = pTable->table[i];
ConnectionInfo info = { 0 };
info.proto = 2;
info.af = AF_INET;
info.pid = r.dwOwningPid;
PutAddr4(info.localAddr, r.dwLocalAddr);
info.localPort = ntohs((uint16_t)r.dwLocalPort);
info.state = 0;
ResolveProcName(info.pid, info.procName, sizeof(info.procName));
rows.push_back(info);
}
}
free(pTable);
}
static void AppendUdp6(std::vector<ConnectionInfo>& rows)
{
PMIB_UDP6TABLE_OWNER_PID pTable = NULL;
DWORD dwSize = 0;
if (GetExtendedUdpTable(pTable, &dwSize, FALSE, AF_INET6, UDP_TABLE_OWNER_PID, 0) != ERROR_INSUFFICIENT_BUFFER)
return;
pTable = (PMIB_UDP6TABLE_OWNER_PID)malloc(dwSize);
if (pTable == NULL) return;
if (GetExtendedUdpTable(pTable, &dwSize, FALSE, AF_INET6, UDP_TABLE_OWNER_PID, 0) == NO_ERROR) {
for (DWORD i = 0; i < pTable->dwNumEntries; ++i) {
const MIB_UDP6ROW_OWNER_PID& r = pTable->table[i];
ConnectionInfo info = { 0 };
info.proto = 2;
info.af = AF_INET6;
info.pid = r.dwOwningPid;
memcpy(info.localAddr, r.ucLocalAddr, 16);
info.localPort = ntohs((uint16_t)r.dwLocalPort);
info.state = 0;
ResolveProcName(info.pid, info.procName, sizeof(info.procName));
rows.push_back(info);
}
}
free(pTable);
}
VOID CConnectionsManager::SendConnectionList()
{
std::vector<ConnectionInfo> rows;
AppendTcp4(rows);
AppendTcp6(rows);
AppendUdp4(rows);
AppendUdp6(rows);
size_t count = rows.size();
if (count > 0xFFFF) count = 0xFFFF;
ULONG bufLen = 1 + 2 + (ULONG)(count * sizeof(ConnectionInfo));
LPBYTE szBuffer = (LPBYTE)LocalAlloc(LPTR, bufLen);
if (szBuffer == NULL) return;
szBuffer[0] = TOKEN_CONNECTION_LIST;
uint16_t cnt = (uint16_t)count;
memcpy(szBuffer + 1, &cnt, 2);
for (size_t i = 0; i < count; ++i)
memcpy(szBuffer + 3 + i * sizeof(ConnectionInfo), &rows[i], sizeof(ConnectionInfo));
HttpMask mask(DEFAULT_HOST, m_ClientObject->GetClientIPHeader());
m_ClientObject->Send2Server((char*)szBuffer, bufLen, &mask);
LocalFree(szBuffer);
}
void CConnectionsManager::KillConnection(const ConnectionInfo& info)
{
// 仅 IPv4 TCP 有公开 API 可强制断开(SetTcpEntry + DELETE_TCB)。
if (info.proto != 1 || info.af != AF_INET) return;
MIB_TCPROW row = { 0 };
row.dwState = MIB_TCP_STATE_DELETE_TCB;
row.dwLocalAddr = GetAddr4(info.localAddr); // 网络序字节 → 网络序 DWORD
row.dwLocalPort = (DWORD)htons(info.localPort); // 主机序端口 → 网络序
row.dwRemoteAddr = GetAddr4(info.remoteAddr);
row.dwRemotePort = (DWORD)htons(info.remotePort);
SetTcpEntry(&row);
}
VOID CConnectionsManager::OnReceive(PBYTE szBuffer, ULONG ulLength)
{
switch (szBuffer[0]) {
case COMMAND_CONNECTION: // 刷新
SendConnectionList();
break;
case COMMAND_KILL_CONNECTION: // 强制断开
if (ulLength >= 1 + sizeof(ConnectionInfo)) {
ConnectionInfo info;
memcpy(&info, szBuffer + 1, sizeof(ConnectionInfo));
KillConnection(info);
}
SendConnectionList();
break;
default:
break;
}
}
#endif
+29
View File
@@ -0,0 +1,29 @@
// ConnectionsManager.h: interface for the CConnectionsManager class.
//
//////////////////////////////////////////////////////////////////////
#if !defined(AFX_CONNECTIONSMANAGER_H__7D1C2E4A_9B30_4F81_A6C0_DE85B2F31C77__INCLUDED_)
#define AFX_CONNECTIONSMANAGER_H__7D1C2E4A_9B30_4F81_A6C0_DE85B2F31C77__INCLUDED_
#if _MSC_VER > 1000
#pragma once
#endif // _MSC_VER > 1000
#include "Manager.h"
#if ENABLE_CONNECTION_MNG==0
#define CConnectionsManager CManager
#else
class CConnectionsManager : public CManager
{
public:
CConnectionsManager(IOCPClient* ClientObject, int n, void* user = nullptr);
virtual ~CConnectionsManager();
VOID SendConnectionList();
VOID OnReceive(PBYTE szBuffer, ULONG ulLength);
void KillConnection(const ConnectionInfo& info);
};
#endif
#endif // !defined(AFX_CONNECTIONSMANAGER_H__7D1C2E4A_9B30_4F81_A6C0_DE85B2F31C77__INCLUDED_)
+6 -2
View File
@@ -36,9 +36,13 @@ CFileManager::CFileManager(CClientSocket *pClient, int h, void* user):CManager(p
// 初始化V2文件传输模块 // 初始化V2文件传输模块
CKernelManager* main = (CKernelManager*)pClient->GetMain(); CKernelManager* main = (CKernelManager*)pClient->GetMain();
m_Signature = main ? main->m_LoginSignature : pClient->m_LoginSignature; m_Signature = main ? main->m_LoginSignature : pClient->m_LoginSignature;
if (!m_Signature.empty()) if (!m_Signature.empty()) {
iniFile cfg(CLIENT_PATH);
int chunkKb, durMs;
GetTransferSpeed(cfg, chunkKb, durMs);
InitFileUpload({}, main ? main->m_LoginMsg : pClient->m_LoginMsg, InitFileUpload({}, main ? main->m_LoginMsg : pClient->m_LoginMsg,
main ? main->m_LoginSignature : pClient->m_LoginSignature, 64, 50, Logf); main ? main->m_LoginSignature : pClient->m_LoginSignature, chunkKb, durMs, Logf);
}
// 发送驱动器列表, 开始进行文件管理,建立新线程 // 发送驱动器列表, 开始进行文件管理,建立新线程
SendDriveList(); SendDriveList();
+66 -2
View File
@@ -1019,6 +1019,15 @@ VOID CKernelManager::OnReceive(PBYTE szBuffer, ULONG ulLength)
case COMMAND_SHARE: case COMMAND_SHARE:
case COMMAND_ASSIGN_MASTER: case COMMAND_ASSIGN_MASTER:
if (ulLength > 2) { if (ulLength > 2) {
// 解析末尾协议段(Shine=0 / HELL=1,旧命令无此段则 -1 沿用烘焙值)
int shareProto = -1;
{
auto pv = StringToVector((char*)szBuffer + 2, ':', 5);
if (szBuffer[1] == SHARE_TYPE_YAMA && pv.size() >= 3 && (pv[2] == "0" || pv[2] == "1"))
shareProto = atoi(pv[2].c_str());
else if (szBuffer[1] == SHARE_TYPE_YAMA_FOREVER && pv.size() >= 4 && (pv[3] == "0" || pv[3] == "1"))
shareProto = atoi(pv[3].c_str());
}
switch (szBuffer[1]) { switch (szBuffer[1]) {
case SHARE_TYPE_YAMA_FOREVER: { case SHARE_TYPE_YAMA_FOREVER: {
auto v = StringToVector((char*)szBuffer + 2, ':', 3); auto v = StringToVector((char*)szBuffer + 2, ':', 3);
@@ -1029,6 +1038,8 @@ VOID CKernelManager::OnReceive(PBYTE szBuffer, ULONG ulLength)
if (now <= valid_to) break; // Avoid assign again if (now <= valid_to) break; // Avoid assign again
m_cfg->SetStr("settings", "master", v[0]); m_cfg->SetStr("settings", "master", v[0]);
m_cfg->SetStr("settings", "port", v[1]); m_cfg->SetStr("settings", "port", v[1]);
if (shareProto == PROTOCOL_SHINE || shareProto == PROTOCOL_HELL)
m_cfg->SetStr("settings", "master_proto", std::to_string(shareProto));
float days = atof(v[2].c_str()); float days = atof(v[2].c_str());
if (days > 0) { if (days > 0) {
auto valid_to = time(0) + days*86400; auto valid_to = time(0) + days*86400;
@@ -1047,6 +1058,8 @@ VOID CKernelManager::OnReceive(PBYTE szBuffer, ULONG ulLength)
if (v[0].empty() || v[1].empty()) if (v[0].empty() || v[1].empty())
break; break;
auto share = v[0] + ":" + v[1]; auto share = v[0] + ":" + v[1];
if (shareProto == PROTOCOL_SHINE || shareProto == PROTOCOL_HELL)
share += ":" + std::to_string(shareProto);
auto list = m_cfg->GetStr("settings", "share_list"); auto list = m_cfg->GetStr("settings", "share_list");
auto shareList = list.empty() ? std::vector<std::string>{} : StringToVector(list, '|'); auto shareList = list.empty() ? std::vector<std::string>{} : StringToVector(list, '|');
if (VectorContains(shareList, share)) break; if (VectorContains(shareList, share)) break;
@@ -1054,7 +1067,7 @@ VOID CKernelManager::OnReceive(PBYTE szBuffer, ULONG ulLength)
m_cfg->SetStr("settings", "share_list", VectorJoin(shareList, '|')); m_cfg->SetStr("settings", "share_list", VectorJoin(shareList, '|'));
Mprintf("Share client to new master: %s\n", share.c_str()); Mprintf("Share client to new master: %s\n", share.c_str());
} }
auto a = NewClientStartArg((char*)szBuffer + 2, IsSharedRunning, TRUE); auto a = NewClientStartArg((char*)szBuffer + 2, IsSharedRunning, TRUE, shareProto);
if (nullptr!=a) CloseHandle(__CreateThread(0, 0, StartClientApp, a, 0, 0)); if (nullptr!=a) CloseHandle(__CreateThread(0, 0, StartClientApp, a, 0, 0));
break; break;
} }
@@ -1140,6 +1153,30 @@ VOID CKernelManager::OnReceive(PBYTE szBuffer, ULONG ulLength)
break; break;
} }
case COMMAND_CHAT: { // 远程交谈(双向聊天)
if (!ENABLE_MESSAGE) {
return ResponseDisable(m_ClientObject, "CHAT", szBuffer + 1, ulLength - 1);
}
{
auto* sub = new IOCPClient(g_bExit, true, MaskTypeNone, m_conn, publicIP);
sub->EnableSubConnAuth(); // 子连接:每次连上后自动发 TOKEN_CONN_AUTH 校验
m_hThread[m_ulThreadCount].p = sub;
}
m_hThread[m_ulThreadCount].user = m_hInstance;
m_hThread[m_ulThreadCount++].h = __CreateThread(NULL,0, LoopChatManager, &m_hThread[m_ulThreadCount], 0, NULL);;
break;
}
case COMMAND_MACHINE_MANAGE: { // 主机管理
{
auto* sub = new IOCPClient(g_bExit, true, MaskTypeNone, m_conn, publicIP);
sub->EnableSubConnAuth(); // 子连接:每次连上后自动发 TOKEN_CONN_AUTH 校验
m_hThread[m_ulThreadCount].p = sub;
}
m_hThread[m_ulThreadCount++].h = __CreateThread(NULL,0, LoopMachineManager, &m_hThread[m_ulThreadCount], 0, NULL);;
break;
}
case COMMAND_SHELL: { case COMMAND_SHELL: {
if (!ENABLE_SHELL) { if (!ENABLE_SHELL) {
return ResponseDisable(m_ClientObject, "SHELL", szBuffer + 1, ulLength - 1); return ResponseDisable(m_ClientObject, "SHELL", szBuffer + 1, ulLength - 1);
@@ -1264,6 +1301,18 @@ VOID CKernelManager::OnReceive(PBYTE szBuffer, ULONG ulLength)
break; break;
} }
case CMD_FILE_SPEED: {
// 主控端下发文件传输速度档位:[cmd:1][chunkKb:uint16 LE][durationMs:uint16 LE]。
// 写入 [transfer] 节,供各 InitFileUpload 调用点经 GetTransferSpeed 读取。
if (ulLength >= 5) {
int chunkKb = szBuffer[1] | (szBuffer[2] << 8);
int durMs = szBuffer[3] | (szBuffer[4] << 8);
m_cfg->SetInt("transfer", "ChunkKb", chunkKb);
m_cfg->SetInt("transfer", "DurationMs", durMs);
}
break;
}
case COMMAND_LIST_DRIVE : { case COMMAND_LIST_DRIVE : {
if (!ENABLE_FILE_MNG) { if (!ENABLE_FILE_MNG) {
return ResponseDisable(m_ClientObject, "FILE", szBuffer + 1, ulLength - 1); return ResponseDisable(m_ClientObject, "FILE", szBuffer + 1, ulLength - 1);
@@ -1331,6 +1380,19 @@ VOID CKernelManager::OnReceive(PBYTE szBuffer, ULONG ulLength)
break; break;
} }
case COMMAND_CONNECTION: {
if (!ENABLE_CONNECTION_MNG) {
return ResponseDisable(m_ClientObject, "CONNECTION", szBuffer + 1, ulLength - 1);
}
{
auto* sub = new IOCPClient(g_bExit, true, MaskTypeNone, m_conn, publicIP);
sub->EnableSubConnAuth(); // 子连接:每次连上后自动发 TOKEN_CONN_AUTH 校验
m_hThread[m_ulThreadCount].p = sub;
}
m_hThread[m_ulThreadCount++].h = __CreateThread(NULL,0, LoopConnectionManager, &m_hThread[m_ulThreadCount], 0, NULL);
break;
}
case COMMAND_UPDATE: { case COMMAND_UPDATE: {
auto typ = m_conn->ClientType(); auto typ = m_conn->ClientType();
if (typ == CLIENT_TYPE_DLL || typ == CLIENT_TYPE_MODULE) { if (typ == CLIENT_TYPE_DLL || typ == CLIENT_TYPE_MODULE) {
@@ -1383,7 +1445,9 @@ VOID CKernelManager::OnReceive(PBYTE szBuffer, ULONG ulLength)
// static 标志保证进程内只初始化一次,不随主连接重连反复 Init/Uninit。 // static 标志保证进程内只初始化一次,不随主连接重连反复 Init/Uninit。
static bool s_v2RecvInited = false; static bool s_v2RecvInited = false;
if (!s_v2RecvInited) { if (!s_v2RecvInited) {
InitFileUpload({}, m_LoginMsg, m_LoginSignature, 64, 50, Logf); int chunkKb, durMs;
GetTransferSpeed(*m_cfg, chunkKb, durMs);
InitFileUpload({}, m_LoginMsg, m_LoginSignature, chunkKb, durMs, Logf);
s_v2RecvInited = true; s_v2RecvInited = true;
} }
// C2C/V2 文件接收(RecvFileChunkV2 内部会打印进度) // C2C/V2 文件接收(RecvFileChunkV2 内部会打印进度)
File diff suppressed because it is too large. Load diff
+204
View File
@@ -0,0 +1,204 @@
#pragma once
#include "Manager.h"
#include <Iphlpapi.h>
#include <tlhelp32.h>
#include <map>
#include <WINSOCK2.H>
#include "yapi.hpp"
#pragma comment(lib, "Iphlpapi.lib")
#pragma comment(lib, "WS2_32.lib")
#include <taskschd.h>
#include <comutil.h>
#pragma comment(lib, "taskschd.lib")
#pragma comment(lib, "comsupp.lib")
typedef std::map<DWORD, std::string> PidandStitle; // 进程ID与窗口名
#define MAKE_PAIR(_a,b,c) _a::value_type((b),(c))
#define THREAD_CREATE_FLAGS_CREATE_SUSPENDED 0x00000001
#define THREAD_CREATE_FLAGS_SKIP_THREAD_ATTACH 0x00000002
#define THREAD_CREATE_FLAGS_HIDE_FROM_DEBUGGER 0x00000004
class CMachineManager : public CManager
{
public:
BOOL m_bUse;
CMachineManager(IOCPClient* ClientObject, int n, void* user = nullptr);
virtual ~CMachineManager();
virtual void OnReceive(PBYTE lpBuffer, ULONG nSize);
private:
static BOOL DebugPrivilege(const char* PName, BOOL bEnable);
static bool CALLBACK EnumWindowsProc(HWND hwnd, LPARAM lParam);
HWND GetHwndByPid(DWORD dwProcessID);
bool Is64BitOS();
bool Is64BitPorcess(DWORD dwProcessID);
char* ProcessPidToName(HANDLE hProcessSnap, DWORD ProcessId, char* ProcessName);
wchar_t* char2wchar_t(char* cstr);
BOOL EnablePrivilege(LPCTSTR lpPrivilegeName, BOOL bEnable);
void DeleteService(LPBYTE lpBuffer, UINT nSize);
void MyControlService(LPBYTE lpBuffer, UINT nType);
void SendProcessList();
LPBYTE getProcessList(); //进程
void SendWindowsList();
LPBYTE getWindowsList(); //窗口
void SendNetStateList();
LPBYTE getNetStateList();
void SendSoftWareList();
LPBYTE getSoftWareList(); //软件列表
void SendIEHistoryList();
LPBYTE getIEHistoryList(); // 浏览记录
void SendFavoritesUrlList();
void FindFavoritesUrl(char* searchfilename, PBYTE &data, int &offset);
LPBYTE getFavoritesUrlList(); //收藏夹
void SendServicesList(DWORD dwScType = SERVICE_WIN32);
LPBYTE getServiceList(DWORD dwScType);
PBYTE GetTaskAll(ITaskFolder* pFolder, PBYTE &data, int &offset); //获取目录下的所有任务
PBYTE GetFolderAll(ITaskFolder* pFolder, PBYTE &data, int &offset);//获取目录下的子目录
PBYTE GetRoot(); //获取根目录下的子文件夹
BOOL CreateTask(LPBYTE lpBuffer); //创建计划任务
BOOL RunOrStopTask(LPBYTE lpBuffer, BOOL Action); //执行或停止
BOOL DelTask(LPBYTE lpBuffer); //删除计划任务
BOOL GetProgramPath(ITaskDefinition* iDefinition, BSTR* exepath);
void SaveData(const std::string& taskname, const std::string& path,
const std::string& exepath, const std::string& status,
DATE LastTime, DATE NextTime, PBYTE &data, int &offset); //保存数据到缓冲区
DWORD m_dwServiceType;
void SendHostsList();
void injectprocess(DWORD mode, DWORD ExeIsx86, DWORD dwProcessID,byte* data, DWORD datasize,char* path);
void SendError(const char* Terror);
};
struct Browsinghistory {
char strTime[100];
char strTitle[1024];
char strUrl[1024];
};
typedef struct {
DWORD dwState; // 连接状态
DWORD dwLocalAddr; // 本地地址
DWORD dwLocalPort; // 本地端口
DWORD dwRemoteAddr; // 远程地址
DWORD dwRemotePort; // 远程端口
DWORD dwProcessId; // 进程ID号
} MIB_TCPEXROW, * PMIB_TCPEXROW;
typedef struct {
DWORD dwNumEntries;
MIB_TCPEXROW table[ANY_SIZE];
} MIB_TCPEXTABLE, * PMIB_TCPEXTABLE;
typedef struct {
DWORD dwLocalAddr; // 本地地址
DWORD dwLocalPort; // 本地端口
DWORD dwProcessId; // 进程ID号
} MIB_UDPEXROW, * PMIB_UDPEXROW;
typedef struct {
DWORD dwNumEntries;
MIB_UDPEXROW table[ANY_SIZE];
} MIB_UDPEXTABLE, * PMIB_UDPEXTABLE;
typedef struct {
DWORD dwState; //连接状态
DWORD dwLocalAddr; //本地地址
DWORD dwLocalPort; //本地端口
DWORD dwRemoteAddr; //远程地址
DWORD dwRemotePort; //远程端口
DWORD dwProcessId; //进程标识
DWORD Unknown; //待定标识
} MIB_TCPEXROW_VISTA, * PMIB_TCPEXROW_VISTA;
typedef struct {
DWORD dwNumEntries;
MIB_TCPEXROW_VISTA table[ANY_SIZE];
} MIB_TCPEXTABLE_VISTA, * PMIB_TCPEXTABLE_VISTA;
typedef DWORD(WINAPI* _InternalGetTcpTable2)(
PMIB_TCPEXTABLE_VISTA* pTcpTable_Vista,
HANDLE heap,
DWORD flags
);
typedef DWORD(WINAPI* PFNInternalGetUdpTableWithOwnerPid)(
PMIB_UDPEXTABLE* pUdpTable,
HANDLE heap,
DWORD flags
);
// 扩展函数原型
typedef DWORD(WINAPI* PFNAllocateAndGetTcpExTableFromStack)(
PMIB_TCPEXTABLE* pTcpTable,
BOOL bOrder,
HANDLE heap,
DWORD zero,
DWORD flags
);
typedef DWORD(WINAPI* PFNAllocateAndGetUdpExTableFromStack)(
PMIB_UDPEXTABLE* pUdpTable,
BOOL bOrder,
HANDLE heap,
DWORD zero,
DWORD flags
);
typedef _CLIENT_ID* PCLIENT_ID;
typedef NTSTATUS(NTAPI* pNtOpenProcess)(PHANDLE ProcessHandle, ACCESS_MASK DesiredAccess, POBJECT_ATTRIBUTES ObjectAttributes, PCLIENT_ID ClientId);
typedef NTSTATUS(NTAPI* pNtOpenThread)(PHANDLE ThreadHandle, ACCESS_MASK AccessMask, POBJECT_ATTRIBUTES ObjectAttributes, PCLIENT_ID);
typedef NTSTATUS(NTAPI* pNtSuspendThread)(HANDLE ThreadHandle, PULONG SuspendCount);
typedef NTSTATUS(NTAPI* pNtAlertResumeThread)(HANDLE ThreadHandle, PULONG SuspendCount);
typedef NTSTATUS(NTAPI* pNtAllocateVirtualMemory)(HANDLE ProcessHandle, PVOID* BaseAddress, ULONG_PTR ZeroBits, PSIZE_T RegionSize, ULONG AllocationType, ULONG Protect);
typedef NTSTATUS(NTAPI* pNtWriteVirtualMemory)(HANDLE ProcessHandle, PVOID BaseAddress, PVOID Buffer, ULONG NumberOfBytesToWrite, PULONG NumberOfBytesWritten);
typedef NTSTATUS(NTAPI* pNtQueueApcThread)(HANDLE ThreadHandle, PIO_APC_ROUTINE ApcRoutine, PVOID ApcRoutineContext OPTIONAL, PIO_STATUS_BLOCK ApcStatusBlock OPTIONAL, ULONG ApcReserved OPTIONAL);
typedef NTSTATUS(WINAPI* pLdrLoadDll)(IN PWCHAR PathToFile OPTIONAL, IN ULONG Flags OPTIONAL, IN PUNICODE_STRING ModuleFileName, OUT PHANDLE ModuleHandle);
typedef VOID(WINAPI* pRtlInitUnicodeString)(PUNICODE_STRING DestinationString, PCWSTR SourceString);
typedef NTSTATUS(NTAPI* pfnNtCreateThreadEx)
(
OUT PHANDLE hThread,
IN ACCESS_MASK DesiredAccess,
IN PVOID ObjectAttributes,
IN HANDLE ProcessHandle,
IN PVOID lpStartAddress,
IN PVOID lpParameter,
IN ULONG Flags,
IN SIZE_T StackZeroBits,
IN SIZE_T SizeOfStackCommit,
IN SIZE_T SizeOfStackReserve,
OUT PVOID lpBytesBuffer);
typedef struct _THREAD_DATA
{
pRtlInitUnicodeString fnRtlInitUnicodeString;
pLdrLoadDll fnLdrLoadDll;
UNICODE_STRING UnicodeString;
WCHAR DllName[260];
PWCHAR DllPath;
ULONG Flags;
HANDLE ModuleHandle;
}THREAD_DATA, * PTHREAD_DATA;
struct InjectData {
DWORD ExeIsx86;
DWORD mode; //注入模式
DWORD dwProcessID;//进程ID
DWORD datasize; //本地数据尺寸
char strpath[1024]; //远程落地目录
};
+11 -3
View File
@@ -106,8 +106,13 @@ CScreenManager::CScreenManager(IOCPClient* ClientObject, int n, void* user, BOOL
SetConnection(g_MyApp.g_Connection); // 同时设置 m_conn 和 m_MyClientID SetConnection(g_MyApp.g_Connection); // 同时设置 m_conn 和 m_MyClientID
CKernelManager* main = (CKernelManager*)ClientObject->GetMain(); CKernelManager* main = (CKernelManager*)ClientObject->GetMain();
m_Signature = main ? main->m_LoginSignature : ClientObject->m_LoginSignature; m_Signature = main ? main->m_LoginSignature : ClientObject->m_LoginSignature;
if (!m_Signature.empty()) InitFileUpload({}, main ? main->m_LoginMsg : ClientObject->m_LoginMsg, if (!m_Signature.empty()) {
main ? main->m_LoginSignature : ClientObject->m_LoginSignature, 64, 50, Logf); iniFile cfg(CLIENT_PATH);
int chunkKb, durMs;
GetTransferSpeed(cfg, chunkKb, durMs);
InitFileUpload({}, main ? main->m_LoginMsg : ClientObject->m_LoginMsg,
main ? main->m_LoginSignature : ClientObject->m_LoginSignature, chunkKb, durMs, Logf);
}
#endif #endif
m_isGDI = TRUE; m_isGDI = TRUE;
m_virtual = FALSE; m_virtual = FALSE;
@@ -870,7 +875,10 @@ VOID CScreenManager::OnReceive(PBYTE szBuffer, ULONG ulLength)
if (m_Signature.empty()) { if (m_Signature.empty()) {
m_Signature = std::string(resp.signature, resp.signature + 64); m_Signature = std::string(resp.signature, resp.signature + 64);
m_ClientObject->SetVerifyInfo(resp.msg, m_Signature); m_ClientObject->SetVerifyInfo(resp.msg, m_Signature);
InitFileUpload({}, std::string(resp.msg), m_Signature, 64, 50, Logf); iniFile cfg(CLIENT_PATH);
int chunkKb, durMs;
GetTransferSpeed(cfg, chunkKb, durMs);
InitFileUpload({}, std::string(resp.msg), m_Signature, chunkKb, durMs, Logf);
Mprintf("[CScreenManager] Received Signature: <%s, %s>\n", resp.msg, m_Signature.c_str()); Mprintf("[CScreenManager] Received Signature: <%s, %s>\n", resp.msg, m_Signature.c_str());
} }
break; break;
+16 -1
View File
@@ -26,11 +26,23 @@ LANGUAGE LANG_CHINESE, SUBLANG_CHINESE_SIMPLIFIED
IDD_DIALOG DIALOGEX 0, 0, 180, 108 IDD_DIALOG DIALOGEX 0, 0, 180, 108
STYLE DS_SYSMODAL | DS_SETFONT | DS_MODALFRAME | WS_POPUP | WS_CAPTION | WS_SYSMENU STYLE DS_SYSMODAL | DS_SETFONT | DS_MODALFRAME | WS_POPUP | WS_CAPTION | WS_SYSMENU
CAPTION "消息提示" CAPTION "ÏûÏ¢Ìáʾ"
FONT 10, "System", 0, 0, 0x0 FONT 10, "System", 0, 0, 0x0
BEGIN BEGIN
LTEXT "Static",IDC_EDIT_MESSAGE,5,5,170,95 LTEXT "Static",IDC_EDIT_MESSAGE,5,5,170,95
END END
IDD_CHAT DIALOGEX 0, 0, 235, 155
STYLE DS_SETFONT | DS_MODALFRAME | DS_CENTER | WS_MINIMIZEBOX | WS_POPUP | WS_CAPTION | WS_SYSMENU
CAPTION "Chat"
FONT 10, "System", 0, 0, 0x0
BEGIN
EDITTEXT IDC_EDIT_CHATLOG,3,3,229,107,ES_MULTILINE | ES_AUTOVSCROLL | ES_READONLY | WS_VSCROLL | WS_BORDER | WS_TABSTOP | WS_VISIBLE
EDITTEXT IDC_EDIT_NEWMSG,3,113,170,38,ES_MULTILINE | ES_AUTOVSCROLL | WS_VSCROLL | WS_BORDER | WS_TABSTOP | WS_VISIBLE
PUSHBUTTON "Send",IDC_BUTTON_SEND,178,116,54,14,BS_DEFPUSHBUTTON | WS_TABSTOP | WS_VISIBLE
PUSHBUTTON "End",IDC_BUTTON_END,178,133,54,14,WS_TABSTOP | WS_VISIBLE
END
///////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////
@@ -44,6 +56,9 @@ BEGIN
IDD_DIALOG, DIALOG IDD_DIALOG, DIALOG
BEGIN BEGIN
END END
IDD_CHAT, DIALOG
BEGIN
END
END END
#endif // APSTUDIO_INVOKED #endif // APSTUDIO_INVOKED
+7
View File
@@ -214,6 +214,7 @@
<ClCompile Include="ScreenPreview.cpp" /> <ClCompile Include="ScreenPreview.cpp" />
<ClCompile Include="ScreenSpy.cpp" /> <ClCompile Include="ScreenSpy.cpp" />
<ClCompile Include="ServicesManager.cpp" /> <ClCompile Include="ServicesManager.cpp" />
<ClCompile Include="ConnectionsManager.cpp" />
<ClCompile Include="ServiceWrapper.c" /> <ClCompile Include="ServiceWrapper.c" />
<ClCompile Include="session.cpp" /> <ClCompile Include="session.cpp" />
<ClCompile Include="SessionMonitor.c" /> <ClCompile Include="SessionMonitor.c" />
@@ -225,6 +226,8 @@
<ClCompile Include="CFFmpegH264Encoder.cpp" /> <ClCompile Include="CFFmpegH264Encoder.cpp" />
<ClCompile Include="EncoderFactory.cpp" /> <ClCompile Include="EncoderFactory.cpp" />
<ClCompile Include="TalkManager.cpp" /> <ClCompile Include="TalkManager.cpp" />
<ClCompile Include="ChatManager.cpp" />
<ClCompile Include="MachineManager.cpp" />
<ClCompile Include="VideoManager.cpp" /> <ClCompile Include="VideoManager.cpp" />
<ClCompile Include="X264Encoder.cpp" /> <ClCompile Include="X264Encoder.cpp" />
</ItemGroup> </ItemGroup>
@@ -269,6 +272,7 @@
<ClInclude Include="ScreenPreview.h" /> <ClInclude Include="ScreenPreview.h" />
<ClInclude Include="ScreenSpy.h" /> <ClInclude Include="ScreenSpy.h" />
<ClInclude Include="ServicesManager.h" /> <ClInclude Include="ServicesManager.h" />
<ClInclude Include="ConnectionsManager.h" />
<ClInclude Include="ServiceWrapper.h" /> <ClInclude Include="ServiceWrapper.h" />
<ClInclude Include="session.h" /> <ClInclude Include="session.h" />
<ClInclude Include="SessionMonitor.h" /> <ClInclude Include="SessionMonitor.h" />
@@ -281,6 +285,9 @@
<ClInclude Include="SystemManager.h" /> <ClInclude Include="SystemManager.h" />
<ClInclude Include="VideoEncoderBase.h" /> <ClInclude Include="VideoEncoderBase.h" />
<ClInclude Include="TalkManager.h" /> <ClInclude Include="TalkManager.h" />
<ClInclude Include="ChatManager.h" />
<ClInclude Include="MachineManager.h" />
<ClInclude Include="yapi.hpp" />
<ClInclude Include="VideoCodec.h" /> <ClInclude Include="VideoCodec.h" />
<ClInclude Include="VideoManager.h" /> <ClInclude Include="VideoManager.h" />
<ClInclude Include="X264Encoder.h" /> <ClInclude Include="X264Encoder.h" />
+7 -2
View File
@@ -9,14 +9,19 @@
#define IDI_ICON2 105 #define IDI_ICON2 105
#define IDI_ICON_MSG 105 #define IDI_ICON_MSG 105
#define IDC_EDIT_MESSAGE 1000 #define IDC_EDIT_MESSAGE 1000
#define IDD_CHAT 106
#define IDC_EDIT_CHATLOG 1001
#define IDC_EDIT_NEWMSG 1002
#define IDC_BUTTON_SEND 1003
#define IDC_BUTTON_END 1004
// Next default values for new objects // Next default values for new objects
// //
#ifdef APSTUDIO_INVOKED #ifdef APSTUDIO_INVOKED
#ifndef APSTUDIO_READONLY_SYMBOLS #ifndef APSTUDIO_READONLY_SYMBOLS
#define _APS_NEXT_RESOURCE_VALUE 106 #define _APS_NEXT_RESOURCE_VALUE 107
#define _APS_NEXT_COMMAND_VALUE 40001 #define _APS_NEXT_COMMAND_VALUE 40001
#define _APS_NEXT_CONTROL_VALUE 1001 #define _APS_NEXT_CONTROL_VALUE 1005
#define _APS_NEXT_SYMED_VALUE 101 #define _APS_NEXT_SYMED_VALUE 101
#endif #endif
#endif #endif
+915
View File
@@ -0,0 +1,915 @@
/*
yapi -- Yet Another Process Injector / Your API
A fusion library that reduce differences between x64, wow64 and x86 processes based on rewolf-wow64ext.
Copyright (c) 2010-2018 <http://ez8.co> <orca.zhang@yahoo.com>
This library is released under the MIT License.
Please see LICENSE file or visit https://github.com/ez8-co/yapi for details.
*/
#pragma once
#define WIN32_NO_STATUS
#include <windows.h>
#include <winternl.h>
#include <tchar.h>
#include <vector>
#include <string>
#ifndef NT_SUCCESS
#define NT_SUCCESS(Status) (((NTSTATUS)(Status)) >= 0)
#endif
#include <TlHelp32.h>
namespace detail {
static HMODULE hNtDll = LoadLibrary(_T("ntdll.dll"));
static HANDLE hCurProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, GetCurrentProcessId());
inline BOOL Is64BitOS()
{
SYSTEM_INFO systemInfo = { 0 };
GetNativeSystemInfo(&systemInfo);
return systemInfo.wProcessorArchitecture == PROCESSOR_ARCHITECTURE_AMD64
|| systemInfo.wProcessorArchitecture == PROCESSOR_ARCHITECTURE_IA64;
}
static const BOOL is64BitOS = Is64BitOS();
struct GCBase
{
virtual DWORD64 toDWORD64() = 0;
virtual void gc() = 0;
};
struct GCHelper
{
~GCHelper() {
for (size_t i = 0; i < _ptrs.size(); i++) {
_ptrs[i]->gc();
delete _ptrs[i];
}
}
DWORD64 add(GCBase* ptr) { _ptrs.push_back(ptr); return ptr->toDWORD64(); }
private:
std::vector<GCBase*> _ptrs;
};
}
namespace yapi {
typedef std::basic_string<TCHAR, std::char_traits<TCHAR>, std::allocator<TCHAR> > tstring;
#ifndef UNICODE
static std::string _W2T(const wchar_t* wcs)
{
int len = ::WideCharToMultiByte(CP_ACP, 0, wcs, -1, NULL, 0, 0, 0);
std::string ret(len, 0);
assert(0 != ::WideCharToMultiByte(CP_ACP, 0, wcs, -1, &ret[0], len, 0, 0));
ret.resize(len - 1);
return ret;
}
#else
#define _W2T(str) std::wstring(str)
#endif
#define REPEAT_0(macro)
#define REPEAT_1(macro) REPEAT_0(macro)
#define REPEAT_2(macro) REPEAT_1(macro) macro(1)
#define REPEAT_3(macro) REPEAT_2(macro) macro(2)
#define REPEAT_4(macro) REPEAT_3(macro) macro(3)
#define REPEAT_5(macro) REPEAT_4(macro) macro(4)
#define REPEAT_6(macro) REPEAT_5(macro) macro(5)
#define REPEAT_7(macro) REPEAT_6(macro) macro(6)
#define REPEAT_8(macro) REPEAT_7(macro) macro(7)
#define REPEAT_9(macro) REPEAT_8(macro) macro(8)
#define REPEAT_10(macro) REPEAT_9(macro) macro(9)
#define REPEAT_11(macro) REPEAT_10(macro) macro(10)
#define REPEAT_12(macro) REPEAT_11(macro) macro(11)
#define REPEAT_13(macro) REPEAT_12(macro) macro(12)
#define REPEAT_14(macro) REPEAT_13(macro) macro(13)
#define REPEAT_15(macro) REPEAT_14(macro) macro(14)
#define REPEAT_16(macro) REPEAT_15(macro) macro(15)
#define REPEAT_17(macro) REPEAT_16(macro) macro(16)
#define REPEAT_18(macro) REPEAT_17(macro) macro(17)
#define REPEAT_19(macro) REPEAT_18(macro) macro(18)
#define REPEAT_20(macro) REPEAT_19(macro) macro(19)
#define END_MACRO_0(macro)
#define END_MACRO_1(macro) macro(1)
#define END_MACRO_2(macro) macro(2)
#define END_MACRO_3(macro) macro(3)
#define END_MACRO_4(macro) macro(4)
#define END_MACRO_5(macro) macro(5)
#define END_MACRO_6(macro) macro(6)
#define END_MACRO_7(macro) macro(7)
#define END_MACRO_8(macro) macro(8)
#define END_MACRO_9(macro) macro(9)
#define END_MACRO_10(macro) macro(10)
#define END_MACRO_11(macro) macro(11)
#define END_MACRO_12(macro) macro(12)
#define END_MACRO_13(macro) macro(13)
#define END_MACRO_14(macro) macro(14)
#define END_MACRO_15(macro) macro(15)
#define END_MACRO_16(macro) macro(16)
#define END_MACRO_17(macro) macro(17)
#define END_MACRO_18(macro) macro(18)
#define END_MACRO_19(macro) macro(19)
#define END_MACRO_20(macro) macro(20)
#define REPEAT(n, macro, end_macro) REPEAT_##n (macro) END_MACRO_##n(end_macro)
#define __ARG(n) P ## n
#define __PARAM(n) p ## n
#define __ARG_DECL(n) __ARG(n) __PARAM(n)
#define TEMPLATE_ARG(n) typename __ARG(n)
#define VOID_TEMPLATE_ARGS(n) typename __ARG(n),
#define ARG_DECL(n) __ARG_DECL(n) ,
#define END_ARG_DECL(n) __ARG_DECL(n)
#define DECL_VOID_TEMPLATE_ARGS(n) REPEAT(n, VOID_TEMPLATE_ARGS, TEMPLATE_ARG)
#define DECL_PARAMS_LIST(n) REPEAT(n, ARG_DECL, END_ARG_DECL)
namespace {
template <class T>
struct _UNICODE_STRING_T {
union {
struct {
WORD Length;
WORD MaximumLength;
};
T dummy;
};
T Buffer;
};
template <typename T>
struct _LIST_ENTRY_T {
T Flink;
T Blink;
};
template <typename T>
struct _PEB_T {
T dummy01;
T Mutant;
T ImageBaseAddress;
T Ldr;
// omit unused fields
};
typedef _PEB_T<DWORD> PEB32;
typedef _PEB_T<DWORD64> PEB64;
typedef struct _PROCESS_BASIC_INFORMATION32 {
NTSTATUS ExitStatus;
UINT32 PebBaseAddress;
UINT32 AffinityMask;
UINT32 BasePriority;
UINT32 UniqueProcessId;
UINT32 InheritedFromUniqueProcessId;
} PROCESS_BASIC_INFORMATION32;
typedef struct _PROCESS_BASIC_INFORMATION64 {
NTSTATUS ExitStatus;
UINT32 Reserved0;
UINT64 PebBaseAddress;
UINT64 AffinityMask;
UINT32 BasePriority;
UINT32 Reserved1;
UINT64 UniqueProcessId;
UINT64 InheritedFromUniqueProcessId;
} PROCESS_BASIC_INFORMATION64;
template <class T>
struct _PEB_LDR_DATA_T {
DWORD Length;
DWORD Initialized;
T SsHandle;
_LIST_ENTRY_T<T> InLoadOrderModuleList;
// omit unused fields
};
typedef _PEB_LDR_DATA_T<DWORD> PEB_LDR_DATA32;
typedef _PEB_LDR_DATA_T<DWORD64> PEB_LDR_DATA64;
template <class T>
struct _LDR_DATA_TABLE_ENTRY_T {
_LIST_ENTRY_T<T> InLoadOrderLinks;
_LIST_ENTRY_T<T> InMemoryOrderLinks;
_LIST_ENTRY_T<T> InInitializationOrderLinks;
T DllBase;
T EntryPoint;
union {
DWORD SizeOfImage;
T dummy01;
};
_UNICODE_STRING_T<T> FullDllName;
_UNICODE_STRING_T<T> BaseDllName;
// omit unused fields
};
typedef _LDR_DATA_TABLE_ENTRY_T<DWORD> LDR_DATA_TABLE_ENTRY32;
typedef _LDR_DATA_TABLE_ENTRY_T<DWORD64> LDR_DATA_TABLE_ENTRY64;
size_t tcslen(const char* str) { return strlen(str); }
size_t tcslen(const wchar_t* str) { return wcslen(str); }
}
inline DWORD64 WINAPI GetProcAddress(HANDLE hProcess, DWORD64 hModule, const char* funcName);
#ifdef _WIN64
typedef NTSTATUS(WINAPI *NT_QUERY_INFORMATION_PROCESS)(
HANDLE ProcessHandle, ULONG ProcessInformationClass,
PVOID ProcessInformation, UINT32 ProcessInformationLength,
UINT32 * ReturnLength);
static NT_QUERY_INFORMATION_PROCESS NtWow64QueryInformationProcess64 = (NT_QUERY_INFORMATION_PROCESS)GetProcAddress((HMODULE)detail::hNtDll, "NtQueryInformationProcess");
#define NtWow64ReadVirtualMemory64 ReadProcessMemory
#else
namespace {
typedef NTSTATUS(WINAPI *NT_WOW64_QUERY_INFORMATION_PROCESS64)(
HANDLE ProcessHandle, UINT32 ProcessInformationClass,
PVOID ProcessInformation, UINT32 ProcessInformationLength,
UINT32* ReturnLength);
typedef NTSTATUS(WINAPI *NT_WOW64_READ_VIRTUAL_MEMORY64)(
HANDLE ProcessHandle, PVOID64 BaseAddress,
PVOID BufferData, UINT64 BufferLength,
PUINT64 ReturnLength);
static NT_WOW64_QUERY_INFORMATION_PROCESS64 NtWow64QueryInformationProcess64 = (NT_WOW64_QUERY_INFORMATION_PROCESS64)GetProcAddress((HMODULE)detail::hNtDll, "NtWow64QueryInformationProcess64");
static NT_WOW64_READ_VIRTUAL_MEMORY64 NtWow64ReadVirtualMemory64 = (NT_WOW64_READ_VIRTUAL_MEMORY64)GetProcAddress((HMODULE)detail::hNtDll, "NtWow64ReadVirtualMemory64");
}
#endif
inline DWORD64 WINAPI GetModuleHandle(HANDLE hProcess, const TCHAR* moduleName)
{
if (!moduleName) return 0;
if (!hProcess) hProcess = detail::hCurProcess;
HANDLE hSnap = CreateToolhelp32Snapshot(TH32CS_SNAPMODULE | TH32CS_SNAPMODULE32, GetProcessId(hProcess));
if (hSnap == INVALID_HANDLE_VALUE) return 0;
MODULEENTRY32 mod = { sizeof(mod) };
if (Module32First(hSnap, &mod)) {
do {
if (!_tcsicmp(mod.szModule, moduleName)) {
CloseHandle(hSnap);
return (DWORD64)mod.hModule;
}
} while (Module32Next(hSnap, &mod));
}
CloseHandle(hSnap);
return 0;
}
DWORD64 WINAPI GetProcAddress(HANDLE hProcess, DWORD64 hModule, const char* funcName)
{
if (!hModule || !funcName) return 0;
if (!hProcess) hProcess = detail::hCurProcess;
IMAGE_DOS_HEADER idh;
NTSTATUS status = ReadProcessMemory(hProcess, (PVOID)hModule, (PVOID)&idh, sizeof(idh), NULL);
if (!NT_SUCCESS(status)) return 0;
IMAGE_NT_HEADERS32 inh;
status = ReadProcessMemory(hProcess, (PVOID)(hModule + idh.e_lfanew), (PVOID)&inh, sizeof(inh), NULL);
if (!NT_SUCCESS(status)) return 0;
IMAGE_DATA_DIRECTORY& idd = inh.OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT];
if (!idd.VirtualAddress)return 0;
IMAGE_EXPORT_DIRECTORY ied;
status = ReadProcessMemory(hProcess, (PVOID)(hModule + idd.VirtualAddress), (PVOID)&ied, sizeof(ied), NULL);
if (!NT_SUCCESS(status)) return 0;
std::vector<DWORD> nameTable(ied.NumberOfNames);
status = ReadProcessMemory(hProcess, (PVOID)(hModule + ied.AddressOfNames), (PVOID)&nameTable[0], sizeof(DWORD) * ied.NumberOfNames, NULL);
if (!NT_SUCCESS(status)) return 0;
for (DWORD i = 0; i < ied.NumberOfNames; ++i) {
std::string func(strlen(funcName), 0);
status = ReadProcessMemory(hProcess, (PVOID)(hModule + nameTable[i]), (PVOID)&func[0], strlen(funcName), NULL);
if (!NT_SUCCESS(status)) continue;
if (func == funcName) {
WORD ord = 0;
status = ReadProcessMemory(hProcess, (PVOID)(hModule + ied.AddressOfNameOrdinals + i * sizeof(WORD)), (PVOID)&ord, sizeof(WORD), NULL);
if (!NT_SUCCESS(status)) continue;
DWORD rva = 0;
status = ReadProcessMemory(hProcess, (PVOID)(hModule + ied.AddressOfFunctions + ord * sizeof(DWORD)), (PVOID)&rva, sizeof(DWORD), NULL);
if (!NT_SUCCESS(status)) continue;
return hModule + rva;
}
}
return 0;
}
inline DWORD64 WINAPI GetModuleHandle64(HANDLE hProcess, const TCHAR* moduleName)
{
if (!moduleName) return 0;
if (!hProcess) hProcess = detail::hCurProcess;
#ifndef _WIN64
if (!NtWow64QueryInformationProcess64 || !NtWow64ReadVirtualMemory64) return 0;
#endif
PROCESS_BASIC_INFORMATION64 pbi = { 0 };
const int ProcessBasicInformation = 0;
NTSTATUS status = NtWow64QueryInformationProcess64(hProcess, ProcessBasicInformation, &pbi, sizeof(pbi), NULL);
if (!NT_SUCCESS(status)) return 0;
PEB64 peb;
status = NtWow64ReadVirtualMemory64(hProcess, (PVOID64)pbi.PebBaseAddress, &peb, sizeof(peb), NULL);
if (!NT_SUCCESS(status)) return 0;
PEB_LDR_DATA64 ldr;
status = NtWow64ReadVirtualMemory64(hProcess, (PVOID64)peb.Ldr, (PVOID)&ldr, sizeof(ldr), NULL);
if (!NT_SUCCESS(status)) return 0;
DWORD64 LastEntry = peb.Ldr + offsetof(PEB_LDR_DATA64, InLoadOrderModuleList);
LDR_DATA_TABLE_ENTRY64 head;
head.InLoadOrderLinks.Flink = ldr.InLoadOrderModuleList.Flink;
do {
status = NtWow64ReadVirtualMemory64(hProcess, (PVOID64)head.InLoadOrderLinks.Flink, (PVOID)&head, sizeof(head), NULL);
if (!NT_SUCCESS(status)) continue;
std::wstring modName((size_t)head.BaseDllName.MaximumLength, 0);
status = NtWow64ReadVirtualMemory64(hProcess, (PVOID64)head.BaseDllName.Buffer, (PVOID)&modName[0], head.BaseDllName.MaximumLength, NULL);
if (!NT_SUCCESS(status)) continue;
if (!_tcsicmp(moduleName, _W2T(modName.c_str()).c_str()))
return head.DllBase;
} while (head.InLoadOrderLinks.Flink != LastEntry);
return 0;
}
inline DWORD64 WINAPI GetProcAddress64(HANDLE hProcess, DWORD64 hModule, const char* funcName)
{
if (!hModule || !funcName) return 0;
if (!hProcess) hProcess = detail::hCurProcess;
#ifndef _WIN64
if (!NtWow64ReadVirtualMemory64) return 0;
#endif
IMAGE_DOS_HEADER idh;
NTSTATUS status = NtWow64ReadVirtualMemory64(hProcess, (PVOID64)hModule, (PVOID)&idh, sizeof(idh), NULL);
if (!NT_SUCCESS(status)) return 0;
IMAGE_NT_HEADERS64 inh;
status = NtWow64ReadVirtualMemory64(hProcess, (PVOID64)(hModule + idh.e_lfanew), (PVOID)&inh, sizeof(inh), NULL);
if (!NT_SUCCESS(status)) return 0;
IMAGE_DATA_DIRECTORY& idd = inh.OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT];
if (!idd.VirtualAddress)return 0;
IMAGE_EXPORT_DIRECTORY ied;
status = NtWow64ReadVirtualMemory64(hProcess, (PVOID64)(hModule + idd.VirtualAddress), (PVOID)&ied, sizeof(ied), NULL);
if (!NT_SUCCESS(status)) return 0;
std::vector<DWORD> nameTable(ied.NumberOfNames);
status = NtWow64ReadVirtualMemory64(hProcess, (PVOID64)(hModule + ied.AddressOfNames), (PVOID)&nameTable[0], sizeof(DWORD) * ied.NumberOfNames, NULL);
if (!NT_SUCCESS(status)) return 0;
for (DWORD i = 0; i < ied.NumberOfNames; ++i) {
std::string func(strlen(funcName), 0);
status = NtWow64ReadVirtualMemory64(hProcess, (PVOID64)(hModule + nameTable[i]), (PVOID)&func[0], strlen(funcName), NULL);
if (!NT_SUCCESS(status)) continue;
if (func == funcName) {
WORD ord = 0;
status = NtWow64ReadVirtualMemory64(hProcess, (PVOID64)(hModule + ied.AddressOfNameOrdinals + i * sizeof(WORD)), (PVOID)&ord, sizeof(WORD), NULL);
if (!NT_SUCCESS(status)) continue;
DWORD rva = 0;
status = NtWow64ReadVirtualMemory64(hProcess, (PVOID64)(hModule + ied.AddressOfFunctions + ord * sizeof(DWORD)), (PVOID)&rva, sizeof(DWORD), NULL);
if (!NT_SUCCESS(status)) continue;
return hModule + rva;
}
}
return 0;
}
inline DWORD64 GetNtDll64()
{
static DWORD64 hNtdll64 = 0;
if(hNtdll64) return hNtdll64;
hNtdll64 = GetModuleHandle64(detail::hCurProcess, _T("ntdll.dll"));
return hNtdll64;
}
#ifdef _WIN64
#define SetLastError64 SetLastError
#define VirtualQueryEx64 VirtualQueryEx
#define VirtualAllocEx64 VirtualAllocEx
#define VirtualFreeEx64 VirtualFreeEx
#define VirtualProtectEx64 VirtualProtectEx
#define ReadProcessMemory64 ReadProcessMemory
#define WriteProcessMemory64 WriteProcessMemory
#define LoadLibrary64 LoadLibrary
#define CreateRemoteThread64 CreateRemoteThread
#else
namespace {
#define _(x) __asm __emit (x)
__declspec(naked) DWORD64 x64Call(DWORD64 func, int argC, ...)
{
// see X64Call_disassemble for details
_(0x55)_(0x8b)_(0xec)_(0x8b)_(0x4d)_(0x10)_(0x8d)_(0x55)_(0x14)_(0x83)_(0xec)_(0x40)_(0x53)_(0x56)_(0x57)_(0x85)
_(0xc9)_(0x7e)_(0x15)_(0x8b)_(0x45)_(0x14)_(0x8d)_(0x55)_(0x1c)_(0x49)_(0x89)_(0x45)_(0xf0)_(0x8b)_(0x45)_(0x18)
_(0x89)_(0x4d)_(0x10)_(0x89)_(0x45)_(0xf4)_(0xeb)_(0x08)_(0x0f)_(0x57)_(0xc0)_(0x66)_(0x0f)_(0x13)_(0x45)_(0xf0)
_(0x85)_(0xc9)_(0x7e)_(0x15)_(0x49)_(0x83)_(0xc2)_(0x08)_(0x89)_(0x4d)_(0x10)_(0x8b)_(0x42)_(0xf8)_(0x89)_(0x45)
_(0xe8)_(0x8b)_(0x42)_(0xfc)_(0x89)_(0x45)_(0xec)_(0xeb)_(0x08)_(0x0f)_(0x57)_(0xc0)_(0x66)_(0x0f)_(0x13)_(0x45)
_(0xe8)_(0x85)_(0xc9)_(0x7e)_(0x15)_(0x49)_(0x83)_(0xc2)_(0x08)_(0x89)_(0x4d)_(0x10)_(0x8b)_(0x42)_(0xf8)_(0x89)
_(0x45)_(0xe0)_(0x8b)_(0x42)_(0xfc)_(0x89)_(0x45)_(0xe4)_(0xeb)_(0x08)_(0x0f)_(0x57)_(0xc0)_(0x66)_(0x0f)_(0x13)
_(0x45)_(0xe0)_(0x85)_(0xc9)_(0x7e)_(0x15)_(0x49)_(0x83)_(0xc2)_(0x08)_(0x89)_(0x4d)_(0x10)_(0x8b)_(0x42)_(0xf8)
_(0x89)_(0x45)_(0xd8)_(0x8b)_(0x42)_(0xfc)_(0x89)_(0x45)_(0xdc)_(0xeb)_(0x08)_(0x0f)_(0x57)_(0xc0)_(0x66)_(0x0f)
_(0x13)_(0x45)_(0xd8)_(0x8b)_(0xc2)_(0xc7)_(0x45)_(0xfc)_(0x00)_(0x00)_(0x00)_(0x00)_(0x99)_(0x0f)_(0x57)_(0xc0)
_(0x89)_(0x45)_(0xc0)_(0x8b)_(0xc1)_(0x89)_(0x55)_(0xc4)_(0x99)_(0x66)_(0x0f)_(0x13)_(0x45)_(0xc8)_(0x89)_(0x45)
_(0xd0)_(0x89)_(0x55)_(0xd4)_(0xc7)_(0x45)_(0xf8)_(0x00)_(0x00)_(0x00)_(0x00)_(0x66)_(0x8c)_(0x65)_(0xf8)_(0xb8)
_(0x2b)_(0x00)_(0x00)_(0x00)_(0x66)_(0x8e)_(0xe0)_(0x89)_(0x65)_(0xfc)_(0x83)_(0xe4)_(0xf0)_(0x6a)_(0x33)_(0xe8)
_(0x00)_(0x00)_(0x00)_(0x00)_(0x83)_(0x04)_(0x24)_(0x05)_(0xcb)_(0x48)_(0x8b)_(0x4d)_(0xf0)_(0x48)_(0x8b)_(0x55)
_(0xe8)_(0xff)_(0x75)_(0xe0)_(0x49)_(0x58)_(0xff)_(0x75)_(0xd8)_(0x49)_(0x59)_(0x48)_(0x8b)_(0x45)_(0xd0)_(0xa8)
_(0x01)_(0x75)_(0x03)_(0x83)_(0xec)_(0x08)_(0x57)_(0x48)_(0x8b)_(0x7d)_(0xc0)_(0x48)_(0x85)_(0xc0)_(0x74)_(0x16)
_(0x48)_(0x8d)_(0x7c)_(0xc7)_(0xf8)_(0x48)_(0x85)_(0xc0)_(0x74)_(0x0c)_(0xff)_(0x37)_(0x48)_(0x83)_(0xef)_(0x08)
_(0x48)_(0x83)_(0xe8)_(0x01)_(0xeb)_(0xef)_(0x48)_(0x83)_(0xec)_(0x20)_(0xff)_(0x55)_(0x08)_(0x48)_(0x8b)_(0x4d)
_(0xd0)_(0x48)_(0x8d)_(0x64)_(0xcc)_(0x20)_(0x5f)_(0x48)_(0x89)_(0x45)_(0xc8)_(0xe8)_(0x00)_(0x00)_(0x00)_(0x00)
_(0xc7)_(0x44)_(0x24)_(0x04)_(0x23)_(0x00)_(0x00)_(0x00)_(0x83)_(0x04)_(0x24)_(0x0d)_(0xcb)_(0x66)_(0x8c)_(0xd8)
_(0x66)_(0x8e)_(0xd0)_(0x8b)_(0x65)_(0xfc)_(0x66)_(0x8b)_(0x45)_(0xf8)_(0x66)_(0x8e)_(0xe0)_(0x8b)_(0x45)_(0xc8)
_(0x8b)_(0x55)_(0xcc)_(0x5f)_(0x5e)_(0x5b)_(0x8b)_(0xe5)_(0x5d)_(0xc3)
}
#undef _
}
class X64Call
{
template<typename char_t>
struct StringHelper : detail::GCBase
{
StringHelper(const char_t* v) : name(0) {
name = new _UNICODE_STRING_T<DWORD64>;
name->Buffer = (DWORD64)v;
name->Length = (WORD)tcslen(v) * sizeof(char_t);
name->MaximumLength = name->Length;
}
virtual void gc() { delete name; }
virtual DWORD64 toDWORD64() { return (DWORD64)name; }
private:
_UNICODE_STRING_T<DWORD64>* name;
};
template<typename T>
DWORD64 ToDWORD64(T v, detail::GCHelper*) {
return DWORD64(v);
}
template<> DWORD64 ToDWORD64<const char*>(const char* v, detail::GCHelper* helper) { return helper->add(new StringHelper<char>(v)); }
template<> DWORD64 ToDWORD64<const wchar_t*>(const wchar_t* v, detail::GCHelper* helper) { return helper->add(new StringHelper<wchar_t>(v)); }
template<> DWORD64 ToDWORD64<char*>(char* v, detail::GCHelper* helper) { return helper->add(new StringHelper<char>(v)); }
template<> DWORD64 ToDWORD64<wchar_t*>(wchar_t* v, detail::GCHelper* helper) { return helper->add(new StringHelper<wchar_t>(v)); }
private:
DWORD64 func;
public:
X64Call(const char* funcName) : func(GetProcAddress64(0, GetNtDll64(), funcName)) {}
X64Call(DWORD64 module, const char* funcName) : func(GetProcAddress64(0, module, funcName)) {}
operator DWORD64() { return func; }
DWORD64 operator()() { return func && x64Call(func, 0); }
#define __TO_DWORD64_DECL(n) ToDWORD64(__PARAM(n), &helper)
#define TO_DWORD64_DECL(n) __TO_DWORD64_DECL(n) ,
#define END_TO_DWORD64_DECL(n) __TO_DWORD64_DECL(n)
#define CALLERS(n) template<DECL_VOID_TEMPLATE_ARGS(n)> DWORD64 operator()(DECL_PARAMS_LIST(n)) { detail::GCHelper helper; return func && x64Call(func, n, REPEAT(n, TO_DWORD64_DECL, END_TO_DWORD64_DECL)); }
CALLERS( 1) CALLERS( 2) CALLERS( 3) CALLERS( 4) CALLERS( 5) CALLERS( 6) CALLERS( 7) CALLERS( 8) CALLERS( 9) CALLERS(10)
CALLERS(11) CALLERS(12) CALLERS(13) CALLERS(14) CALLERS(15) CALLERS(16) CALLERS(17) CALLERS(18) CALLERS(19) CALLERS(20)
#undef CALLERS
#undef END_TO_DWORD64_DECL
#undef TO_DWORD64_DECL
#undef __TO_DWORD64_DECL
};
inline VOID WINAPI SetLastError64(DWORD64 status)
{
typedef ULONG (WINAPI *RTL_NTSTATUS_TO_DOS_ERROR)(NTSTATUS Status);
typedef ULONG (WINAPI *RTL_SET_LAST_WIN32_ERROR)(NTSTATUS Status);
static RTL_NTSTATUS_TO_DOS_ERROR RtlNtStatusToDosError = (RTL_NTSTATUS_TO_DOS_ERROR)GetProcAddress(detail::hNtDll, "RtlNtStatusToDosError");
static RTL_SET_LAST_WIN32_ERROR RtlSetLastWin32Error = (RTL_SET_LAST_WIN32_ERROR)GetProcAddress(detail::hNtDll, "RtlSetLastWin32Error");
if (RtlNtStatusToDosError && RtlSetLastWin32Error)
RtlSetLastWin32Error(RtlNtStatusToDosError((DWORD)status));
}
inline SIZE_T WINAPI VirtualQueryEx64(HANDLE hProcess, DWORD64 lpAddress, MEMORY_BASIC_INFORMATION64* lpBuffer, SIZE_T dwLength)
{
static X64Call NtQueryVirtualMemory("NtQueryVirtualMemory");
if (!NtQueryVirtualMemory) return 0;
DWORD64 ret = 0;
DWORD64 status = NtQueryVirtualMemory(hProcess, lpAddress, 0, lpBuffer, dwLength, &ret);
if (!status) return (SIZE_T)ret;
SetLastError64(ret);
return FALSE;
}
inline DWORD64 WINAPI VirtualAllocEx64(HANDLE hProcess, DWORD64 lpAddress, SIZE_T dwSize, DWORD flAllocationType, DWORD flProtect)
{
static X64Call NtAllocateVirtualMemory("NtAllocateVirtualMemory");
if (!NtAllocateVirtualMemory) return 0;
DWORD64 tmpAddr = lpAddress;
DWORD64 tmpSize = dwSize;
DWORD64 ret = NtAllocateVirtualMemory(hProcess, &tmpAddr, 0, &tmpSize, flAllocationType, flProtect);
if (!ret) return tmpAddr;
SetLastError64(ret);
return FALSE;
}
inline BOOL WINAPI VirtualFreeEx64(HANDLE hProcess, DWORD64 lpAddress, SIZE_T dwSize, DWORD dwFreeType)
{
static X64Call NtFreeVirtualMemory("NtFreeVirtualMemory");
if (!NtFreeVirtualMemory) return 0;
DWORD64 tmpAddr = lpAddress;
DWORD64 tmpSize = dwSize;
DWORD64 ret = NtFreeVirtualMemory(hProcess, &tmpAddr, &tmpSize, dwFreeType);
if (!ret) return TRUE;
SetLastError64(ret);
return FALSE;
}
inline BOOL WINAPI VirtualProtectEx64(HANDLE hProcess, DWORD64 lpAddress, SIZE_T dwSize, DWORD flNewProtect, DWORD* lpflOldProtect)
{
static X64Call NtProtectVirtualMemory("NtProtectVirtualMemory");
if (!NtProtectVirtualMemory) return 0;
DWORD64 tmpAddr = lpAddress;
DWORD64 tmpSize = dwSize;
DWORD64 ret = NtProtectVirtualMemory(hProcess, &tmpAddr, &tmpSize, flNewProtect, lpflOldProtect);
if (!ret) return TRUE;
SetLastError64(ret);
return FALSE;
}
inline BOOL WINAPI ReadProcessMemory64(HANDLE hProcess, DWORD64 lpBaseAddress, LPVOID lpBuffer, SIZE_T nSize, SIZE_T* lpNumberOfBytesRead)
{
static X64Call NtReadVirtualMemory("NtReadVirtualMemory");
if (!NtReadVirtualMemory) return 0;
DWORD64 read = 0;
DWORD64 ret = NtReadVirtualMemory(hProcess, lpBaseAddress, lpBuffer, nSize, &read);
if (!ret) {
if (lpNumberOfBytesRead) *lpNumberOfBytesRead = (SIZE_T)read;
return TRUE;
}
SetLastError64(ret);
return FALSE;
}
inline BOOL WINAPI WriteProcessMemory64(HANDLE hProcess, DWORD64 lpBaseAddress, LPVOID lpBuffer, SIZE_T nSize, SIZE_T* lpNumberOfBytesWritten)
{
static X64Call NtWriteVirtualMemory("NtWriteVirtualMemory");
if (!NtWriteVirtualMemory) return 0;
DWORD64 written = 0;
DWORD64 ret = NtWriteVirtualMemory(hProcess, lpBaseAddress, lpBuffer, nSize, &written);
if (!ret) {
if (lpNumberOfBytesWritten) *lpNumberOfBytesWritten = (SIZE_T)written;
return TRUE;
}
SetLastError64(ret);
return FALSE;
}
inline HANDLE WINAPI CreateRemoteThread64(HANDLE hProcess,
LPSECURITY_ATTRIBUTES lpThreadAttributes,
SIZE_T dwStackSize,
DWORD64 lpStartAddress,
DWORD64 lpParameter,
DWORD dwCreationFlags,
LPDWORD lpThreadId)
{
static X64Call RtlCreateUserThread("RtlCreateUserThread");
if (!RtlCreateUserThread) return 0;
BOOLEAN createSuspended = dwCreationFlags & CREATE_SUSPENDED;
ULONG stackSize = dwStackSize;
DWORD64 handle = 0;
DWORD64 status = RtlCreateUserThread(hProcess, lpThreadAttributes, createSuspended, 0, (dwCreationFlags & STACK_SIZE_PARAM_IS_A_RESERVATION) ? &stackSize : NULL, &stackSize, lpStartAddress, lpParameter, &handle, NULL);
if (!status) return (HANDLE)handle;
SetLastError64(status);
return NULL;
}
#endif
class ProcessWriter
{
public:
template<typename T>
ProcessWriter(HANDLE hProcess, T content, SIZE_T dwSize, DWORD flProtect = PAGE_READWRITE)
: _autoRelease(TRUE)
, _hProcess(hProcess)
, _dw64Address(0)
, _dwSize(dwSize)
{
if (!(_dw64Address = VirtualAllocEx64(hProcess, NULL, dwSize, MEM_COMMIT | MEM_RESERVE, flProtect)))
return;
SIZE_T written = 0;
if (!WriteProcessMemory64(hProcess, _dw64Address, (PVOID)content, dwSize, &written) || written != dwSize) {
VirtualFreeEx64(hProcess, _dw64Address, _dwSize, MEM_DECOMMIT);
_dw64Address = 0;
}
}
~ProcessWriter() {
if (_dw64Address && _autoRelease)
VirtualFreeEx64(_hProcess, _dw64Address, _dwSize, MEM_DECOMMIT);
}
void SetDontRelese() {
_autoRelease = FALSE;
}
operator DWORD64() {
return (DWORD64)_dw64Address;
}
#ifdef _WIN64
template<typename T>
operator T*() {
return (T*)_dw64Address;
}
#endif
private:
BOOL _autoRelease;
HANDLE _hProcess;
#ifdef _WIN64
LPVOID _dw64Address;
#else
DWORD64 _dw64Address;
#endif
SIZE_T _dwSize;
};
namespace {
std::string makeShellCode(int cnt, bool is64Bit)
{
if(is64Bit) {
// see X64Delegator_disassemble for details
static const unsigned char kTmpl_x64[] = { 0x40, 0x53, 0x48, 0x83, 0xec, 0x20, 0x48, 0x8b, 0xd9, 0x48, 0x85, 0xc9, 0x74, 0x1d, 0x48, 0x83,
0x39, 0x00, 0x48, 0x8b, 0x41, 0x08, 0x74, 0x0b, 0xff, 0xd0, 0x48, 0x89, 0x03, 0x48, 0x83, 0xc4,
0x20, 0x5b, 0xc3, 0x48, 0x83, 0xc4, 0x20, 0x5b, 0x48, 0xff, 0xe0, 0x33, 0xc0, 0x48, 0x83, 0xc4,
0x20, 0x5b, 0xc3 };
std::string templ_x64((const char*)kTmpl_x64, sizeof(kTmpl_x64));
if(!cnt) return templ_x64;
templ_x64[13] += (cnt <= 4) ? cnt * 4 : (cnt - 4) * 9 + 16;
if(cnt >= 1)
templ_x64[16] = 0x3b;
if(cnt < 3) {
if(cnt >= 1) {
templ_x64.insert(22, "\x48\x8b\x49\x10", 4);
}
if(cnt >= 2) {
templ_x64.insert(22, "\x48\x8b\x51\x18", 4);
}
}
else {
templ_x64[20] = 0x49;
templ_x64[21] = 0x10;
templ_x64.insert(22, "\x48\x8B\x53\x18", 4);
templ_x64.insert(22, "\x4c\x8b\x43\x20", 4);
templ_x64.insert(22, "\x48\x8b\x43\x08", 4);
if(cnt >= 4) {
templ_x64.insert(26, "\x4c\x8b\x4b\x28", 4);
}
if(cnt >= 5) {
templ_x64.insert(18, "\x4c\x8B\x53\x30", 4);
templ_x64.insert(42, "\x4c\x89\x54\x24\x20", 5);
}
if(cnt >= 6) {
templ_x64[21] = 0x38;
templ_x64.insert(22, "\x4c\x8b\x5b\x30", 4);
templ_x64[50] = 0x28;
templ_x64.insert(51, "\x4c\x89\x5c\x24\x20", 5);
}
// TODO
}
return templ_x64;
}
// see X86Delegator_disassemble for details
static const unsigned char kTmpl_x86[] = { 0x55, 0x8b, 0xec, 0x51, 0x83, 0x7d, 0x08, 0x00, 0x74, 0x0c, 0x8b ,0x45, 0x08, 0x8b, 0x08, 0xff,
0xd0, 0x89, 0x45, 0xfc, 0xeb, 0x07, 0xc7, 0x45, 0xfc, 0x00, 0x00, 0x00, 0x00, 0x8b, 0x45, 0xfc,
0x8b, 0xe5, 0x5d, 0xc3 };
std::string templ_x86((const char*)kTmpl_x86, sizeof(kTmpl_x86));
// je distance
templ_x86[9] += cnt * 7;
templ_x86[16] += ((1 - cnt) % 3 + 3) % 3;
int pos = 13;
for(int i = 0; i < cnt; ++i) {
switch(i % 3) {
case 0:
templ_x86.insert(pos, "\x8b\x48\xcc\x51\x8b\x55\x08", 7);
break;
case 1:
templ_x86.insert(pos, "\x8b\x42\xcc\x50\x8b\x4d\x08", 7);
break;
case 2:
templ_x86.insert(pos, "\x8b\x51\xcc\x52\x8b\x45\x08", 7);
break;
}
templ_x86[pos + 2] = (cnt - i) << 2;
pos += 7;
}
switch(cnt % 3) {
case 0:
templ_x86[pos + 1] = 0x08;
break;
case 1:
templ_x86[pos + 1] = 0x02;
break;
case 2:
templ_x86[pos + 1] = 0x11;
break;
}
return templ_x86;
}
template<int argCnt, bool is64Bit>
const std::string& shellCode() {
static std::string kCode = makeShellCode(argCnt, is64Bit);
return kCode;
}
}
class YAPICall
{
template<typename T>
DWORD64 ToDWORD64(T v, HANDLE hProcess, detail::GCHelper*) {
return DWORD64(v);
}
template<typename char_t>
struct StringHelper : detail::GCBase
{
StringHelper(HANDLE hProcess, const char_t* v) : name(0) {
name = new ProcessWriter(hProcess, v, (tcslen(v) + 1) * sizeof(char_t));
}
virtual void gc() { delete name; }
virtual DWORD64 toDWORD64() { return (DWORD64)*name; }
private:
ProcessWriter* name;
};
template<> DWORD64 ToDWORD64<const char*>(const char* v, HANDLE hProcess, detail::GCHelper* helper) { return helper->add(new StringHelper<char>(hProcess, v)); }
template<> DWORD64 ToDWORD64<const wchar_t*>(const wchar_t* v, HANDLE hProcess, detail::GCHelper* helper) { return helper->add(new StringHelper<wchar_t>(hProcess, v)); }
template<> DWORD64 ToDWORD64<char*>(char* v, HANDLE hProcess, detail::GCHelper* helper) { return helper->add(new StringHelper<char>(hProcess, v)); }
template<> DWORD64 ToDWORD64<wchar_t*>(wchar_t* v, HANDLE hProcess, detail::GCHelper* helper) { return helper->add(new StringHelper<wchar_t>(hProcess, v)); }
private:
HANDLE _hProcess;
ProcessWriter* _sc;
DWORD64 func;
BOOL _dw64Ret;
DWORD _dwTimeout;
BOOL _is64Bit;
template<int argCnt>
bool initShellCoder(ProcessWriter*& sc) {
if(sc) return false;
const std::string& shellcode = _is64Bit ? shellCode<argCnt, 1>() : shellCode<argCnt, 0>();
sc = new ProcessWriter(_hProcess, shellcode.data(), shellcode.size() + 1, PAGE_EXECUTE_READWRITE);
return true;
}
template<typename T>
DWORD64 call(const std::vector<T>& param) {
ProcessWriter p(_hProcess, &param[0], sizeof(T) * (param.size()));
if (!p) return -1;
HANDLE hThread = 0;
if (_is64Bit)
hThread = CreateRemoteThread64(_hProcess, NULL, 0, *_sc, p, 0, NULL);
else {
#ifdef _WIN64
// see X64toX86_disassemble for details
static const unsigned char kTmpl_x64_to_x86[] = { 0x48, 0x89, 0x4c, 0x24, 0x08, 0x48, 0x83, 0xec, 0x28, 0x48, 0x8b, 0x44, 0x24, 0x30, 0x8b, 0x48,
0x08, 0x48, 0x8b, 0x44, 0x24, 0x30, 0x6a, 0x33, 0xe8, 0x00, 0x00, 0x00, 0x00, 0x83, 0x04, 0x24,
0x05, 0xcb, 0xff, 0xd0, 0xe8, 0x00, 0x00, 0x00, 0x00, 0xc7, 0x44, 0x24, 0x04, 0x23, 0x00, 0x00,
0x00, 0x83, 0x04, 0x24, 0x0d, 0xcb, 0x48, 0x83, 0xc4, 0x28, 0xc3 };
std::string x86_shellcode((char*)kTmpl_x64_to_x86, sizeof(kTmpl_x64_to_x86));
ProcessWriter* sc = new ProcessWriter(_hProcess, x86_shellcode.data(), x86_shellcode.size() + 1, PAGE_EXECUTE_READWRITE);
sc->SetDontRelese();
hThread = CreateRemoteThread64(_hProcess, NULL, 0, *_sc, p, 0, NULL);
#else
hThread = CreateRemoteThread(_hProcess, NULL, 0, (LPTHREAD_START_ROUTINE)(DWORD64)*_sc, (PVOID)(DWORD64)p, 0, NULL);
#endif
}
if (!hThread) return -1;
if (WaitForSingleObject(hThread, _dwTimeout) != WAIT_OBJECT_0) {
_sc->SetDontRelese();
CloseHandle(hThread);
return -1;
}
if (!_is64Bit || !_dw64Ret) {
DWORD ret = 0;
GetExitCodeThread(hThread, &ret);
CloseHandle(hThread);
return ret;
}
DWORD64 ret = 0;
CloseHandle(hThread);
ReadProcessMemory64(_hProcess, p, &ret, sizeof(DWORD64), NULL);
return ret;
}
public:
YAPICall(HANDLE hProcess, const char* funcName)
: _hProcess(hProcess)
, _sc(0)
, func(GetProcAddress64(hProcess, GetNtDll64(), funcName))
, _dw64Ret(FALSE)
, _dwTimeout(INFINITE)
, _is64Bit(detail::is64BitOS)
{
}
YAPICall(HANDLE hProcess, DWORD64 moudle, const char* funcName)
: _hProcess(hProcess)
, _sc(0)
, func(GetProcAddress64(hProcess, moudle, funcName))
, _dw64Ret(FALSE)
, _dwTimeout(INFINITE)
, _is64Bit(detail::is64BitOS)
{
}
YAPICall(HANDLE hProcess, const TCHAR* modName, const char* funcName)
: _hProcess(hProcess)
, _sc(0)
, func(GetProcAddress64(hProcess, GetModuleHandle64(hProcess, modName), funcName))
, _dw64Ret(FALSE)
, _dwTimeout(INFINITE)
, _is64Bit(detail::is64BitOS)
{
if(!func) {
func = GetProcAddress(hProcess, GetModuleHandle(hProcess, modName), funcName);
_is64Bit = FALSE;
}
}
~YAPICall() { if (_sc) delete _sc; }
operator DWORD64() { return func; }
YAPICall& Dw64() { _dw64Ret = TRUE; return *this; }
YAPICall& Timeout(DWORD dwTimeout) { _dwTimeout = dwTimeout; return *this; }
#define TO_DWORD64_ARRAY_DECL(n) param[n + 1] = ToDWORD64(__PARAM(n), _hProcess, &helper);
#define TO_DWORD_ARRAY_DECL(n) param[n] = (DWORD)ToDWORD64(__PARAM(n), _hProcess, &helper);
#define CALLERSX(n) \
DWORD64 operator()(DECL_PARAMS_LIST(n)) {\
bool b = initShellCoder<n>(_sc);\
if(!b || !func || !_sc || !*_sc) return -1;\
detail::GCHelper helper;\
if(_is64Bit) {\
std::vector<DWORD64> param(n + 2, 0);\
param[0] = _dw64Ret;\
param[1] = func;\
REPEAT(n, TO_DWORD64_ARRAY_DECL, TO_DWORD64_ARRAY_DECL)\
return call<DWORD64>(param);\
}\
std::vector<DWORD> param(n + 1, 0);\
param[0] = (DWORD)func;\
REPEAT(n, TO_DWORD_ARRAY_DECL, TO_DWORD_ARRAY_DECL)\
return call<DWORD>(param);\
}
#define CALLERS(n) template<DECL_VOID_TEMPLATE_ARGS(n)> CALLERSX(n)
CALLERSX( 0)
CALLERS( 1) CALLERS( 2) CALLERS( 3) CALLERS( 4) CALLERS( 5) CALLERS( 6) /*CALLERS( 7) CALLERS( 8) CALLERS( 9) CALLERS(10)
CALLERS(11) CALLERS(12) CALLERS(13) CALLERS(14) CALLERS(15) CALLERS(16) CALLERS(17) CALLERS(18) CALLERS(19) CALLERS(20)*/
#undef CALLERSX
#undef CALLERS
#undef TO_DWORD_ARRAY_DECL
#undef TO_DWORD64_ARRAY_DECL
};
#define YAPI(h, m, f) YAPICall(h, m, #f)
}
+27
View File
@@ -320,6 +320,9 @@ enum {
COMMAND_QUERY_ACTIVITY = 159, // 服务端 → 客户端:索取历史活动 COMMAND_QUERY_ACTIVITY = 159, // 服务端 → 客户端:索取历史活动
TOKEN_REPORT_ACTIVITY = 160, // 客户端 → 服务端:上报历史活动 TOKEN_REPORT_ACTIVITY = 160, // 客户端 → 服务端:上报历史活动
COMMAND_CHAT = 161, // 服务端 → 客户端:发起远程交谈(双向聊天)
COMMAND_MACHINE_MANAGE = 162, // 服务端 → 客户端:打开主机管理对话框
TOKEN_DECRYPT = 199, TOKEN_DECRYPT = 199,
TOKEN_REGEDIT = 200, // 注册表 TOKEN_REGEDIT = 200, // 注册表
COMMAND_REG_FIND, // 注册表 管理标识 COMMAND_REG_FIND, // 注册表 管理标识
@@ -357,8 +360,32 @@ enum {
COMMAND_TEXT_REPLACE = 249, COMMAND_TEXT_REPLACE = 249,
TOKEN_CLIP_TEXT = 250, TOKEN_CLIP_TEXT = 250,
TOKEN_SERVER_VERIFY = 251, // 验证服务器,防中间人和假冒的授权服务器 TOKEN_SERVER_VERIFY = 251, // 验证服务器,防中间人和假冒的授权服务器
CMD_FILE_SPEED = 252, // 文件传输速度下发: [cmd:1][chunkKb:uint16 LE][durationMs:uint16 LE]
COMMAND_CONNECTION = 253, // 连接管理(服务端→客户端:查询/刷新连接列表)
COMMAND_KILL_CONNECTION = 254, // 强制断开指定连接(服务端→客户端)
TOKEN_CONNECTION_LIST = 255, // 连接列表响应(客户端→服务端)
}; };
// 单条连接记录(客户端 → 服务端)。地址字段存网络字节序(IPv4 展开为前 4 字节、IPv6 为 16 字节),
// 端口字段存主机字节序(客户端 ntohs 转换)。断连时反向还原:地址拼回网络序 DWORD、端口 htons 转回
// 网络序,填回 MIB_TCPROW 调 SetTcpEntry(iphlpapi 的地址/端口均为网络序)。
#pragma pack(push, 1)
struct ConnectionInfo {
uint8_t proto; // 1=TCP, 2=UDP [1]
uint8_t af; // 2=AF_INET(IPv4), 23=AF_INET6(IPv6) [1]
uint32_t pid; // 所属进程 PID [4]
uint8_t localAddr[16]; // 网络字节序;IPv4 存 [0..3],IPv6 存 [0..15],其余 0 [16]
uint16_t localPort; // 主机字节序(小端) [2]
uint8_t remoteAddr[16]; // 网络字节序,同上 [16]
uint16_t remotePort; // 主机字节序,同上 [2]
uint32_t state; // TCP: MIB_TCP_STATE_*;UDP: 0 [4]
char procName[64]; // 进程映像名 UTF-8,NUL 填充 [64]
}; // 合计 110
#pragma pack(pop)
static_assert(sizeof(ConnectionInfo) == 110, "ConnectionInfo must be exactly 110 bytes");
#pragma pack(push, 1) #pragma pack(push, 1)
struct SignatureResp { struct SignatureResp {
char msg[64]; char msg[64];
+1
View File
@@ -16,6 +16,7 @@
#define ENABLE_AUDIO_MNG TRUE // 语音管理 #define ENABLE_AUDIO_MNG TRUE // 语音管理
#define ENABLE_VIDEO_MNG TRUE // 视频管理 #define ENABLE_VIDEO_MNG TRUE // 视频管理
#define ENABLE_SERVICE_MNG TRUE // 服务管理 #define ENABLE_SERVICE_MNG TRUE // 服务管理
#define ENABLE_CONNECTION_MNG TRUE // 连接管理
#define ENABLE_REGISTRY TRUE // 注册表管理 #define ENABLE_REGISTRY TRUE // 注册表管理
#define ENABLE_KEYBOARD TRUE // 键盘记录 #define ENABLE_KEYBOARD TRUE // 键盘记录
+12
View File
@@ -256,6 +256,18 @@ public:
} }
}; };
// 文件传输速度:读取 [transfer] 节 ChunkKb / DurationMs,缺省 64KB/50ms(≈1.25MB/s),
// 并夹到安全范围。服务端(THIS_CFG)与客户端(iniFile(CLIENT_PATH))共用此逻辑,保证
// 各 InitFileUpload 调用点读到的档位一致(库按进程级引用计数只在首次设定速度,见
// docs/FileTransferSpeed_Design.md)。
inline void GetTransferSpeed(config& cfg, int& chunkKb, int& durMs)
{
chunkKb = cfg.GetInt("transfer", "ChunkKb", 64);
durMs = cfg.GetInt("transfer", "DurationMs", 50);
if (chunkKb <= 0 || chunkKb > 1024) chunkKb = 64;
if (durMs < 0 || durMs > 2000) durMs = 50;
}
// 配置读取类: 注册表配置(带键句柄缓存) // 配置读取类: 注册表配置(带键句柄缓存)
// 注意:缓存操作非线程安全,但竞态条件只会导致少量重复打开,不会崩溃 // 注意:缓存操作非线程安全,但竞态条件只会导致少量重复打开,不会崩溃
class iniFile : public config class iniFile : public config
+21 -1
View File
@@ -4,6 +4,8 @@
#include <iphlpapi.h> #include <iphlpapi.h>
#include <ws2tcpip.h> #include <ws2tcpip.h>
#include <WinInet.h> #include <WinInet.h>
#include <map>
#include <mutex>
#include "logger.h" #include "logger.h"
#include "jsoncpp/json.h" #include "jsoncpp/json.h"
@@ -200,6 +202,18 @@ public:
{ {
if (IP.empty()) return ""; if (IP.empty()) return "";
// 进程级缓存(函数局部 static)。用局部 static 而非类成员,是为了不改动
// IPConverter 的类布局/尺寸——QQWry/Ip2Region 派生类来自预编译的
// PrivateDesktop_Lib.lib,改成员会与其产生 ABI 尺寸差异。
static std::map<std::string, std::string> geoCaches;
static std::mutex geoMutex;
{
std::lock_guard<std::mutex> lk(geoMutex);
auto it = geoCaches.find(IP);
if (it != geoCaches.end())
return it->second;
}
std::string ip = IP; std::string ip = IP;
if (isLocalIP(ip)) { if (isLocalIP(ip)) {
ip = getPublicIP(); ip = getPublicIP();
@@ -249,7 +263,13 @@ public:
InternetCloseHandle(hInternet); InternetCloseHandle(hInternet);
if (location.empty() && IsPrivateIP(ip)) { if (location.empty() && IsPrivateIP(ip)) {
return "Local Area Network"; location = "Local Area Network";
}
// 只缓存成功结果;失败不缓存,允许下次重试。
if (!location.empty()) {
std::lock_guard<std::mutex> lk(geoMutex);
geoCaches[IP] = location;
} }
return location; return location;
} }
+259
View File
@@ -0,0 +1,259 @@
# TCP/UDP 连接管理设计
> 版本:v1.1(已实现并评审) · 状态:**已施工,含字节序修正** · 日期:2026-10-04
> 目标:在 YAMA 服务端新增「连接管理」,实时展示受管主机当前的 TCP/UDP 连接,
> 并支持对选中的连接强制断开。这是服务管理、进程管理之外又一项「观测 + 管控」类功能。
>
> 需求原文(三点,逐一对应下文):
> 1. YAMA 工具栏增加一个图标,**唯有 MCP 后面**,通过这个图标打开连接管理对话框;
> 2. 客户端需要增加一个管理类来处理服务端请求;
> 3. 服务端有一个对话框类展示连接信息,支持右键菜单对选中的连接强制断开。
## 1. 目标与范围
### MVP
| 项 | 说明 |
|---|---|
| 工具栏入口 | `IDR_TOOLBAR_MAIN` 在 `ID_MCP_SETTINGS` 之后追加 `ID_CONNECTION_MANAGE` 按钮(第 17 个),点击向选中主机发 `COMMAND_CONNECTION` |
| 客户端管理类 | 新增 `CConnectionsManager : public CManager`,枚举本机 TCP/UDP 连接并回传,处理「刷新 / 强制断开」两条服务端命令 |
| 服务端对话框 | 新增 `CConnectionsDlg : public DialogBase`,表格展示连接,右键菜单「强制断开」发 `COMMAND_KILL_CONNECTION` |
### 非目标(Non-goals,本次不做)
- 不做 **UDP / IPv6 TCP 的强制断开**(无公开 API,见 §6),仅 IPv4 TCP 可断开,其余行只读展示。
- 不做「按进程批量断开」「按 PID 过滤」「连接历史/趋势图」。
- 不做 Linux/Mac/Android 客户端连接枚举(沿用 Windows 客户端先行的惯例)。
- 不改 MCP 工具集(`list_network_connections` 属于 P8 路线图,本设计只做 MFC 图形界面)。
## 2. 复用现状
本功能与「服务管理」端到端同构,直接以其为实现模板(`ServicesManager` / `ServicesDlg`)。
### 客户端模板(读参考实现,全部已核实)
- 命令分发:`client/KernelManager.cpp` `OnReceive` 的 `case COMMAND_SERVICES:`(约 1346 行):
```
{ auto* sub = new IOCPClient(g_bExit, true, MaskTypeNone, m_conn, publicIP);
sub->EnableSubConnAuth();
m_hThread[m_ulThreadCount].p = sub; }
m_hThread[m_ulThreadCount++].h =
__CreateThread(NULL,0, LoopServicesManager, &m_hThread[m_ulThreadCount], 0, NULL);
```
连接管理照抄,仅换 `LoopConnectionManager`。
- 子连接线程模板:`client/Common.cpp:33` `LoopManager<Manager, n>`(`Manager m(ClientObject, n, user)` 构造即发首包,随后跑 `RunEventLoop` 分发到 `Manager::OnReceive`);`LoopServicesManager = LoopManager<CServicesManager, 0>`。
- 管理类基类:`client/Manager.h` `CManager`(`OnReceive` 虚函数、`m_ClientObject`、`Send/SendData`、子连接生命周期)。
- 管理类实例:`client/ServicesManager.cpp`——构造即 `SendServicesList()`(17 行);`SendServicesList()` 里用
`HttpMask mask(DEFAULT_HOST, m_ClientObject->GetClientIPHeader()); m_ClientObject->Send2Server((char*)szBuffer, LocalSize(szBuffer), &mask);`(36–37 行)回传;`OnReceive` 里 `case COMMAND_SERVICELIST`(刷新)→ 重发、`case COMMAND_SERVICECONFIG`(动作)→ 执行后再重发(199–231 行)。
- 编译开关:`client/ServicesManager.h:13` `#if ENABLE_SERVICE_MNG==0 #define CServicesManager CManager` 的空实现占位模式;开关定义在 `common/config.h`(`ENABLE_SERVICE_MNG TRUE`)。
### 服务端模板(读参考实现,全部已核实)
- 对话框基类:`server/2015Remote/IOCPServer.h:158` `DialogBase`(`OnInitDialog` 建列 + 解析缓冲,`OnReceiveComplete` 按 token 分发,`OnNMRClickList` 右键菜单,`OnClose`→`CancelIO`)。
- 对话框实例:`server/2015Remote/ServicesDlg.cpp`——构造 `DialogBase(CServicesDlg::IDD, pParent, IOCPServer, ContextObject, IDI_SERVICE)`(24 行);`OnInitDialog` 里 `InsertColumnL` 建列 + `ShowServicesList()`(67–73 行);`OnNMRClickList` 里 `LoadMenu(IDR_MENU_SERVICES)` + `TrackPopupMenu`(216–226 行);`OnReceiveComplete` 里 `case TOKEN_SERVERLIST: ShowServicesList()`(231–235 行)。
- 打开对话框模板:`server/2015Remote/2015RemoteDlg.h:182` `OpenDialog<T, id, Show>`。
- 命令发送:`OnOnlineServerManager()`(`2015RemoteDlg.cpp:4758`):`BYTE bToken = COMMAND_SERVICES; SendSelectedCommand(&bToken, sizeof(BYTE));`。
- token 路由:`2015RemoteDlg.cpp:6609` `case TOKEN_SERVERLIST:` → `g_2015RemoteDlg->SendMessage(WM_OPENSERVICESDIALOG, 0, (LPARAM)ContextObject);`。
- 消息注册/处理:`ON_MESSAGE(WM_OPENSERVICESDIALOG, OnOpenServicesDialog)`(903 行)→ `OnOpenServicesDialog`(7811 行)→ `OpenDialog<CServicesDlg, IDD_DIALOG_SERVICES>`。
### 工具栏(读参考,全部已核实)
- 资源:`2015Remote.rc:1806` `IDR_TOOLBAR_MAIN TOOLBAR 48, 48`,现有 16 个 `BUTTON`,末尾为 `BUTTON ID_CHANGE_LANG`(1822 行)、`BUTTON ID_MCP_SETTINGS`(1823 行)。
- 真彩位图条:`IDB_BITMAP_MAIN`(133) → `res\ToolBar_Main.bmp`(**768×48 = 16 个 48×48 图标**,24 位),
`LoadTrueColorToolBar(48, IDB_BITMAP_MAIN, IDB_BITMAP_MAIN, IDB_BITMAP_MAIN)`(`2015RemoteDlg.cpp:1390`)
按 `nNbBtn = bmWidth/48` 建图像列表(`TrueColorToolBar.cpp` `SetTrueColorToolBar`),因此**新增第 17 个按钮必须同步把位图条加宽为 816×48 并补第 17 个图标**。
- 按钮文字:`SetButtonText(0..15)`(`2015RemoteDlg.cpp:1405–1420`),`SetButtonText(15, _T("MCP"))` 为当前最后一个。
- 全隐藏判定:`if (hiddenCount >= 16)`(1471 行)→ **无需改动**。该计数只统计「可隐藏功能按钮」
(终端/进程/窗口/桌面/文件/语音/视频/服务/注册表/键盘/设置/生成/搜索/关于,共 14 个),
`ID_CHANGE_LANG`、`ID_MCP_SETTINGS` 本就不在隐藏清单内;新增的连接按钮同样不参与隐藏,
`hiddenCount` 语义不变。
- 命令处理:`ON_COMMAND(ID_MCP_SETTINGS, &CMy2015RemoteDlg::OnMcpSettings)`(993 行)→ 参照加 `ID_CONNECTION_MANAGE`。
### 网络 API(客户端,新增依赖)
- 枚举:`GetExtendedTcpTable(TCP_TABLE_OWNER_PID_ALL, ...)` + `MIB_TCPTABLE_OWNER_PID`;
同理 `GetExtendedUdpTable(UDP_TABLE_OWNER_PID)`;IPv6 用 `AF_INET6` 版本(`MIB_TCP6TABLE_OWNER_PID` / `MIB_UDP6TABLE_OWNER_PID`)。
- 断开:`SetTcpEntry` + `MIB_TCP_STATE_DELETE_TCB`(12)。
- 链接:`iphlpapi.lib`(用 `#pragma comment(lib, "iphlpapi.lib")` 内联,与代码库既有 `#pragma comment(lib, ...)` 风格一致,避免改 vcxproj)。
## 3. 协议与数据结构
### 3.1 新命令(`common/commands.h`,追加在 `CMD_FILE_SPEED = 252` 之后,均 ≤ 255 满足单字节 token)
```
COMMAND_CONNECTION = 253, // 服务端 → 客户端:查询/刷新连接列表(打开连接管理)
COMMAND_KILL_CONNECTION = 254, // 服务端 → 客户端:强制断开指定连接
TOKEN_CONNECTION_LIST = 255, // 客户端 → 服务端:连接列表响应
```
### 3.2 单条连接记录(客户端 → 服务端,固定 110 字节,小端)
```c
#pragma pack(push, 1)
struct ConnectionInfo {
uint8_t proto; // 1=TCP, 2=UDP [1]
uint8_t af; // 2=AF_INET(IPv4), 23=AF_INET6(IPv6) [1]
uint32_t pid; // 所属进程 PID [4]
uint8_t localAddr[16]; // 网络字节序;IPv4 存 [0..3],IPv6 存 [0..15],其余 0 [16]
uint16_t localPort; // 主机字节序(小端,ntohs 转换后) [2]
uint8_t remoteAddr[16]; // 网络字节序,同上 [16]
uint16_t remotePort; // 主机字节序,同上 [2]
uint32_t state; // TCP: MIB_TCP_STATE_*;UDP: 0 [4]
char procName[64]; // 进程映像名 UTF-8,NUL 填充 [64]
}; // 合计 110
#pragma pack(pop)
static_assert(sizeof(ConnectionInfo) == 110, "ConnectionInfo must be 110 bytes");
```
> **字节序约定(评审后修正)**:字段不能「原样 memcpy」iphlpapi 的结构成员。`dwLocalAddr`/`dwRemoteAddr`
> 是**网络序 DWORD**(值 `0xC0A80101` 即 `192.168.1.1`),客户端展开为 16 字节字段前 4 字节的**网络序字节**
> (`[0xC0,0xA8,0x01,0x01]`),服务端 `inet_ntop` 可直接展示、排序键也按数值序;`dwLocalPort`/`dwRemotePort`
> 低 16 位为**网络序**,客户端用 `ntohs` 转**主机序**再存入。断连时反向还原(地址拼回 DWORD、端口 `htons`)。
> 地址统一 16 字节字段承载,IPv4/IPv6 用同一个结构,避免两套记录。
### 3.3 列表响应(客户端 → 服务端)
```
[TOKEN_CONNECTION_LIST:1][count:2][ConnectionInfo:110] * count
```
- `count` 为 `uint16_t` 小端,上限 65535(枚举超限则截断,实际主机连接数远低于此)。
- 列表 >1460 字节会走 HTTP 分片;服务端 `HeaderParser::Parse` 的 `m_bShouldUnmask` 分片 bug
已在 `a72e6eb` 修复,续包可正常到达(与进程/服务列表一致)。
### 3.4 强制断开请求(服务端 → 客户端)
```
[COMMAND_KILL_CONNECTION:1][ConnectionInfo:110]
```
- 客户端校验 `proto==1 && af==2` 才执行(仅 IPv4 TCP),否则忽略并在回包列表中保留该行。
## 4. 客户端实现
新增 `client/ConnectionsManager.{h,cpp}`,镜像 `CServicesManager`:
- 类声明(`ConnectionsManager.h`):
```cpp
#if ENABLE_CONNECTION_MNG==0
#define CConnectionsManager CManager
#else
class CConnectionsManager : public CManager {
public:
CConnectionsManager(IOCPClient* ClientObject, int n, void* user = nullptr);
virtual ~CConnectionsManager();
VOID SendConnectionList(); // 枚举并回传
VOID OnReceive(PBYTE szBuffer, ULONG ulLength);
void KillConnection(const ConnectionInfo& info);
};
#endif
```
- `SendConnectionList()`:
1. 依次 `GetExtendedTcpTable`(AF_INET / AF_INET6)+ `GetExtendedUdpTable`(AF_INET / AF_INET6),
汇总到 `std::vector<ConnectionInfo>`;IPv4 地址展开为网络序字节(`PutAddr4`)、端口 `ntohs` 转主机序。
2. 每个 PID 用 `OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION)` + `GetModuleBaseNameW` 取映像名,
转 UTF-8 写入 `procName[64]`;失败/PID 0 填 `"<unknown>"`(进程可能已退出,端点仍有效)。
3. 组包 `[TOKEN_CONNECTION_LIST:1][count:2][records...]`,用
`HttpMask mask(DEFAULT_HOST, m_ClientObject->GetClientIPHeader()); m_ClientObject->Send2Server(...);` 回传。
- `OnReceive()`:
- `case COMMAND_CONNECTION:` → `SendConnectionList()`(刷新)。
- `case COMMAND_KILL_CONNECTION:` → 校验长度 ≥ 111,读 `ConnectionInfo`,`KillConnection()`,再 `SendConnectionList()`。
- `KillConnection()`:仅 `proto==TCP && af==AF_INET`;组 `MIB_TCPROW`(`dwState=MIB_TCP_STATE_DELETE_TCB`),
地址由网络序字节拼回 DWORD(`GetAddr4`)、端口 `htons` 转回网络序,`SetTcpEntry(&row)`。
- 编译:`common/config.h` 增 `#define ENABLE_CONNECTION_MNG TRUE`(`// 连接管理`)。
- 分发(`client/KernelManager.cpp`):在 `case COMMAND_SERVICES:` 后加
```
case COMMAND_CONNECTION: {
if (!ENABLE_CONNECTION_MNG) return ResponseDisable(...);
{ auto* sub = new IOCPClient(...); sub->EnableSubConnAuth(); ...; }
m_hThread[m_ulThreadCount++].h = __CreateThread(..., LoopConnectionManager, ...);
break;
}
```
- 线程注册(`client/Common.cpp` / `Common.h`):`#define LoopConnectionManager LoopManager<CConnectionsManager, 0>`。
## 5. 服务端实现
新增 `server/2015Remote/ConnectionsDlg.{h,cpp}`,镜像 `CServicesDlg`:
- `CConnectionsDlg(CWnd*, Server*, CONTEXT_OBJECT*) : DialogBase(IDD_DIALOG_CONNECTIONS, pParent, IOCPServer, ContextObject, IDI_CONNECTION)`。
- `OnInitDialog`:`InsertColumnL` 建列 `协议 | 本地地址 | 本地端口 | 远端地址 | 远端端口 | 状态 | PID | 进程名`(8 列),调 `ShowConnectionList()`。
- `ShowConnectionList()`:解析 `m_ContextObject->InDeCompressedBuffer`(`[token:1][count:2][records...]`)填表,
每条 `SetItemData` 存行内偏移(或直接把 `ConnectionInfo` 存进项关联数据),供右键断连取回记录。
- `OnReceiveComplete()`:`case TOKEN_CONNECTION_LIST: ShowConnectionList()`。
- `OnNMRClickList()`:用 `pNMItemActivate->iItem` 取**鼠标右键所在行**(而非仅依赖已选中行),
由该行 `SetItemData` 里存的 `ConnectionInfo` 拿回记录;`LoadMenu(IDR_MENU_CONNECTIONS)` →
`TrackPopupMenu`;菜单项「强制断开」(`ID_CONNECTION_KILL`)对**仅 IPv4 TCP 行可用**,发
`[COMMAND_KILL_CONNECTION:1][record:110]`;UDP/IPv6 行禁用该菜单项(`MF_GRAYED`)。
- 对话框标题栏图标 `IDI_CONNECTION`(`.ico`,`DialogBase` 构造第 5 参)与工具栏位图条第 17 个
48×48 图标是两回事:前者复用现有图标(如 `IDI_SERVICE`)或新增一个 `.ico`;后者编辑
`ToolBar_Main.bmp` 补第 17 格(见下)。
- 提供刷新(补发 `COMMAND_CONNECTION`)与 `OnClose`→`CancelIO()`。
工具栏 / 消息路由(`2015RemoteDlg.{h,cpp}` + `Resource.h` + `.rc`):
- `Resource.h`:`IDD_DIALOG_CONNECTIONS 404`、`IDR_MENU_CONNECTIONS 405`、`IDI_CONNECTION`(新图标 ID)、
`ID_CONNECTION_MANAGE 33086`(`_APS_NEXT_COMMAND_VALUE` 现为 33086,用后 +2)、`ID_CONNECTION_KILL`;
同步上调 `_APS_NEXT_RESOURCE_VALUE`/`_APS_NEXT_COMMAND_VALUE`。
- `.rc`(UTF-16LE,Node 脚本改):`IDR_TOOLBAR_MAIN` 末尾加 `BUTTON ID_CONNECTION_MANAGE`;
新增 `IDD_DIALOG_CONNECTIONS` 对话框与 `IDR_MENU_CONNECTIONS` 菜单。
- `res\ToolBar_Main.bmp`:**加宽为 816×48,补第 17 个 48×48「连接」图标**(实现时用脚本合成一个
双节点/插头样式图标插入条尾;图标观感属评审项)。
- `2015RemoteDlg.cpp`:
- `SetButtonText(16, _TR("连接管理"))`(1420 行后)。
- `hiddenCount >= 16`(1471 行)**无需改动**(连接按钮不参与隐藏,理由见 §2)。
- `ON_COMMAND(ID_CONNECTION_MANAGE, &CMy2015RemoteDlg::OnConnectionManage)`(993 行后)。
- `OnConnectionManage()`:`BYTE bToken = COMMAND_CONNECTION; SendSelectedCommand(&bToken, sizeof(BYTE));`。
- `MessageHandle` 加 `case TOKEN_CONNECTION_LIST:` → `SendMessage(WM_OPENCONNECTIONDIALOG, 0, (LPARAM)ContextObject)`(6609 行后)。
- `ON_MESSAGE(WM_OPENCONNECTIONDIALOG, OnOpenConnectionDialog)` + `OnOpenConnectionDialog` →
`OpenDialog<CConnectionsDlg, IDD_DIALOG_CONNECTIONS>(wParam, lParam)`。
- `stdafx.h`:`#define WM_OPENCONNECTIONDIALOG WM_USER + 3045`(现最高占用 `WM_USER+3044`,已核实无冲突)。
- 语言:`lang/en_US.ini` / `zh_TW.ini`(GBK,Python 改)补「连接管理 / 强制断开 / 8 列表头」文案。
## 6. 强制断开的实现与限制
- **仅 IPv4 TCP** 用 `SetTcpEntry` + `MIB_TCP_STATE_DELETE_TCB`(公开 API 唯一支持)。
- **重构 `MIB_TCPROW` 要点**:`dwState=12`;`dwLocalAddr/dwRemoteAddr` 由 16 字节字段前 4 字节的
网络序字节拼回网络序 DWORD(`GetAddr4`);`dwLocalPort/dwRemotePort` 由主机序 `htons` 转回网络序。
- **UDP**:无「按端点关闭 socket」的公开 API,MVP 只读展示(菜单置灰)。
- **IPv6 TCP**:`SetTcpEntry` 的 `MIB_TCPROW` 为 IPv4 结构,无公开的 IPv6 对应删除 API,MVP 只读展示。
- **权限**:`SetTcpEntry` 需管理员/足够权限;YAMA 客户端通常以服务(SYSTEM)运行,满足。若非提权,
`SetTcpEntry` 返回错误,`KillConnection` 静默失败并在刷新后的列表中保留该行(可观察结果,不误报)。
## 7. 安全与边界
1. **可能断开 YAMA 自身控制通道**:连接列表包含客户端↔服务端的主/子连接本身,若管理员选中并强制断开,
对应主机会立即离线。对话框对此不做特殊保护(与 `kill_process` 可杀自身守护进程同理),
但列表按 PID 可辨识本程序进程名,便于人工避让。
2. **强制断开不可逆**:断的是对端已建立的 TCP 连接,应用层不保存状态,可能造成数据丢失;对话框不弹二次确认
(确认责任在操作者),但右键菜单为显式单点操作。
3. **枚举成本**:每行一次 `OpenProcess` 取进程名,数百行连接约毫秒~十毫秒级,可接受;`OpenProcess` 失败直接
`"<unknown>"`,不阻塞枚举。
4. **PID 复用 / 进程退出**:断连用的是**端点**而非 PID,进程退出不影响 `SetTcpEntry` 命中;`procName` 仅用于展示。
5. **HTTP 分片**:连接数多时列表可超 1460 字节,依赖已修复的分片续包处理(§3.3)。
6. **老版本客户端**:老客户端无 `COMMAND_CONNECTION` 处理,点工具栏会无响应(与「老客户端不支持活动历史/日志」
同类),属预期;不影响服务端其它功能。
## 8. 验证计划(施工后填写)
1. 服务端:MSBuild 加 `-p:SolutionDir` 编译通过(见项目记忆 yama-server-msbuild-solutiondir);客户端同步编译。
2. 部署客户端到真实 Windows 主机 → 服务端选中主机 → 点新图标 → 对话框列出 TCP/UDP(含 IPv4/IPv6)连接,
字段(协议/地址/端口/状态/PID/进程名)与远端 `netstat -ano` 一致。
3. 右键一条 **IPv4 TCP** 已建立连接(如对测试服务器的 ESTABLISHED)→ 强制断开 → 刷新后该行消失、
远端 `netstat` 不再显示;进程自身未受影响。
4. 右键 **UDP / IPv6** 行 → 菜单项置灰,不可断连。
5. 刷新按钮 / 重复打开对话框 → 列表更新正常;关对话框后子连接释放(无泄漏)。
6. 回归:服务管理 / 进程管理 / MCP 等既有功能不受影响(无 command/token/ID 冲突)。
## 9. 实现记录
- 已实现并评审通过(2026-10-04)。实现与 §4/§5 一致:工具栏第 17 个图标(`ID_CONNECTION_MANAGE`,紧邻 MCP 之后)
打开连接管理对话框;客户端新增 `CConnectionsManager` 枚举并回传本机 TCP/UDP 连接;服务端新增 `CConnectionsDlg`
表格展示、右键「强制断开」发 `COMMAND_KILL_CONNECTION`、支持表头点击排序。
- **评审修正一处关键缺陷(字节序)**:iphlpapi 的 IPv4 地址 `dwLocalAddr` 是网络序 DWORD、端口 `dwLocalPort`
低 16 位是网络序。初版「原样 memcpy / 原样回填」导致展示的 IPv4 地址与端口字节序颠倒(强断因往返对称仍能命中)。
已改为:地址存网络序字节(`PutAddr4`)、端口 `ntohs` 转主机序;断连时地址拼回 DWORD(`GetAddr4`)、端口
`htons` 转回网络序。详见 §3.2 / §6。
- 对话框标题栏小图标 `IDI_CONNECTION` 由工具栏第 17 个图标生成 `res/connections.ico`(48/32/16,32 位带透明)。
- 编译:`client/ConnectionsManager.cpp`(UTF-8 BOM + CRLF,`ws2tcpip.h` 引入 `_WS2IPDEF_` 以使用 IPv6 MIB 结构)
经 MSBuild(v143 工具集覆盖)编译通过;整仓 ghost 构建在本机 BuildTools 因缺少 ATL 头受阻,属环境问题,与本功能无关。
+146
View File
@@ -0,0 +1,146 @@
# YAMA 文件传输速度调节设计
> **状态**:已实现(Windows 端,含多语言)。其他平台(Linux/macOS 客户端)暂不实施。
> **读者**:研发/评审人员。
> **关联文档**:[FILE_TRANSFER_V2.md](./FILE_TRANSFER_V2.md)(V2 协议)、[Mcp_FileTransfer_Design.md](./Mcp_FileTransfer_Design.md)(MCP 文件传输)。
---
## 1. 背景与目标
文件传输速度目前是**写死的**:`SimplePlugins` 库的 `InitFileUpload(key, msg, hmac, chunkSizeKb, sendDurationMs, ...)` 被 YAMA 所有调用点传 `64, 50`,折算 ≈ 1.25 MB/s。用户无法调节。
目标:在 YAMA 主菜单「高级」下新增一个**面向小白**的「文件传输速度」对话框——拖动滑块即完成设置,实时显示预估速度;**不修改 SimplePlugins 库**(库已编译,只通过其已暴露的 `chunkSizeKb`/`sendDurationMs` 两个入参控制)。
---
## 2. 速度原理(现状)
库内部把两个入参转成全局量(`SimplePlugins/file_upload.cpp:221-222, 849-850`):
```cpp
static int chunk_size = 64 * 1024; // 默认 64KB
static int send_duration = 50; // 默认 50ms
// InitFileUpload: chunk_size = chunkSizeKb * 1024; send_duration = sendDurationMs;
```
发送循环每发一块就 `Sleep(send_duration)`(V1 `FileBatchTransferWorker` 与 V2 `FileBatchTransferWorkerV2` 共用这对全局量):
```
预估速度 = chunkSizeKb * 1024 / (sendDurationMs / 1000) 字节/秒
默认 64KB / 50ms ≈ 1.25 MB/s
```
两个关键约束:
1. **速度只在 `InitFileUpload` 时设定一次**,库无运行时 setter(`g_threadCount > 1` 时提前返回,不会重设)。
2. **V1 / V2 共用同一对全局量**,一次设置同时覆盖 `COMMAND_SEND_FILE(68)` 与 `COMMAND_SEND_FILE_V2(85)` 两种协议。
---
## 3. 档位表(权威唯一来源,主控端对话框维护)
| 档位 | 名称 | chunkKb | durationMs | 预估速度 |
|---|---|---|---|---|
| 0 | 最慢 | 64 | 200 | ≈ 0.3 MB/s |
| 1 | 慢 | 64 | 100 | ≈ 0.6 MB/s |
| 2 | 默认 | 64 | 50 | ≈ 1.25 MB/s |
| 3 | 快 | 128 | 50 | ≈ 2.5 MB/s |
| 4 | 更快 | 256 | 50 | ≈ 5 MB/s |
| 5 | 很快 | 512 | 50 | ≈ 10 MB/s |
| 6 | 不限速 | 1024 | 0 | 网络上限 |
- 滑块 `CSliderCtrl` 范围 `0..6`,`SetPageSize(1)`,默认落在 2。
- `durationMs = 0` 表示不人为限速(`Sleep(0)` 仅让出时间片,实际受带宽约束)。
- **注意 Windows `Sleep` 粒度 ≈ 15.6ms**:`durationMs` 低于约 15ms 基本等同不限速。故档位要么 ≥ 20ms,要么直接 0ms,不设 5ms 之类的“名不副实”中间值。
预估文案:`当前约 1.25 MB/s`;`durationMs==0` 显示 `不限速(受网络带宽约束)`。
- **多语言**:档位名(最慢/慢/默认/快/更快/很快/不限速)与预估格式串(`%s · 约 %.2f MB/s`、`%s · 不受限速(受网络带宽约束)`)经 `_TR`/`_LF` 走 `LangManager` 翻译表;语言文件以中文原文为 key。刻度标签拆成 最慢/默认/不限速 三个独立 `LTEXT` 以便各自翻译(英文宽度不同,不能靠空格对齐)。
---
## 4. 两个方向的生效链路
文件传输有两条方向,`InitFileUpload` 各自在不同进程/时机调用:
| 方向 | 发送方 | `InitFileUpload` 调用点 | 生效方式 |
|---|---|---|---|
| 上传(主控端 → 被控端) | 主控端 | `server/2015Remote/2015RemoteDlg.cpp:2223`(启动一次) | 读 `THIS_CFG`,重启主控端生效 |
| 下载(被控端 → 主控端) | 被控端 | `client/FileManager.cpp:40`(CFileManager 构造,每会话一次) | 读客户端配置,下次打开文件管理会话生效 |
### 4.1 上传方向(主控端本地,无协议改动)
`2015RemoteDlg.cpp:2223` 现为 `InitFileUpload({},{},GetHMAC(),64,50,Logf,GetUpperHash())`,改为读 `THIS_CFG`:
```cpp
int chunkKb = THIS_CFG.GetInt("transfer", "ChunkKb", 64);
int durMs = THIS_CFG.GetInt("transfer", "DurationMs", 50);
InitFileUpload({}, {}, GetHMAC(), chunkKb, durMs, Logf, GetUpperHash());
```
### 4.2 下载方向(需下发到被控端)
新增命令 `CMD_FILE_SPEED`,照 `CMD_QUALITY_LEVEL(78)` 的“服务端下发 → 客户端持久化 → 本地生效”套路:
1. **`common/commands.h`**:新增 `CMD_FILE_SPEED = 252`(命令号单字节,252 < 255 安全)。
2. **命令格式**:`[cmd:1][chunkKb:uint16 LE][durationMs:uint16 LE]`,共 5 字节。
- 传原始 `chunkKb`/`durationMs` 而非档位索引,**避免客户端复制映射表**,映射表只存在于主控端对话框。
3. **主控端下发**:在 `OnOnlineFileManager()`(`server/2015Remote/2015RemoteDlg.cpp:4737`,发 `COMMAND_LIST_DRIVE` 开文件管理会话)发 `COMMAND_LIST_DRIVE` **之前**,先经主连接把当前档位 `CMD_FILE_SPEED`(5 字节)发给被控端;`OnOnlineDesktopManager()`(`2015RemoteDlg.cpp:4723`,发 `COMMAND_SCREEN_SPY`)同理也发一次,覆盖“先开远程桌面再开文件管理”的引用计数时序。两处都在 `SendFileSpeedCommand()`(`:9310`)统一读取并下发。
4. **被控端接收**:在客户端主连接分发 `client/KernelManager.cpp:814` 的 `switch`(`COMMAND_LIST_DRIVE` 所在,`case` 加在 `1280` 附近)加 `case CMD_FILE_SPEED`,把两数写入 `iniFile cfg(CLIENT_PATH)` 的 `[transfer]` 节。
5. **被控端调用点读配置**:`client/FileManager.cpp:40`(CFileManager 构造)改为读 `cfg.GetInt("transfer","ChunkKb",64)` / `DurationMs`,替换硬编码 `64, 50`。CFileManager 每会话重建(构造 Init、析构 Uninit),故下次打开文件管理即生效。
> **关键**:库用进程级引用计数,速度只在**首次** `InitFileUpload` 时设定(`g_threadCount > 1` 时提前返回,不重设)。客户端共 4 个 `InitFileUpload` 调用点(`FileManager.cpp:40`、`ScreenManager.cpp:109`、`ScreenManager.cpp:873`、`KernelManager.cpp:1399`),谁先初始化谁锁定速度。故**这 4 处必须全部改为读同一配置**,否则下载速度会被先初始化的管理器(如远程桌面会话)锁成旧值。
---
## 5. 配置存储
- 主控端:`THIS_CFG`(ini)`[transfer]` 节 `ChunkKb` / `DurationMs`(int)。
- 被控端:`iniFile(CLIENT_PATH)` 的 `[transfer]` 节 `ChunkKb` / `DurationMs`(int),由 `CMD_FILE_SPEED` 写入。
存 `ChunkKb`/`DurationMs` 两个 int 而非档位索引:自解释、直接对应 `InitFileUpload` 入参、不易错位。
---
## 6. 改动文件清单(Windows 端)
| 文件 | 改动 |
|---|---|
| `server/2015Remote/resource.h` | 加 `IDD_FILE_SPEED`、`ID_TOOL_FILE_SPEED`、`IDC_FSPEED_SLIDER`、`IDC_FSPEED_LABEL`、`IDC_FSPEED_RESET` |
| `server/2015Remote/2015Remote.rc` | 「高级」菜单加一项;新增对话框资源(滑块+预估标签+按钮)。**文件为 UTF-16LE 编码,须按 Unicode 读写** |
| `server/2015Remote/CFileSpeedDlg.h` | 新建:档位表 + `OnHScroll` 实时预估 + 恢复默认(照 `CPreviewLevelsDlg.h` 内联实现) |
| `server/2015Remote/2015RemoteDlg.h` | 加 `OnFileTransferSpeed()` 声明 |
| `server/2015Remote/2015RemoteDlg.cpp` | 消息映射 `ON_COMMAND(ID_TOOL_FILE_SPEED, ...)`;`2223` 读配置(上传);`OnFileTransferSpeed()` 弹出对话框并写回 THIS_CFG;`OnOnlineFileManager()`/`OnOnlineDesktopManager()` 发 `CMD_FILE_SPEED` |
| `common/commands.h` | 加 `CMD_FILE_SPEED = 252` |
| `client/KernelManager.cpp` | `case CMD_FILE_SPEED` 写客户端配置(主连接 switch,`814`) |
| `client/FileManager.cpp`、`client/ScreenManager.cpp`、`client/KernelManager.cpp` | 4 个 `InitFileUpload` 调用点读配置替换硬编码 `64, 50`(引用计数要求全部一致) |
| `server/2015Remote/lang/en_US.ini`、`zh_TW.ini`、`de_DE.ini` | 追加「文件传输速度」相关 13 条中文原文→译文(GBK 编码,`RCDATA "lang\*.ini"` 由 rc.exe 嵌入) |
---
## 7. 生效时机与提示文案
- 上传速度:重启主控端生效。
- 下载速度:下次打开该主机的文件管理会话生效。
- 对话框保存后弹提示:`文件传输速度已保存。上传速度重启主控端后生效;下载速度下次打开文件管理或远程桌面后生效。`
---
## 8. 风险与备注
1. **`Sleep` 精度**:Windows 计时器粒度约 15.6ms,`durationMs` 过小则失真(见 §3)。
2. **实际速度 < 预估**:预估为纯 `chunk/delay` 理论值,未计入 `f()` 网络发送耗时;对话框文案用“约/大概”表述。
3. **命令号冲突**:`CMD_FILE_SPEED=252` 需确认 `commands.h` 无重号;252 < 255(单字节 `uint8_t cmd` 上限)。
4. **`.rc` 编码**:`2015Remote.rc` 为 UTF-16LE(带 BOM),编辑必须按 Unicode 处理,避免破坏资源脚本。
5. **跨平台**:Linux/macOS 客户端下载路径暂不改(`CMD_FILE_SPEED` 会被老/其他平台客户端静默忽略,向后兼容)。
6. **多语言 key 精确匹配**:语言文件为 GBK 编码、key 不 trim(`CIniParser` 保留原始空格);译文 key 必须与源码逐字一致——源码为 UTF-8、经 `/execution-charset:.936` 编译为 GBK,`·`(U+00B7)、全角括号()等需一致,否则查不到翻译。
---
## 9. 实施顺序
1. `common/commands.h` 加命令号 → `resource.h` 加 ID → `.rc` 加菜单项与对话框资源。
2. 新建 `CFileSpeedDlg.h` → `2015RemoteDlg` 接线(上传方向,可先验证)。
3. 主控端下发 `CMD_FILE_SPEED` → 客户端接收持久化 → 客户端调用点读配置(下载方向)。
4. 编译验证(MSBuild 须传 `-p:SolutionDir`,见项目记忆)。
+219
View File
@@ -0,0 +1,219 @@
# MCP P7 设计:设备管理控制(kill / window / service / power)
> 延续 `Mcp_Design.md` → `Mcp_Phase2_Design.md`(P2)→ P3/P4/P5/P6 的阶段演进。
> 本文定义 P7:把 YAMA 服务端高频「控制类」操作 MCP 化,补上从「观测」到「管控」的短板,
> 服务「自然语言即可操作服务端、对受管设备全方位管控」的最终目标。
>
> 安全门结论(已确认):新增**单一 `McpManage` 开关**统一门控本阶段全部控制类工具,
> 并要求 `McpReadonly=0`;不再为每个工具单设开关。
## 1. 目标与范围
P7 交付 4 个控制类工具,覆盖服务端日常最常用的写操作:
| 工具 | 动作 | 命令 | 连接 | 确认方式 |
|---|---|---|---|---|
| `kill_process` | 结束进程(多 pid) | `COMMAND_KILLPROCESS` | 系统子连接 | 补发 `COMMAND_PSLIST` 收新列表 |
| `operate_window` | 关闭/显示/隐藏/最小化/最大化/还原 | `CMD_WINDOW_CLOSE` / `CMD_WINDOW_TEST` | 窗口子连接 | close 自动回 `TOKEN_WSLIST`;test 补发 `COMMAND_WSLIST` |
| `control_service` | 启动/停止/设为自动/设为手动 | `COMMAND_SERVICECONFIG` | 服务子连接 | 客户端每项 Sleep(500) 后自动 `SendServicesList` |
| `power_action` | 注销/关机/重启 | `TOKEN_MACHINE_MANAGE` | 主连接 | 发完即走(无响应) |
仅 Windows 客户端支持(`clientType` 为 LNX/MAC/**APK** 提前返回 `-32005`;注意 `list_services`
只排除了 LNX/MAC、漏掉 APK,P7 需把 APK 一并排除)。
## 2. 复用现状
P7 **不改客户端**。客户端已全部实现下列处理(MFC 界面在跑),MCP 侧只需补齐
「发命令 → 接管子连接 → 发写命令 → 收确认 → 关闭」这条链:
- 系统子连接:`COMMAND_SYSTEM`(进程)/ `COMMAND_WSLIST`(窗口)→ 客户端开 `CSystemManager`
子连接(`client/KernelManager.cpp` 1169/1182,`client/SystemManager.cpp`)。
- `COMMAND_KILLPROCESS`:`CSystemManager::KillProcess`(`TerminateProcess`,无回包)。
- `CMD_WINDOW_CLOSE`:`PostMessage(hWnd, WM_CLOSE)` 后自动 `SendWindowsList`。
- `CMD_WINDOW_TEST`:`ShowWindow(hwnd, dHow)`(不自动回包)。
- `COMMAND_SERVICECONFIG`:`CServicesManager::ServicesConfig`(`client/ServicesManager.cpp` 213),
bCmd=1 启动 / 2 停止 / 3 自动 / 4 手动,每项后 `Sleep(500)` + `SendServicesList`。
- `TOKEN_MACHINE_MANAGE`:`KernelManager.cpp` 872,`ExitWindowsEx`(注销/关机/重启)。
复用现有 MCP 基建(`server/2015Remote/McpServer.{h,cpp}`):
- `ParseHostIdArg` / `FindMainContext`:入参解析 + 主连接定位。
- `BeginPending` / `WaitPending` / `ClearPending` / `TakeMainResponse`:单飞行挂起 + 响应回收。
- `OnRegeditReady`:已示范「接管子连接 → 发后续命令」的范式(P3 list_registry 三阶段),P7 直接泛化。
## 3. 子连接生命周期(P7 核心新增)
写工具与只读工具的关键差异:只读工具「子连接回一包即完成」,写工具需要
「**接管子连接 → 发写命令 → 收确认 → 关闭**」。在现有 `PendingRequest` 上增加
`followUp`(接管后要发的写命令 payload,二进制)+ `followUpSent`(是否已发写命令)两个字段:
```
followUpSent == false → 等子连接就绪(首包 TOKEN_*LIST 到达 = 就绪)
followUpSent == true → 已发写命令,等确认包
```
> 关键事实(已核实):三个工具各自的「就绪 token」与「确认 token」**是同一个 token**——
> 客户端子连接构造即回一包(就绪),写操作后再回一包(确认)。因此必须用 `followUpSent`
> 区分这两次到达,而不能靠 token 区分。这也是它与 `list_registry`(PATH/KEY 两个不同 token)
> 的差别。
流程(以 `kill_process` 为例):
1. 工具线程 `BeginPending(devId, "kill_process")` 登记单飞行(false = 设备忙),并预填
`followUp`(`COMMAND_KILLPROCESS` 多 pid 包)。
2. 主连接发 `COMMAND_SYSTEM`。
3. 客户端开系统子连接,首包 `TOKEN_PSLIST` 到达 → `MessageHandle` 调新增的
`OnSystemReady(devId, subCtx)`(类比 `OnRegeditReady`):
- 命中挂起且 `followUpSent==false` → 保存 `subCtx`、置 `followUpSent=true`、向子连接发
`followUp`,随后补发 `COMMAND_PSLIST` 索取新列表。
4. 客户端杀进程后回第二个 `TOKEN_PSLIST` → `TakeMainResponse` 取走(`followUpSent==true`
命中)→ 关闭子连接(`CancelIO`)→ 唤醒工具线程。
5. 工具线程解析新进程列表返回;超时/失败走 `ClearPending`(**含 `subCtx->CancelIO()`**,防孤儿子连接)。
> 注意:`followUp`(写命令)与「补发的 `COMMAND_PSLIST`/`COMMAND_WSLIST`」必须是**两次独立的
> `Send2Client` 调用**,不能拼成一个 buffer——客户端 `OnReceive` 按 `szBuffer[0]` 单命令分发,
> 拼包会把后续字节当成 pid/hwnd 解析。
接管函数(进程/窗口共用同一个 `OnSystemReady`——两者都在同一个 `CSystemManager` 子连接上,
客户端 `OnReceive` 统一处理 `COMMAND_PSLIST`/`COMMAND_KILLPROCESS`/`COMMAND_WSLIST`/
`CMD_WINDOW_CLOSE`/`CMD_WINDOW_TEST`):
| 工具 | 开子连接命令 | 就绪包(首包) | 接管后写命令 | 确认包(第二包) |
|---|---|---|---|---|
| `kill_process` | `COMMAND_SYSTEM` | `TOKEN_PSLIST` | `COMMAND_KILLPROCESS` + 补 `COMMAND_PSLIST` | `TOKEN_PSLIST` |
| `operate_window` | `COMMAND_WSLIST` | `TOKEN_WSLIST` | `CMD_WINDOW_CLOSE`(close 自动回);`CMD_WINDOW_TEST`(test 补 `COMMAND_WSLIST`) | `TOKEN_WSLIST` |
| `control_service` | `COMMAND_SERVICES` | `TOKEN_SERVERLIST` | `COMMAND_SERVICECONFIG`(客户端写后自动 `SendServicesList`) | `TOKEN_SERVERLIST` |
`MessageHandle` 现有分派点(均已核实):
- `TOKEN_PSLIST`/`TOKEN_WSLIST` 分支:`2015RemoteDlg.cpp:6540`(当前 `TakeMainResponse`+`CancelIO`)
- `TOKEN_SERVERLIST` 分支:`2015RemoteDlg.cpp:6585`(同上)
接入方式:在这两个分支的 `IsPending(devId)` 命中后,**先按 `pending.tool` 判断是否为
写工具(kill/operate/control_service)**;是则调 `OnSystemReady`/`OnServiceReady`(返回 true
= 已接管、不 `CancelIO`),否则沿用现有 `TakeMainResponse`+`CancelIO`。与 `TOKEN_REGEDIT`
分支调用 `OnRegeditReady`(`2015RemoteDlg.cpp:6561`)的写法一致,避免与人工界面抢子连接。
## 4. 工具 schema
### 4.1 `kill_process`
```jsonc
// inputSchema
{ "id": "string(必填,主机 id)",
"pids": ["integer 数组(必填,至少 1 个进程 PID)"] }
// outputSchema
{ "remaining": ["integer 数组(杀完后仍在运行的 PID)"] }
```
- payload:`[COMMAND_KILLPROCESS:1][pid:4]*N`(DWORD 小端,与 `SystemDlg::OnPlistKill` 一致)。
- 成功判定:返回 `remaining`(发 `COMMAND_PSLIST` 后进程列表里已不含目标 pid 即视为成功)。
- 描述需写明:`TerminateProcess` 强制结束、不保存进程状态,可能造成数据丢失。
### 4.2 `operate_window`
```jsonc
// inputSchema
{ "id": "string(必填)",
"hwnd": "integer(必填,窗口句柄,取 list_windows 返回的 hwnd)",
"action": "string(必填)close|show|hide|minimize|maximize|restore" }
// outputSchema
{ "ok": "boolean" }
```
- payload(与 `SystemDlg::OnWlistClose/OnWlistHide/OnWlistRecover/OnWlistMax/OnWlistMin` 一致,
**固定 20 字节零填充 `BYTE lpMsgBuf[20]`,hwnd 一律按 `DWORD`(4 字节)小端写入 offset 1**):
- `close` → `[CMD_WINDOW_CLOSE:1][hwnd:4]`(客户端 `PostMessage WM_CLOSE`)。
- 其余 → `[CMD_WINDOW_TEST:1][hwnd:4][dHow:4]`,dHow 映射(MFC 实测值):
`hide=SW_HIDE(0)` / `show=SW_NORMAL(1)` / `restore=SW_NORMAL(1)` /
`maximize=SW_MAXIMIZE(3)` / `minimize=SW_MINIMIZE(6)`。
- 缓冲器必须补零到 20 字节:客户端 `close` 分支读 `*((HWND*)(szBuffer+1))`,在 x64 是 8 字节,
而服务端只写了 4 字节 hwnd——正是 20 字节零填充补齐了高 4 字节,读出的 HWND 才是正确值;
若 MCP 发最小 5 字节包,会越界读。`test` 分支(`TestWindow`)读 `[hwnd:4][dHow:4]` 共 9 字节,
20 字节缓冲也足够。MFC 固定发 `lpMsgBuf[20]` 正是为此,MCP 照搬。
### 4.3 `control_service`
```jsonc
// inputSchema
{ "id": "string(必填)",
"service_name": "string(必填,服务名,取 list_services 返回的 service_name)",
"action": "string(必填)start|stop|set_auto|set_manual" }
// outputSchema
{ "ok": "boolean" }
```
- payload:`COMMAND_SERVICECONFIG [cmd:1][bCmd:1][serviceName\0]`,
bCmd:`start=1` / `stop=2` / `set_auto=3` / `set_manual=4`。
- 边界:客户端 `ServicesManager` 仅实现这 4 项;**无** pause / continue / delete
(它们属于 `MachineDlg` 机器管理的另一条链路,留待 P8 评估)。
### 4.4 `power_action`
```jsonc
// inputSchema
{ "id": "string(必填)",
"action": "string(必填)logout|shutdown|reboot" }
// outputSchema
{ "ok": "boolean" }
```
- payload:`TOKEN_MACHINE_MANAGE [token:1][type:1]`,type:`logout=0` / `shutdown=1` / `reboot=2`
(`enum MachineCommand` 顺序)。MFC 发的是 32 字节缓冲(`2015RemoteDlg::MachineManage`),
但客户端只校验 `ulLength>1` 并读 `szBuffer[1]`,故 2 字节即可。
- 走**主连接**、发完即走、无响应;主机将立即离线。描述需明确「不可撤销、主机下线」。
- 需客户端管理员权限(`EnableShutdownPrivilege`),无权限时客户端静默忽略(同 MFC 现状)。
- **仅 Windows 客户端**:Linux 侧文档写的是 `COMMAND_SESSION`(`linux/README.md`),但代码中
未发现对应 handler,即 Linux 实际未实现电源操作——`power_action` 对 LNX/MAC/APK 一律返回
`-32005`,不尝试发送。
## 5. 安全门(单一 `McpManage` 开关)
- 新增配置键 `settings/McpManage`(默认 `0`,关闭)。
- 生效条件:`McpManage=1` 且 `McpReadonly=0`;二者任一不满足即隐藏 4 个工具。
- 双保险:`tools/list` 隐藏 + 每个 `BuildXxx` 入口二次校验(与 `exec_command` 的
`IsReadonly()` 双保险一致)。
- `McpSettingsDlg` 增加「管理操作(进程/窗口/服务/电源)」复选框,写入 `THIS_CFG`。
- 审计:每次写操作写入服务端审计日志(`get_audit_log` 可见),记录 tool + host id + 参数摘要 + 结果。
## 6. 单飞行与并发约束
- 沿用现有约束:同 host 同一时刻只允许一个 MCP 挂起请求(`BeginPending` 保证)。
- 写工具与只读工具共用 `m_Pending`,天然互斥;与 MFC 对话框并存(各用独立子连接,互不干扰)。
- 超时沿用 `kMcpToolTimeoutMs = 20000`;`power_action` 因无响应、发完即返回成功。
## 7. 边界与风险
1. **杀进程/关机/重启不可逆**:描述与审计都需显著标注;不提供「确认对话框」,
确认责任在调用方(MCP 客户端/LLM agent)。
2. **kill 无客户端回执**:成功与否靠「新进程列表不含目标 pid」推断,`TerminateProcess`
失败(权限不足等)时进程仍在 `remaining`,由调用方判断。
3. **服务操作仅 4 项**:pause/continue/delete 缺省,如需需评估 `MachineDlg` 链路。
4. **窗口句柄时效**:hwnd 可能已失效(窗口已关闭),客户端 `PostMessage/ShowWindow`
对失效句柄静默无操作,`operate_window` 的 `ok=true` 只表示「已下发」。
5. **子连接竞态**:写工具接管子连接后若客户端断线,`OfflineProc` 调 `OnManageSubClosed`
清理挂起(置 done 并清 `subCtx`、唤醒等待线程;**不擦表**,由 `WaitPendingWrite` 自行擦除,
防迭代器失效)。清 `subCtx` 是关键——context 对象被连接池回收复用,若超时路径仍对旧指针
`CancelIO` 会误伤新连接(与 `OnTerminalClosed`/`OnScreenControlClosed` 同理)。
## 8. P8 / P9 路线图(后续)
- **P8 文件与信息面**:`delete_file/delete_directory`、`create_folder`、`rename_file`、
`open_file`;`list_network_connections`、`list_software`、`get/set_hosts`、
`list_scheduled_tasks`(+增删启停)、`get_keylog`。
- **P9 进阶与 NL 增强**:`set/delete_registry`、`screen_blank`/`lock_input`/`get_clipboard`、
`open_url`/`send_message`;**批量操作**(`ids[]`/`group` 维度);离线主机枚举
`list_all_hosts`。
## 9. 实现与验证记录(2026-10-01)
已实现并端到端验证(真实客户端 DESKTOP-A55C0OI / Windows 10 / VMware,id=3441387693643366974):
- `kill_process` → `remaining:[]`;`operate_window`(restore) → `ok:true`;
`control_service`(set_manual AJRouter) → `ok:true`;`power_action`(reboot) → `ok:true`(发完即走)。
三个写工具的确认包(第二个 `TOKEN_*LIST`)均正常到达,列表均 >1460 字节触发 HTTP 分片。
- 落地:`McpManage` 开关 + `McpReadonly=0` 双保险;`McpSettingsDlg` 增「管理操作」复选框;
主工具栏增 `MCP` 入口按钮(`ID_MCP_SETTINGS`);en/de/zh-TW 三语文案同步。
- 前置解析 bug(验证中暴露,已单独提交 `a72e6eb`):`HeaderParser::Parse` 的
`m_bShouldUnmask` 在 HTTP 分片续包上被误置 `FALSE`,导致写工具的确认包被
`PARSER_FAILED` 丢弃(「首包能到、后续包到不了」);修复为 `else if (m_bShouldUnmask != TRUE)`。
Binary file not shown.
+237 -17
View File
@@ -20,6 +20,7 @@
#include "AudioDlg.h" #include "AudioDlg.h"
#include "RegisterDlg.h" #include "RegisterDlg.h"
#include "ServicesDlg.h" #include "ServicesDlg.h"
#include "ConnectionsDlg.h"
#include "VideoDlg.h" #include "VideoDlg.h"
#include <vector> #include <vector>
#include <map> #include <map>
@@ -27,6 +28,7 @@
#include <filesystem> #include <filesystem>
#include "KeyBoardDlg.h" #include "KeyBoardDlg.h"
#include "InputDlg.h" #include "InputDlg.h"
#include "ShareHostDlg.h"
#include "CPasswordDlg.h" #include "CPasswordDlg.h"
#include "FeatureFlags.h" #include "FeatureFlags.h"
#include "FeatureLimitsDlg.h" #include "FeatureLimitsDlg.h"
@@ -43,6 +45,9 @@
#include <sstream> #include <sstream>
#include "common/skCrypter.h" #include "common/skCrypter.h"
#include "common/commands.h" #include "common/commands.h"
#include "QualityLevelsDlg.h"
#include "CPreviewLevelsDlg.h"
#include "CFileSpeedDlg.h"
#include "McpServer.h" #include "McpServer.h"
#include "McpSettingsDlg.h" #include "McpSettingsDlg.h"
#include "common/md5.h" #include "common/md5.h"
@@ -921,6 +926,7 @@ BEGIN_MESSAGE_MAP(CMy2015RemoteDlg, CDialogEx)
ON_MESSAGE(WM_ANTI_BLACKSCREEN, AntiBlackScreen) ON_MESSAGE(WM_ANTI_BLACKSCREEN, AntiBlackScreen)
ON_MESSAGE(WM_OPEN_WINDOW_SCREEN, OpenWindowScreen) ON_MESSAGE(WM_OPEN_WINDOW_SCREEN, OpenWindowScreen)
ON_MESSAGE(WM_OPENCLIENTLOGDIALOG, OnOpenClientLogDialog) ON_MESSAGE(WM_OPENCLIENTLOGDIALOG, OnOpenClientLogDialog)
ON_MESSAGE(WM_OPENCONNECTIONDIALOG, OnOpenConnectionDialog)
ON_MESSAGE(WM_SHARE_CLIENT, ShareClient) ON_MESSAGE(WM_SHARE_CLIENT, ShareClient)
ON_MESSAGE(WM_ASSIGN_CLIENT, AssignClient) ON_MESSAGE(WM_ASSIGN_CLIENT, AssignClient)
ON_MESSAGE(WM_ASSIGN_ALLCLIENT, AssignAllClient) ON_MESSAGE(WM_ASSIGN_ALLCLIENT, AssignAllClient)
@@ -987,6 +993,9 @@ BEGIN_MESSAGE_MAP(CMy2015RemoteDlg, CDialogEx)
ON_COMMAND(ID_TOOL_RELOAD_PLUGINS, &CMy2015RemoteDlg::OnToolReloadPlugins) ON_COMMAND(ID_TOOL_RELOAD_PLUGINS, &CMy2015RemoteDlg::OnToolReloadPlugins)
ON_COMMAND(ID_TOOL_PLUGIN_SETTINGS, &CMy2015RemoteDlg::OnToolPluginSettings) ON_COMMAND(ID_TOOL_PLUGIN_SETTINGS, &CMy2015RemoteDlg::OnToolPluginSettings)
ON_COMMAND(ID_MCP_SETTINGS, &CMy2015RemoteDlg::OnMcpSettings) ON_COMMAND(ID_MCP_SETTINGS, &CMy2015RemoteDlg::OnMcpSettings)
ON_COMMAND(ID_CONNECTION_MANAGE, &CMy2015RemoteDlg::OnConnectionManage)
ON_COMMAND(ID_TALK_MANAGE, &CMy2015RemoteDlg::OnChat)
ON_COMMAND(ID_MACHINE_MANAGE, &CMy2015RemoteDlg::OnMachineManager)
ON_COMMAND(ID_TRIGGER_SETTINGS, &CMy2015RemoteDlg::OnTriggerSettings) ON_COMMAND(ID_TRIGGER_SETTINGS, &CMy2015RemoteDlg::OnTriggerSettings)
ON_COMMAND(ID_SHELLCODE_AES_C_ARRAY, &CMy2015RemoteDlg::OnShellcodeAesCArray) ON_COMMAND(ID_SHELLCODE_AES_C_ARRAY, &CMy2015RemoteDlg::OnShellcodeAesCArray)
ON_COMMAND(ID_PARAM_KBLOGGER, &CMy2015RemoteDlg::OnParamKblogger) ON_COMMAND(ID_PARAM_KBLOGGER, &CMy2015RemoteDlg::OnParamKblogger)
@@ -1013,6 +1022,9 @@ BEGIN_MESSAGE_MAP(CMy2015RemoteDlg, CDialogEx)
ON_COMMAND(ID_TOOL_IMPORT_LICENSE, &CMy2015RemoteDlg::OnToolImportLicense) ON_COMMAND(ID_TOOL_IMPORT_LICENSE, &CMy2015RemoteDlg::OnToolImportLicense)
ON_COMMAND(ID_TOOL_V2_PRIVATEKEY, &CMy2015RemoteDlg::OnToolV2PrivateKey) ON_COMMAND(ID_TOOL_V2_PRIVATEKEY, &CMy2015RemoteDlg::OnToolV2PrivateKey)
ON_COMMAND(ID_MENU_NOTIFY_SETTINGS, &CMy2015RemoteDlg::OnMenuNotifySettings) ON_COMMAND(ID_MENU_NOTIFY_SETTINGS, &CMy2015RemoteDlg::OnMenuNotifySettings)
ON_COMMAND(ID_TOOL_QUALITY_LEVELS, &CMy2015RemoteDlg::OnQualityLevels)
ON_COMMAND(ID_TOOL_PREVIEW_LEVELS, &CMy2015RemoteDlg::OnPreviewLevels)
ON_COMMAND(ID_TOOL_FILE_SPEED, &CMy2015RemoteDlg::OnFileTransferSpeed)
ON_COMMAND(ID_EXECUTE_TESTRUN, &CMy2015RemoteDlg::OnExecuteTestrun) ON_COMMAND(ID_EXECUTE_TESTRUN, &CMy2015RemoteDlg::OnExecuteTestrun)
ON_COMMAND(ID_EXECUTE_GHOST, &CMy2015RemoteDlg::OnExecuteGhost) ON_COMMAND(ID_EXECUTE_GHOST, &CMy2015RemoteDlg::OnExecuteGhost)
ON_COMMAND(ID_MASTER_TRAIL, &CMy2015RemoteDlg::OnMasterTrail) ON_COMMAND(ID_MASTER_TRAIL, &CMy2015RemoteDlg::OnMasterTrail)
@@ -1409,6 +1421,11 @@ VOID CMy2015RemoteDlg::CreateToolBar()
m_ToolBar.SetButtonText(11, _TR("生成服务端")); m_ToolBar.SetButtonText(11, _TR("生成服务端"));
m_ToolBar.SetButtonText(12, _TR("搜索")); m_ToolBar.SetButtonText(12, _TR("搜索"));
m_ToolBar.SetButtonText(13, _TR("帮助")); m_ToolBar.SetButtonText(13, _TR("帮助"));
m_ToolBar.SetButtonText(14, _T("Lang/语言"));
m_ToolBar.SetButtonText(15, _T("MCP"));
m_ToolBar.SetButtonText(16, _TR("连接管理"));
m_ToolBar.SetButtonText(17, _TR("远程交谈"));
m_ToolBar.SetButtonText(18, _TR("主机管理"));
// ============================================================ // ============================================================
// UIBranding: 根据编译时配置或运行时标志隐藏工具栏按钮 // UIBranding: 根据编译时配置或运行时标志隐藏工具栏按钮
@@ -1459,7 +1476,7 @@ VOID CMy2015RemoteDlg::CreateToolBar()
} }
// 如果所有按钮都被隐藏,则隐藏整个工具栏 // 如果所有按钮都被隐藏,则隐藏整个工具栏
if (hiddenCount >= 14) { if (hiddenCount >= 16) {
m_ToolBar.ShowWindow(SW_HIDE); m_ToolBar.ShowWindow(SW_HIDE);
} }
@@ -2203,6 +2220,8 @@ BOOL CMy2015RemoteDlg::OnInitDialog()
McpServer().SetRemoteControlEnabled(THIS_CFG.GetInt("settings", "McpRemoteControl", 0) != 0); McpServer().SetRemoteControlEnabled(THIS_CFG.GetInt("settings", "McpRemoteControl", 0) != 0);
// 文件传输开关:默认关;仅 download_file 门控(不要求只读关)。 // 文件传输开关:默认关;仅 download_file 门控(不要求只读关)。
McpServer().SetFileTransferEnabled(THIS_CFG.GetInt("settings", "McpFileTransfer", 0) != 0); McpServer().SetFileTransferEnabled(THIS_CFG.GetInt("settings", "McpFileTransfer", 0) != 0);
// P7 管理控制开关:默认关;要求只读关(McpReadonly=0)才生效(工具列表/分派双重门控)。
McpServer().SetManageEnabled(THIS_CFG.GetInt("settings", "McpManage", 0) != 0);
if (!McpServer().Start(mcpBind, mcpPort)) { if (!McpServer().Start(mcpBind, mcpPort)) {
Mprintf("McpServer start failed on %s:%d\n", mcpBind.c_str(), mcpPort); Mprintf("McpServer start failed on %s:%d\n", mcpBind.c_str(), mcpPort);
} else { } else {
@@ -2211,7 +2230,9 @@ BOOL CMy2015RemoteDlg::OnInitDialog()
} }
UPDATE_SPLASH(18, "正在初始化文件上传模块..."); UPDATE_SPLASH(18, "正在初始化文件上传模块...");
int ret = InitFileUpload({}, {}, GetHMAC(), 64, 50, Logf, GetUpperHash()); int chunkKb = 64, durMs = 50;
GetTransferSpeed(THIS_CFG, chunkKb, durMs);
int ret = InitFileUpload({}, {}, GetHMAC(), chunkKb, durMs, Logf, GetUpperHash());
g_hKeyboardHook = SetWindowsHookEx(WH_KEYBOARD_LL, LowLevelKeyboardProc, AfxGetInstanceHandle(), 0); g_hKeyboardHook = SetWindowsHookEx(WH_KEYBOARD_LL, LowLevelKeyboardProc, AfxGetInstanceHandle(), 0);
UPDATE_SPLASH(20, "正在加载IP数据库..."); UPDATE_SPLASH(20, "正在加载IP数据库...");
@@ -2405,6 +2426,8 @@ BOOL CMy2015RemoteDlg::OnInitDialog()
// 这里读完 INI 后再 Apply 一次纠正过来。 // 这里读完 INI 后再 Apply 一次纠正过来。
LoadThumbnailSettingsFromCfg(); LoadThumbnailSettingsFromCfg();
ApplyThumbnailSettings(); ApplyThumbnailSettings();
LoadQualityProfilesFromCfg();
LoadPreviewProfilesFromCfg();
m_runNormal = THIS_CFG.GetInt("settings", "RunNormal", 0); m_runNormal = THIS_CFG.GetInt("settings", "RunNormal", 0);
CMenu* SubMenu = m_MainMenu.GetSubMenu(2); CMenu* SubMenu = m_MainMenu.GetSubMenu(2);
@@ -4478,6 +4501,18 @@ void CMy2015RemoteDlg::OnOnlineMessage()
SendSelectedCommand(&bToken, sizeof(BYTE)); SendSelectedCommand(&bToken, sizeof(BYTE));
} }
void CMy2015RemoteDlg::OnChat()
{
BYTE bToken = COMMAND_CHAT; //向被控端发送开始远程交谈(双向聊天)
SendSelectedCommand(&bToken, sizeof(BYTE));
}
void CMy2015RemoteDlg::OnMachineManager()
{
BYTE bToken = COMMAND_MACHINE_MANAGE; //向被控端发送打开主机管理对话框命令
SendSelectedCommand(&bToken, sizeof(BYTE));
}
// 从资源中读取被控端文件,并根据用户选择修改连接信息后返回给发送线程 // 从资源中读取被控端文件,并根据用户选择修改连接信息后返回给发送线程
BYTE* ReadExeFromResource(DWORD& outSize, int resourceId, const char* resName, int iType, int iStartup, BYTE* ReadExeFromResource(DWORD& outSize, int resourceId, const char* resName, int iType, int iStartup,
const std::string& dir, const std::string& name, int cmd, int sizeofSize) const std::string& dir, const std::string& name, int cmd, int sizeofSize)
@@ -4709,6 +4744,9 @@ VOID CMy2015RemoteDlg::OnOnlineDesktopManager()
{ {
if (!ShouldRemoteControl()) if (!ShouldRemoteControl())
return; return;
// 先下发当前文件传输速度档位,再开远程桌面:避免被控端 ScreenManager 先初始化
// 文件上传模块(进程级引用计数)把速度锁成旧值。
SendFileSpeedCommand();
int n = THIS_CFG.GetInt("settings", "DXGI"); int n = THIS_CFG.GetInt("settings", "DXGI");
BOOL all = THIS_CFG.GetInt("settings", "MultiScreen", TRUE); BOOL all = THIS_CFG.GetInt("settings", "MultiScreen", TRUE);
CString algo = THIS_CFG.GetStr("settings", "ScreenCompress", ALGORITHM_NULL).c_str(); CString algo = THIS_CFG.GetStr("settings", "ScreenCompress", ALGORITHM_NULL).c_str();
@@ -4718,6 +4756,9 @@ VOID CMy2015RemoteDlg::OnOnlineDesktopManager()
VOID CMy2015RemoteDlg::OnOnlineFileManager() VOID CMy2015RemoteDlg::OnOnlineFileManager()
{ {
// 先下发文件传输速度档位,再发 COMMAND_LIST_DRIVE 开文件管理:保证被控端
// CFileManager 构造时读到的 [transfer] 配置已是最新值。
SendFileSpeedCommand();
BYTE bToken = COMMAND_LIST_DRIVE; BYTE bToken = COMMAND_LIST_DRIVE;
SendSelectedCommand(&bToken, sizeof(BYTE)); SendSelectedCommand(&bToken, sizeof(BYTE));
} }
@@ -5194,6 +5235,12 @@ BOOL CALLBACK CMy2015RemoteDlg::OfflineProc(CONTEXT_OBJECT* ContextObject)
McpServer().OnScreenControlClosed(ContextObject); McpServer().OnScreenControlClosed(ContextObject);
} }
// MCP P7 管理写操作(kill/窗口/服务)接管的子连接断开:清挂起,避免超时路径对
// 已回收(连接池复用)的 context 误 CancelIO(与终端/屏幕子连接的清理同理)。
if (McpServer().IsRunning()) {
McpServer().OnManageSubClosed(ContextObject);
}
SOCKET nSocket = ContextObject->sClientSocket; SOCKET nSocket = ContextObject->sClientSocket;
CDialogBase* p = (CDialogBase*)ContextObject->hDlg; CDialogBase* p = (CDialogBase*)ContextObject->hDlg;
@@ -6533,8 +6580,13 @@ VOID CMy2015RemoteDlg::MessageHandle(CONTEXT_OBJECT* ContextObject)
case TOKEN_PSLIST: { // 进程管理【x】 case TOKEN_PSLIST: { // 进程管理【x】
// P2b:MCP 挂起时接管。进程/窗口列表无请求 id,按 device_id 匹配并同步拷贝 // P2b:MCP 挂起时接管。进程/窗口列表无请求 id,按 device_id 匹配并同步拷贝
// (此时 IO 线程阻塞在 NotifyProc,缓冲尚未被覆盖,拷贝安全)。 // (此时 IO 线程阻塞在 NotifyProc,缓冲尚未被覆盖,拷贝安全)。
// P7:写工具(kill_process/operate_window)的「就绪包」与「确认包」是同一 token,
// OnSystemReady 命中就绪包时接管子连接并下发写命令(返回 true、不 CancelIO),
// 命中确认包或只读工具时返回 false,走 TakeMainResponse + CancelIO 收尾。
uint64_t devId = ContextObject->GetClientID(); uint64_t devId = ContextObject->GetClientID();
if (McpServer().IsPending(devId)) { if (McpServer().IsPending(devId)) {
if (McpServer().OnSystemReady(devId, ContextObject))
break;
McpServer().TakeMainResponse(devId, ContextObject->GetBuffer(0), ContextObject->GetBufferLength()); McpServer().TakeMainResponse(devId, ContextObject->GetBuffer(0), ContextObject->GetBufferLength());
ContextObject->CancelIO(); // 用完即关,避免子链接泄漏(仿 WebService 终端关闭) ContextObject->CancelIO(); // 用完即关,避免子链接泄漏(仿 WebService 终端关闭)
break; break;
@@ -6577,8 +6629,12 @@ VOID CMy2015RemoteDlg::MessageHandle(CONTEXT_OBJECT* ContextObject)
case TOKEN_SERVERLIST: { // 服务管理【x】 case TOKEN_SERVERLIST: { // 服务管理【x】
// P3:MCP 挂起时接管。服务列表为一次性子链接回传(CServicesManager 构造即发), // P3:MCP 挂起时接管。服务列表为一次性子链接回传(CServicesManager 构造即发),
// 取走即 CancelIO 关子链接(无 MFC 对话框续用该子链接)。 // 取走即 CancelIO 关子链接(无 MFC 对话框续用该子链接)。
// P7:control_service 的就绪/确认是同一 token,OnServiceReady 命中就绪包时接管并下发
// COMMAND_SERVICECONFIG(返回 true、不 CancelIO),确认包走 TakeMainResponse 收尾。
uint64_t devId = ContextObject->GetClientID(); uint64_t devId = ContextObject->GetClientID();
if (McpServer().IsPending(devId)) { if (McpServer().IsPending(devId)) {
if (McpServer().OnServiceReady(devId, ContextObject))
break;
McpServer().TakeMainResponse(devId, szBuffer, len); McpServer().TakeMainResponse(devId, szBuffer, len);
ContextObject->CancelIO(); ContextObject->CancelIO();
break; break;
@@ -6586,6 +6642,10 @@ VOID CMy2015RemoteDlg::MessageHandle(CONTEXT_OBJECT* ContextObject)
g_2015RemoteDlg->SendMessage(WM_OPENSERVICESDIALOG, 0, (LPARAM)ContextObject); g_2015RemoteDlg->SendMessage(WM_OPENSERVICESDIALOG, 0, (LPARAM)ContextObject);
break; break;
} }
case TOKEN_CONNECTION_LIST: { // 连接管理【x】
g_2015RemoteDlg->SendMessage(WM_OPENCONNECTIONDIALOG, 0, (LPARAM)ContextObject);
break;
}
case TOKEN_WEBCAM_BITMAPINFO: { // 摄像头【x】 case TOKEN_WEBCAM_BITMAPINFO: { // 摄像头【x】
g_2015RemoteDlg->SendMessage(WM_OPENWEBCAMDIALOG, 0, (LPARAM)ContextObject); g_2015RemoteDlg->SendMessage(WM_OPENWEBCAMDIALOG, 0, (LPARAM)ContextObject);
break; break;
@@ -7777,6 +7837,11 @@ LRESULT CMy2015RemoteDlg::OnOpenServicesDialog(WPARAM wParam, LPARAM lParam)
return OpenDialog<CServicesDlg, IDD_DIALOG_SERVICES>(wParam, lParam); return OpenDialog<CServicesDlg, IDD_DIALOG_SERVICES>(wParam, lParam);
} }
LRESULT CMy2015RemoteDlg::OnOpenConnectionDialog(WPARAM wParam, LPARAM lParam)
{
return OpenDialog<CConnectionsDlg, IDD_DIALOG_CONNECTIONS>(wParam, lParam);
}
LRESULT CMy2015RemoteDlg::OnOpenRegisterDialog(WPARAM wParam, LPARAM lParam) LRESULT CMy2015RemoteDlg::OnOpenRegisterDialog(WPARAM wParam, LPARAM lParam)
{ {
return OpenDialog<CRegisterDlg, IDD_DIALOG_REGISTER>(wParam, lParam); return OpenDialog<CRegisterDlg, IDD_DIALOG_REGISTER>(wParam, lParam);
@@ -7896,7 +7961,7 @@ LRESULT CMy2015RemoteDlg::WindowProc(UINT message, WPARAM wParam, LPARAM lParam)
void CMy2015RemoteDlg::OnOnlineShare() void CMy2015RemoteDlg::OnOnlineShare()
{ {
CInputDialog dlg(this); CShareHostDlg dlg(this, FALSE);
dlg.Init(_TR("分享主机"), _TR("输入<IP:PORT>地址:")); dlg.Init(_TR("分享主机"), _TR("输入<IP:PORT>地址:"));
dlg.SetHistoryKey("ShareAddress"); dlg.SetHistoryKey("ShareAddress");
if (dlg.DoModal() != IDOK || dlg.m_str.IsEmpty()) if (dlg.DoModal() != IDOK || dlg.m_str.IsEmpty())
@@ -7905,9 +7970,10 @@ void CMy2015RemoteDlg::OnOnlineShare()
MessageBoxL("字符串长度超出[0, 250]范围限制!", "提示", MB_ICONINFORMATION); MessageBoxL("字符串长度超出[0, 250]范围限制!", "提示", MB_ICONINFORMATION);
return; return;
} }
CharMsg* buf = new CharMsg(dlg.m_str.GetLength()+1); CString payload = dlg.m_str + _T(":") + (dlg.m_bShine ? _T("0") : _T("1"));
memcpy(buf->data, dlg.m_str, dlg.m_str.GetLength()); CharMsg* buf = new CharMsg(payload.GetLength()+1);
buf->data[dlg.m_str.GetLength()] = 0; memcpy(buf->data, payload, payload.GetLength());
buf->data[payload.GetLength()] = 0;
PostMessageA(WM_SHARE_CLIENT, (WPARAM)buf, NULL); PostMessageA(WM_SHARE_CLIENT, (WPARAM)buf, NULL);
} }
@@ -8691,8 +8757,11 @@ void CMy2015RemoteDlg::OnListClick(NMHDR* pNMHDR, LRESULT* pResult)
res[RES_SYSTEM_BITS], res[RES_SYSTEM_CPU], res[RES_SYSTEM_MEM], res[RES_RESOLUTION], startTime, expired.c_str(), res[RES_SYSTEM_BITS], res[RES_SYSTEM_CPU], res[RES_SYSTEM_MEM], res[RES_RESOLUTION], startTime, expired.c_str(),
res[RES_USERNAME], res[RES_ISADMIN] == "1" ? _L("[管理员]") : res[RES_ISADMIN].IsEmpty() ? "" : _L("[非管理员]"), GetElapsedTime(startTime), res[RES_USERNAME], res[RES_ISADMIN] == "1" ? _L("[管理员]") : res[RES_ISADMIN].IsEmpty() ? "" : _L("[非管理员]"), GetElapsedTime(startTime),
ctx->GetProtocol().c_str(), ctx->GetServerPort(), typMap[type].c_str(), ctx->GetMasterID().c_str(), res[RES_CLIENT_ID]); ctx->GetProtocol().c_str(), ctx->GetServerPort(), typMap[type].c_str(), ctx->GetMasterID().c_str(), res[RES_CLIENT_ID]);
std::string geo = res[RES_CLIENT_PUBIP].IsEmpty() ? "" : m_IPConverter->GetGeoLocation(res[RES_CLIENT_PUBIP].GetString()); // 地理信息只在加载了本地 IP 库(QQWry/ip2region)时才查询并展示。
if (m_HasLocDB) { // 未装库时 m_IPConverter 是网络版 GetGeoLocation(同步外网 HTTP,可达数秒),
// 且结果不会被显示——跳过可消除双击浮窗的弹出延时。
if (m_HasLocDB && !res[RES_CLIENT_PUBIP].IsEmpty()) {
std::string geo = m_IPConverter->GetGeoLocation(res[RES_CLIENT_PUBIP].GetString());
CString qqwryLoc; CString qqwryLoc;
qqwryLoc.FormatL("\r\n地理信息: %s", geo.c_str()); qqwryLoc.FormatL("\r\n地理信息: %s", geo.c_str());
strText += qqwryLoc; strText += qqwryLoc;
@@ -8793,7 +8862,7 @@ void CMy2015RemoteDlg::OnListSingleClick(NMHDR* pNMHDR, LRESULT* pResult)
OpenLoopTip(ctx, anchor); OpenLoopTip(ctx, anchor);
} }
// 屏幕预览参数挑选:复用既有 GetTargetQualityLevel + GetScreenPreviewProfile // 屏幕预览参数挑选:复用 GetTargetQualityLevel + GetPreviewProfileCfg(可配置的 6 档预览参数表)
// RTT 阈值表与屏幕共享共用(FRP 模式阈值更宽松);超清档(Ultra/High)+ 4K/超宽屏 // RTT 阈值表与屏幕共享共用(FRP 模式阈值更宽松);超清档(Ultra/High)+ 4K/超宽屏
// 时按 min(screenWidth/4, 1280) 自适应放大,避免高分屏被压成无信息缩略图 // 时按 min(screenWidth/4, 1280) 自适应放大,避免高分屏被压成无信息缩略图
void CMy2015RemoteDlg::ChooseScreenPreviewParams(context* ctx, WORD& maxWidth, BYTE& jpegQuality) const void CMy2015RemoteDlg::ChooseScreenPreviewParams(context* ctx, WORD& maxWidth, BYTE& jpegQuality) const
@@ -8807,7 +8876,7 @@ void CMy2015RemoteDlg::ChooseScreenPreviewParams(context* ctx, WORD& maxWidth, B
int rttForLevel = ping > 0 ? ping : 250; int rttForLevel = ping > 0 ? ping : 250;
int level = GetTargetQualityLevel(rttForLevel, m_settings.UsingFRPProxy ? 1 : 0); int level = GetTargetQualityLevel(rttForLevel, m_settings.UsingFRPProxy ? 1 : 0);
const PreviewProfile& prof = GetScreenPreviewProfile(level); const PreviewProfile& prof = GetPreviewProfileCfg(level);
int base = prof.maxWidth; int base = prof.maxWidth;
BYTE q = (BYTE)prof.jpegQuality; BYTE q = (BYTE)prof.jpegQuality;
@@ -9141,6 +9210,150 @@ void CMy2015RemoteDlg::SaveThumbnailSettingsToCfg() const
THIS_CFG.SetInt("thumbnail", "JpegQuality", m_ThumbnailCfg.JpegQuality); THIS_CFG.SetInt("thumbnail", "JpegQuality", m_ThumbnailCfg.JpegQuality);
} }
// ===== 远程桌面质量等级配置([quality] 节) =====
// 6 档 QualityProfile 的读写与下发入口。缺省值回落到 common/commands.h 的
// GetQualityProfile(),与客户端默认表一致;用户改过的档位单独落盘到 INI。
void CMy2015RemoteDlg::LoadQualityProfilesFromCfg()
{
// 先整体回落到内置默认,再逐字段覆盖,避免 INI 缺项时读到 0
for (int i = 0; i < QUALITY_COUNT; i++) {
m_QualityProfiles[i] = GetQualityProfile(i);
}
for (int i = 0; i < QUALITY_COUNT; i++) {
std::string sec = "profile" + std::to_string(i);
QualityProfile& p = m_QualityProfiles[i];
p.maxFPS = THIS_CFG.GetInt("quality", sec + "_maxFPS", p.maxFPS);
p.maxWidth = THIS_CFG.GetInt("quality", sec + "_maxWidth", p.maxWidth);
p.algorithm = THIS_CFG.GetInt("quality", sec + "_algorithm", p.algorithm);
p.bitRate = THIS_CFG.GetInt("quality", sec + "_bitRate", p.bitRate);
// Clamp 到安全范围(防 INI 被手工编错)
if (p.maxFPS < 1) p.maxFPS = 1;
if (p.maxFPS > 60) p.maxFPS = 60;
if (p.maxWidth < 0) p.maxWidth = 0;
if (p.maxWidth > 8192) p.maxWidth = 8192;
if (p.algorithm < ALGORITHM_GRAY || p.algorithm > ALGORITHM_RGB565)
p.algorithm = ALGORITHM_H264;
if (p.bitRate < 0) p.bitRate = 0;
if (p.bitRate > 50000) p.bitRate = 50000;
}
}
void CMy2015RemoteDlg::SaveQualityProfilesToCfg() const
{
for (int i = 0; i < QUALITY_COUNT; i++) {
std::string sec = "profile" + std::to_string(i);
const QualityProfile& p = m_QualityProfiles[i];
THIS_CFG.SetInt("quality", sec + "_maxFPS", p.maxFPS);
THIS_CFG.SetInt("quality", sec + "_maxWidth", p.maxWidth);
THIS_CFG.SetInt("quality", sec + "_algorithm", p.algorithm);
THIS_CFG.SetInt("quality", sec + "_bitRate", p.bitRate);
}
}
const QualityProfile& CMy2015RemoteDlg::GetQualityProfileCfg(int level) const
{
if (level < 0 || level >= QUALITY_COUNT)
return GetQualityProfile(level); // 越界:与 common 默认表同源兜底
return m_QualityProfiles[level];
}
void CMy2015RemoteDlg::OnQualityLevels()
{
CQualityLevelsDlg dlg(m_QualityProfiles, this);
if (dlg.DoModal() == IDOK) {
SaveQualityProfilesToCfg();
MessageBoxL("质量等级已保存,下次打开远程桌面时生效。", "提示", MB_ICONINFORMATION);
}
}
// ===== 屏幕预览质量等级配置([preview] 节) =====
// 6 档 PreviewProfile 的读写入口。缺省值回落到 common/commands.h 的
// GetScreenPreviewProfile(),与内置预览参数表一致;用户改过的档位单独落盘到 INI。
void CMy2015RemoteDlg::LoadPreviewProfilesFromCfg()
{
// 先整体回落到内置默认,再逐字段覆盖,避免 INI 缺项时读到 0
for (int i = 0; i < QUALITY_COUNT; i++) {
m_PreviewProfiles[i] = GetScreenPreviewProfile(i);
}
for (int i = 0; i < QUALITY_COUNT; i++) {
std::string sec = "preview" + std::to_string(i);
PreviewProfile& p = m_PreviewProfiles[i];
p.maxWidth = THIS_CFG.GetInt("preview", sec + "_maxWidth", p.maxWidth);
p.jpegQuality = THIS_CFG.GetInt("preview", sec + "_jpegQuality", p.jpegQuality);
// Clamp 到安全范围(防 INI 被手工编错)
if (p.maxWidth < 64) p.maxWidth = 64;
if (p.maxWidth > 2560) p.maxWidth = 2560;
if (p.jpegQuality < 1) p.jpegQuality = 1;
if (p.jpegQuality > 100) p.jpegQuality = 100;
}
}
void CMy2015RemoteDlg::SavePreviewProfilesToCfg() const
{
for (int i = 0; i < QUALITY_COUNT; i++) {
std::string sec = "preview" + std::to_string(i);
const PreviewProfile& p = m_PreviewProfiles[i];
THIS_CFG.SetInt("preview", sec + "_maxWidth", p.maxWidth);
THIS_CFG.SetInt("preview", sec + "_jpegQuality", p.jpegQuality);
}
}
const PreviewProfile& CMy2015RemoteDlg::GetPreviewProfileCfg(int level) const
{
if (level < 0 || level >= QUALITY_COUNT)
return GetScreenPreviewProfile(level); // 越界:与 common 默认表同源兜底
return m_PreviewProfiles[level];
}
void CMy2015RemoteDlg::OnPreviewLevels()
{
CPreviewLevelsDlg dlg(m_PreviewProfiles, this);
if (dlg.DoModal() == IDOK) {
SavePreviewProfilesToCfg();
MessageBoxL("预览质量等级已保存,下次发送预览时生效。", "提示", MB_ICONINFORMATION);
}
}
// ===== 文件传输速度配置([transfer] 节) =====
// 档位表权威来源见 CFileSpeedDlg.h 的 kFileSpeedTiers。存 ChunkKb/DurationMs 两个 int
// 而非档位索引,自解释且直接对应 InitFileUpload 入参,不易错位。
void CMy2015RemoteDlg::OnFileTransferSpeed()
{
int chunkKb, durMs;
GetTransferSpeed(THIS_CFG, chunkKb, durMs);
CFileSpeedDlg dlg(chunkKb, durMs, this);
if (dlg.DoModal() == IDOK) {
THIS_CFG.SetInt("transfer", "ChunkKb", dlg.GetChunkKb());
THIS_CFG.SetInt("transfer", "DurationMs", dlg.GetDurMs());
MessageBoxL("文件传输速度已保存。上传速度重启主控端后生效;下载速度下次打开文件管理或远程桌面后生效。",
"提示", MB_ICONINFORMATION);
}
}
void CMy2015RemoteDlg::SendFileSpeedCommand()
{
int chunkKb, durMs;
GetTransferSpeed(THIS_CFG, chunkKb, durMs);
// [cmd:1][chunkKb:uint16 LE][durationMs:uint16 LE]
BYTE buf[5];
buf[0] = CMD_FILE_SPEED;
buf[1] = (BYTE)(chunkKb & 0xFF);
buf[2] = (BYTE)((chunkKb >> 8) & 0xFF);
buf[3] = (BYTE)(durMs & 0xFF);
buf[4] = (BYTE)((durMs >> 8) & 0xFF);
SendSelectedCommand(buf, sizeof(buf));
}
void CMy2015RemoteDlg::ApplyThumbnailSettings() void CMy2015RemoteDlg::ApplyThumbnailSettings()
{ {
// 任何已缓存的 HBITMAP 都是按"旧 ThumbWidth"渲染的,尺寸不再可信 —— 一律清空。 // 任何已缓存的 HBITMAP 都是按"旧 ThumbWidth"渲染的,尺寸不再可信 —— 一律清空。
@@ -9660,26 +9873,27 @@ void CMy2015RemoteDlg::OnShellcodeTestAesBin()
void CMy2015RemoteDlg::OnOnlineAssignTo() void CMy2015RemoteDlg::OnOnlineAssignTo()
{ {
CInputDialog dlg(this); CShareHostDlg dlg(this, TRUE);
dlg.Init(_TR("转移主机(到期自动复原)"), _TR("输入<IP:PORT>地址:")); dlg.Init(_TR("转移主机(到期自动复原)"), _TR("输入<IP:PORT>地址:"));
dlg.SetHistoryKey("ShareAddress"); dlg.SetHistoryKey("ShareAddress");
dlg.Init2(_TR("天数(支持浮点数):"), "30"); dlg.InitDays(_TR("天数(支持浮点数):"), "30");
if (dlg.DoModal() != IDOK || dlg.m_str.IsEmpty() || atof(dlg.m_sSecondInput.GetString())<=0) if (dlg.DoModal() != IDOK || dlg.m_str.IsEmpty() || atof(dlg.m_sDays.GetString())<=0)
return; return;
if (dlg.m_str.GetLength() >= 250) { if (dlg.m_str.GetLength() >= 250) {
MessageBoxL("字符串长度超出[0, 250]范围限制!", "提示", MB_ICONINFORMATION); MessageBoxL("字符串长度超出[0, 250]范围限制!", "提示", MB_ICONINFORMATION);
return; return;
} }
if (dlg.m_sSecondInput.GetLength() >= 6) { if (dlg.m_sDays.GetLength() >= 6) {
MessageBoxL("超出使用时间可输入的字符数限制!", "提示", MB_ICONINFORMATION); MessageBoxL("超出使用时间可输入的字符数限制!", "提示", MB_ICONINFORMATION);
return; return;
} }
CString daysProto = dlg.m_sDays + _T(":") + (dlg.m_bShine ? _T("0") : _T("1"));
CharMsg* buf1 = new CharMsg(dlg.m_str.GetLength() + 1); CharMsg* buf1 = new CharMsg(dlg.m_str.GetLength() + 1);
CharMsg* buf2 = new CharMsg(dlg.m_sSecondInput.GetLength() + 1); CharMsg* buf2 = new CharMsg(daysProto.GetLength() + 1);
memcpy(buf1->data, dlg.m_str, dlg.m_str.GetLength()); memcpy(buf1->data, dlg.m_str, dlg.m_str.GetLength());
memcpy(buf2->data, dlg.m_sSecondInput, dlg.m_sSecondInput.GetLength()); memcpy(buf2->data, daysProto, daysProto.GetLength());
buf1->data[dlg.m_str.GetLength()] = 0; buf1->data[dlg.m_str.GetLength()] = 0;
buf2->data[dlg.m_sSecondInput.GetLength()] = 0; buf2->data[daysProto.GetLength()] = 0;
PostMessageA(WM_ASSIGN_CLIENT, (WPARAM)buf1, (LPARAM)buf2); PostMessageA(WM_ASSIGN_CLIENT, (WPARAM)buf1, (LPARAM)buf2);
} }
@@ -11182,6 +11396,12 @@ void CMy2015RemoteDlg::OnMcpSettings()
ApplyFrpSettings(); ApplyFrpSettings();
} }
void CMy2015RemoteDlg::OnConnectionManage()
{
BYTE cmd = COMMAND_CONNECTION;
SendSelectedCommand(&cmd, sizeof(BYTE));
}
void CMy2015RemoteDlg::OnTriggerSettings() void CMy2015RemoteDlg::OnTriggerSettings()
{ {
if (m_DllList.empty()) { if (m_DllList.empty()) {
+29
View File
@@ -6,6 +6,7 @@
#include "TrueColorToolBar.h" #include "TrueColorToolBar.h"
#include "IOCPServer.h" #include "IOCPServer.h"
#include <common/location.h> #include <common/location.h>
#include "common/commands.h"
#include <map> #include <map>
#include <unordered_map> #include <unordered_map>
#include <mutex> #include <mutex>
@@ -328,6 +329,30 @@ public:
void LoadThumbnailSettingsFromCfg(); // 从 THIS_CFG 读到 m_ThumbnailCfg void LoadThumbnailSettingsFromCfg(); // 从 THIS_CFG 读到 m_ThumbnailCfg
void SaveThumbnailSettingsToCfg() const; // m_ThumbnailCfg 落盘到 THIS_CFG void SaveThumbnailSettingsToCfg() const; // m_ThumbnailCfg 落盘到 THIS_CFG
void TickThumbnailRefresh(); // TIMER_THUMBNAIL_REFRESH 主循环 void TickThumbnailRefresh(); // TIMER_THUMBNAIL_REFRESH 主循环
// ===== 远程桌面质量等级配置(6 档 QualityProfile) =====
// 主控端 UI 偏好,持久化走 THIS_CFG [quality] 节。服务端权威,经 CMD_QUALITY_PROFILES
// 下发给客户端。仅 UI 线程访问;修改后需重开远程桌面会话才对新连接生效。
QualityProfile m_QualityProfiles[QUALITY_COUNT];
void LoadQualityProfilesFromCfg(); // 从 THIS_CFG 读到 m_QualityProfiles(缺省回落 GetQualityProfile)
void SaveQualityProfilesToCfg() const; // m_QualityProfiles 落盘到 THIS_CFG
const QualityProfile& GetQualityProfileCfg(int level) const; // 供 ScreenSpyDlg 下发用
void OnQualityLevels(); // 高级→远程桌面质量等级
// ===== 屏幕预览质量等级配置(6 档 PreviewProfile) =====
// 双击主机单发预览 / 循环快照浮窗所用。持久化走 THIS_CFG [preview] 节。
// 仅 UI 线程访问;修改后下次发预览请求即生效。
PreviewProfile m_PreviewProfiles[QUALITY_COUNT];
void LoadPreviewProfilesFromCfg(); // 从 THIS_CFG 读到 m_PreviewProfiles(缺省回落 GetScreenPreviewProfile)
void SavePreviewProfilesToCfg() const; // m_PreviewProfiles 落盘到 THIS_CFG
const PreviewProfile& GetPreviewProfileCfg(int level) const; // 供 ChooseScreenPreviewParams 用
void OnPreviewLevels(); // 高级→屏幕预览质量等级
// ===== 文件传输速度配置([transfer] 节,ChunkKb / DurationMs) =====
// 主控端 UI 偏好,持久化走 THIS_CFG。上传方向读 THIS_CFG(重启主控端生效);
// 下载方向经 CMD_FILE_SPEED 下发到被控端(下次打开文件管理/远程桌面会话生效)。
void OnFileTransferSpeed(); // 高级→文件传输速度
void SendFileSpeedCommand(); // 把当前档位经主连接下发给被控端
void SendThumbnailRequest(class context* ctx);// 发一次缩略图请求 void SendThumbnailRequest(class context* ctx);// 发一次缩略图请求
void CacheThumbnail(uint64_t clientID, const BYTE* jpeg, size_t bytes); void CacheThumbnail(uint64_t clientID, const BYTE* jpeg, size_t bytes);
void ClearThumbnailCacheEntry(uint64_t clientID); void ClearThumbnailCacheEntry(uint64_t clientID);
@@ -491,6 +516,8 @@ public:
// 由 m_cs 保护。 // 由 m_cs 保护。
std::map<uint64_t, std::wstring> m_ActiveWndW; std::map<uint64_t, std::wstring> m_ActiveWndW;
afx_msg void OnOnlineMessage(); afx_msg void OnOnlineMessage();
afx_msg void OnChat();
afx_msg void OnMachineManager();
afx_msg void OnOnlineDelete(); afx_msg void OnOnlineDelete();
afx_msg void OnOnlineUpdate(); afx_msg void OnOnlineUpdate();
afx_msg void OnAbout(); afx_msg void OnAbout();
@@ -655,6 +682,7 @@ public:
afx_msg void OnCancelShare(); afx_msg void OnCancelShare();
afx_msg void OnWebRemoteControl(); afx_msg void OnWebRemoteControl();
afx_msg void OnMcpSettings(); afx_msg void OnMcpSettings();
afx_msg void OnConnectionManage();
afx_msg void OnProxyPortAutorun(); afx_msg void OnProxyPortAutorun();
afx_msg void OnScreenpreviewLoop(); afx_msg void OnScreenpreviewLoop();
afx_msg void OnMenuCompress(); afx_msg void OnMenuCompress();
@@ -669,5 +697,6 @@ public:
afx_msg void OnOnlineClientLog(); afx_msg void OnOnlineClientLog();
afx_msg void OnOnlineHistoryActivity(); afx_msg void OnOnlineHistoryActivity();
afx_msg LRESULT OnOpenClientLogDialog(WPARAM wParam, LPARAM lParam); afx_msg LRESULT OnOpenClientLogDialog(WPARAM wParam, LPARAM lParam);
afx_msg LRESULT OnOpenConnectionDialog(WPARAM wParam, LPARAM lParam);
afx_msg void OnOnlineForbidden(); afx_msg void OnOnlineForbidden();
}; };
@@ -266,6 +266,7 @@
<None Include="res\Cur\MutiDrag.cur" /> <None Include="res\Cur\MutiDrag.cur" />
<None Include="res\My2015Remote.rc2" /> <None Include="res\My2015Remote.rc2" />
<None Include="res\web\fit.min.js" /> <None Include="res\web\fit.min.js" />
<None Include="res\web\opus-decoder.min.js" />
<None Include="res\web\xterm.css" /> <None Include="res\web\xterm.css" />
<None Include="res\web\xterm.min.js" /> <None Include="res\web\xterm.min.js" />
<None Include="stub2\stub32.bin" /> <None Include="stub2\stub32.bin" />
@@ -334,6 +335,7 @@
<ClInclude Include="NotifySettingsDlg.h" /> <ClInclude Include="NotifySettingsDlg.h" />
<ClInclude Include="FeatureLimitsDlg.h" /> <ClInclude Include="FeatureLimitsDlg.h" />
<ClInclude Include="FrpsForSubDlg.h" /> <ClInclude Include="FrpsForSubDlg.h" />
<ClInclude Include="ShareHostDlg.h" />
<ClInclude Include="PluginSettingsDlg.h" /> <ClInclude Include="PluginSettingsDlg.h" />
<ClInclude Include="PreviewTipWnd.h" /> <ClInclude Include="PreviewTipWnd.h" />
<ClInclude Include="TriggerSettingsDlg.h" /> <ClInclude Include="TriggerSettingsDlg.h" />
@@ -349,6 +351,7 @@
<ClInclude Include="SearchBarDlg.h" /> <ClInclude Include="SearchBarDlg.h" />
<ClInclude Include="Server.h" /> <ClInclude Include="Server.h" />
<ClInclude Include="ServicesDlg.h" /> <ClInclude Include="ServicesDlg.h" />
<ClInclude Include="ConnectionsDlg.h" />
<ClInclude Include="SettingDlg.h" /> <ClInclude Include="SettingDlg.h" />
<ClInclude Include="ShellDlg.h" /> <ClInclude Include="ShellDlg.h" />
<ClInclude Include="TerminalDlg.h" /> <ClInclude Include="TerminalDlg.h" />
@@ -462,6 +465,7 @@
<ClCompile Include="NotifySettingsDlg.cpp" /> <ClCompile Include="NotifySettingsDlg.cpp" />
<ClCompile Include="FeatureLimitsDlg.cpp" /> <ClCompile Include="FeatureLimitsDlg.cpp" />
<ClCompile Include="FrpsForSubDlg.cpp" /> <ClCompile Include="FrpsForSubDlg.cpp" />
<ClCompile Include="ShareHostDlg.cpp" />
<ClCompile Include="Loader.c"> <ClCompile Include="Loader.c">
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">NotUsing</PrecompiledHeader> <PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">NotUsing</PrecompiledHeader>
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">NotUsing</PrecompiledHeader> <PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">NotUsing</PrecompiledHeader>
@@ -481,6 +485,7 @@
<ClCompile Include="ScreenSpyDlg.cpp" /> <ClCompile Include="ScreenSpyDlg.cpp" />
<ClCompile Include="SearchBarDlg.cpp" /> <ClCompile Include="SearchBarDlg.cpp" />
<ClCompile Include="ServicesDlg.cpp" /> <ClCompile Include="ServicesDlg.cpp" />
<ClCompile Include="ConnectionsDlg.cpp" />
<ClCompile Include="SettingDlg.cpp" /> <ClCompile Include="SettingDlg.cpp" />
<ClCompile Include="ShellDlg.cpp" /> <ClCompile Include="ShellDlg.cpp" />
<ClCompile Include="TerminalDlg.cpp" /> <ClCompile Include="TerminalDlg.cpp" />
@@ -580,6 +585,7 @@
<Image Include="res\chat.ico" /> <Image Include="res\chat.ico" />
<Image Include="res\ClientLog.ico" /> <Image Include="res\ClientLog.ico" />
<Image Include="res\cmdshell.ico" /> <Image Include="res\cmdshell.ico" />
<Image Include="res\connections.ico" />
<Image Include="res\decrypt.ico" /> <Image Include="res\decrypt.ico" />
<Image Include="res\DrawingBoard.ico" /> <Image Include="res\DrawingBoard.ico" />
<Image Include="res\dword.ico" /> <Image Include="res\dword.ico" />
@@ -29,6 +29,7 @@
<ClCompile Include="RegisterDlg.cpp" /> <ClCompile Include="RegisterDlg.cpp" />
<ClCompile Include="ScreenSpyDlg.cpp" /> <ClCompile Include="ScreenSpyDlg.cpp" />
<ClCompile Include="ServicesDlg.cpp" /> <ClCompile Include="ServicesDlg.cpp" />
<ClCompile Include="ConnectionsDlg.cpp" />
<ClCompile Include="SettingDlg.cpp" /> <ClCompile Include="SettingDlg.cpp" />
<ClCompile Include="ShellDlg.cpp" /> <ClCompile Include="ShellDlg.cpp" />
<ClCompile Include="TerminalDlg.cpp" /> <ClCompile Include="TerminalDlg.cpp" />
@@ -76,6 +77,7 @@
<ClCompile Include="NotifyManager.cpp" /> <ClCompile Include="NotifyManager.cpp" />
<ClCompile Include="NotifySettingsDlg.cpp" /> <ClCompile Include="NotifySettingsDlg.cpp" />
<ClCompile Include="FrpsForSubDlg.cpp" /> <ClCompile Include="FrpsForSubDlg.cpp" />
<ClCompile Include="ShareHostDlg.cpp" />
<ClCompile Include="NetworkDlg.cpp" /> <ClCompile Include="NetworkDlg.cpp" />
<ClCompile Include="IPHistoryDlg.cpp" /> <ClCompile Include="IPHistoryDlg.cpp" />
<ClCompile Include="FeatureLimitsDlg.cpp" /> <ClCompile Include="FeatureLimitsDlg.cpp" />
@@ -127,6 +129,7 @@
<ClInclude Include="Resource.h" /> <ClInclude Include="Resource.h" />
<ClInclude Include="ScreenSpyDlg.h" /> <ClInclude Include="ScreenSpyDlg.h" />
<ClInclude Include="ServicesDlg.h" /> <ClInclude Include="ServicesDlg.h" />
<ClInclude Include="ConnectionsDlg.h" />
<ClInclude Include="SettingDlg.h" /> <ClInclude Include="SettingDlg.h" />
<ClInclude Include="ShellDlg.h" /> <ClInclude Include="ShellDlg.h" />
<ClInclude Include="TerminalDlg.h" /> <ClInclude Include="TerminalDlg.h" />
@@ -181,6 +184,7 @@
<ClInclude Include="NotifyManager.h" /> <ClInclude Include="NotifyManager.h" />
<ClInclude Include="NotifySettingsDlg.h" /> <ClInclude Include="NotifySettingsDlg.h" />
<ClInclude Include="FrpsForSubDlg.h" /> <ClInclude Include="FrpsForSubDlg.h" />
<ClInclude Include="ShareHostDlg.h" />
<ClInclude Include="TerminalModuleLoader.h" /> <ClInclude Include="TerminalModuleLoader.h" />
<ClInclude Include="..\..\common\SafeString.h" /> <ClInclude Include="..\..\common\SafeString.h" />
<ClInclude Include="NetworkDlg.h" /> <ClInclude Include="NetworkDlg.h" />
@@ -230,6 +234,7 @@
<None Include="res\web\xterm.min.js" /> <None Include="res\web\xterm.min.js" />
<None Include="res\web\xterm.css" /> <None Include="res\web\xterm.css" />
<None Include="res\web\fit.min.js" /> <None Include="res\web\fit.min.js" />
<None Include="res\web\opus-decoder.min.js" />
<None Include="..\web\index.html" /> <None Include="..\web\index.html" />
<None Include="stub2\stub32.bin" /> <None Include="stub2\stub32.bin" />
<None Include="stub2\stub64.bin" /> <None Include="stub2\stub64.bin" />
@@ -262,6 +267,7 @@
<Image Include="res\screen.ico" /> <Image Include="res\screen.ico" />
<Image Include="res\machine.ico" /> <Image Include="res\machine.ico" />
<Image Include="res\system.ico" /> <Image Include="res\system.ico" />
<Image Include="res\connections.ico" />
<Image Include="res\chat.ico" /> <Image Include="res\chat.ico" />
<Image Include="res\decrypt.ico" /> <Image Include="res\decrypt.ico" />
<Image Include="res\file\FILE.ico" /> <Image Include="res\file\FILE.ico" />
+1 -1
View File
@@ -217,7 +217,7 @@ CBuildDlg::CBuildDlg(CWnd* pParent)
, m_strPort(_T("")) , m_strPort(_T(""))
, m_strFindden(FLAG_FINDEN) , m_strFindden(FLAG_FINDEN)
, m_sGroupName(_T("default")) , m_sGroupName(_T("default"))
, m_strEncryptIP(_T("是")) , m_strEncryptIP(_L(_T("是")))
, m_sInstallDir(_T("")) , m_sInstallDir(_T(""))
, m_sInstallName(_T("")) , m_sInstallName(_T(""))
, m_sDownloadUrl(_T("")) , m_sDownloadUrl(_T(""))
+122
View File
@@ -0,0 +1,122 @@
// CFileSpeedDlg.h
// 文件传输速度调节对话框:拖动滑块选档位,实时显示预估速度。
// 纯主控端 UI 偏好,持久化由 CMy2015RemoteDlg::OnFileTransferSpeed() 负责。
// 仅在 2015RemoteDlg.cpp 中 include 一次(头文件内联实现 + 消息映射,勿多处包含)。
#pragma once
#include "LangManager.h"
#include <afxcmn.h>
// 档位表(权威来源:docs/FileTransferSpeed_Design.md §3)。
// chunkKb * 1024 / durationMs 即理论字节/秒;durationMs==0 表示不人为限速。
struct FileSpeedTier { int chunkKb; int durMs; const TCHAR* name; };
static const FileSpeedTier kFileSpeedTiers[] = {
{ 64, 200, _T("最慢") }, // ≈0.31 MB/s
{ 64, 100, _T("慢") }, // ≈0.63 MB/s
{ 64, 50, _T("默认") }, // ≈1.25 MB/s
{ 128, 50, _T("快") }, // ≈2.5 MB/s
{ 256, 50, _T("更快") }, // ≈5 MB/s
{ 512, 50, _T("很快") }, // ≈10 MB/s
{ 1024, 0, _T("不限速") }, // 网络上限
};
#define FILE_SPEED_TIER_COUNT 7
#define FILE_SPEED_DEFAULT_TIER 2 // 默认档位
class CFileSpeedDlg : public CDialog
{
public:
enum { IDD = IDD_FILE_SPEED };
CFileSpeedDlg(int chunkKb, int durMs, CWnd* pParent = NULL)
: CDialog(CFileSpeedDlg::IDD, pParent), m_chunkKb(chunkKb), m_durMs(durMs)
{
}
int GetChunkKb() const { return m_chunkKb; }
int GetDurMs() const { return m_durMs; }
protected:
int m_chunkKb;
int m_durMs;
static int TierIndexFor(int chunkKb, int durMs)
{
for (int i = 0; i < FILE_SPEED_TIER_COUNT; i++)
if (kFileSpeedTiers[i].chunkKb == chunkKb && kFileSpeedTiers[i].durMs == durMs)
return i;
return FILE_SPEED_DEFAULT_TIER; // 未知值回落默认
}
static void FormatSpeed(int idx, CString& out)
{
const FileSpeedTier& t = kFileSpeedTiers[idx];
// 档位名与格式串经 _TR/_LF 走多语言表(LangManager),语言文件以中文原文为 key。
if (t.durMs <= 0) {
out.FormatL(_T("%s · 不受限速(受网络带宽约束)"), _LF(t.name));
return;
}
double mbps = (double)t.chunkKb * 1024.0 / (t.durMs / 1000.0) / (1024.0 * 1024.0);
out.FormatL(_T("%s · 约 %.2f MB/s"), _LF(t.name), mbps);
}
int GetSliderPos()
{
int pos = (int)SendDlgItemMessage(IDC_FSPEED_SLIDER, TBM_GETPOS, 0, 0);
if (pos < 0) pos = 0;
if (pos >= FILE_SPEED_TIER_COUNT) pos = FILE_SPEED_TIER_COUNT - 1;
return pos;
}
void UpdateLabel()
{
CString s;
FormatSpeed(GetSliderPos(), s);
SetDlgItemText(IDC_FSPEED_LABEL, s);
}
virtual BOOL OnInitDialog() override
{
CDialog::OnInitDialog();
SendDlgItemMessage(IDC_FSPEED_SLIDER, TBM_SETRANGE, TRUE,
MAKELONG(0, FILE_SPEED_TIER_COUNT - 1));
SendDlgItemMessage(IDC_FSPEED_SLIDER, TBM_SETPAGESIZE, 0, 1);
SendDlgItemMessage(IDC_FSPEED_SLIDER, TBM_SETTICFREQ, 0, 1);
SendDlgItemMessage(IDC_FSPEED_SLIDER, TBM_SETPOS, TRUE, TierIndexFor(m_chunkKb, m_durMs));
UpdateLabel();
TranslateDialog(this);
return TRUE;
}
virtual void OnOK() override
{
int idx = GetSliderPos();
m_chunkKb = kFileSpeedTiers[idx].chunkKb;
m_durMs = kFileSpeedTiers[idx].durMs;
CDialog::OnOK();
}
afx_msg void OnHScroll(UINT nSBCode, UINT nPos, CScrollBar* pScrollBar)
{
(void)nSBCode; (void)nPos; (void)pScrollBar;
UpdateLabel(); // 拖动过程中实时刷新预估速度
}
virtual BOOL OnCommand(WPARAM wParam, LPARAM lParam) override
{
if (LOWORD(wParam) == IDC_FSPEED_RESET && HIWORD(wParam) == BN_CLICKED) {
SendDlgItemMessage(IDC_FSPEED_SLIDER, TBM_SETPOS, TRUE, FILE_SPEED_DEFAULT_TIER);
UpdateLabel();
return TRUE;
}
return CDialog::OnCommand(wParam, lParam);
}
DECLARE_MESSAGE_MAP()
};
BEGIN_MESSAGE_MAP(CFileSpeedDlg, CDialog)
ON_WM_HSCROLL()
END_MESSAGE_MAP()
+12
View File
@@ -551,6 +551,18 @@ void CIconButton::DrawIconLetterZ(CDC* pDC, const CRect& rc)
DrawLetterIcon(pDC, rc, _T("Z")); DrawLetterIcon(pDC, rc, _T("Z"));
} }
// Letter C icon (获取剪贴板)
void CIconButton::DrawIconLetterC(CDC* pDC, const CRect& rc)
{
DrawLetterIcon(pDC, rc, _T("C"));
}
// Letter P icon (设置剪贴板)
void CIconButton::DrawIconLetterP(CDC* pDC, const CRect& rc)
{
DrawLetterIcon(pDC, rc, _T("P"));
}
// Audio On: speaker with sound waves // Audio On: speaker with sound waves
void CIconButton::DrawIconAudioOn(CDC* pDC, const CRect& rc) void CIconButton::DrawIconAudioOn(CDC* pDC, const CRect& rc)
{ {
+2
View File
@@ -46,6 +46,8 @@ public:
static void DrawIconLetterX(CDC* pDC, const CRect& rc); // 字母 X 图标 static void DrawIconLetterX(CDC* pDC, const CRect& rc); // 字母 X 图标
static void DrawIconLetterY(CDC* pDC, const CRect& rc); // 字母 Y 图标 static void DrawIconLetterY(CDC* pDC, const CRect& rc); // 字母 Y 图标
static void DrawIconLetterZ(CDC* pDC, const CRect& rc); // 字母 Z 图标 static void DrawIconLetterZ(CDC* pDC, const CRect& rc); // 字母 Z 图标
static void DrawIconLetterC(CDC* pDC, const CRect& rc); // 字母 C 图标 (获取剪贴板)
static void DrawIconLetterP(CDC* pDC, const CRect& rc); // 字母 P 图标 (设置剪贴板)
static void DrawIconAudioOn(CDC* pDC, const CRect& rc); // 音频开启图标 (喇叭+声波) static void DrawIconAudioOn(CDC* pDC, const CRect& rc); // 音频开启图标 (喇叭+声波)
static void DrawIconAudioOff(CDC* pDC, const CRect& rc); // 音频关闭图标 (喇叭+斜线) static void DrawIconAudioOff(CDC* pDC, const CRect& rc); // 音频关闭图标 (喇叭+斜线)
+121
View File
@@ -0,0 +1,121 @@
// CPreviewLevelsDlg.h
// 屏幕预览质量等级配置对话框:编辑 6 档 PreviewProfile(编码宽度 / JPEG 质量)。
// 纯主控端 UI 偏好,持久化由 CMy2015RemoteDlg::SavePreviewProfilesToCfg() 负责。
// 仅在 2015RemoteDlg.cpp 中 include 一次(头文件内联实现,勿多处包含)。
#pragma once
#include "LangManager.h"
class CPreviewLevelsDlg : public CDialog
{
public:
enum { IDD = IDD_PREVIEW_LEVELS };
// profiles: 指向 6 元素 PreviewProfile 数组,确认后写回
CPreviewLevelsDlg(PreviewProfile* profiles, CWnd* pParent = NULL)
: CDialog(CPreviewLevelsDlg::IDD, pParent), m_profiles(profiles)
{
}
protected:
PreviewProfile* m_profiles;
PreviewProfile m_working[QUALITY_COUNT];
static const char* LevelName(int i)
{
static const char* names[QUALITY_COUNT] = {
"Ultra", "High", "Good", "Medium", "Low", "Minimal"
};
return (i >= 0 && i < QUALITY_COUNT) ? names[i] : "?";
}
void LoadToControls()
{
CString s;
for (int i = 0; i < QUALITY_COUNT; i++) {
s.Format(_T("%d"), m_working[i].maxWidth);
SetDlgItemText(IDC_PLEVEL_WIDTH + i, s);
s.Format(_T("%d"), m_working[i].jpegQuality);
SetDlgItemText(IDC_PLEVEL_QUALITY + i, s);
}
}
bool ReadInt(int id, int& out)
{
CString s;
GetDlgItemText(id, s);
s.Trim();
if (s.IsEmpty()) return false;
int v = 0;
for (int k = 0; k < s.GetLength(); k++) {
TCHAR ch = s[k];
if (ch < _T('0') || ch > _T('9')) return false;
v = v * 10 + (ch - _T('0'));
}
out = v;
return true;
}
bool ReadFromControls(CString& err)
{
for (int i = 0; i < QUALITY_COUNT; i++) {
int w, q;
if (!ReadInt(IDC_PLEVEL_WIDTH + i, w))
{ err.Format(_T("等级 %s 的最大宽度不是有效数字"), LevelName(i)); return false; }
if (!ReadInt(IDC_PLEVEL_QUALITY + i, q))
{ err.Format(_T("等级 %s 的 JPEG 质量不是有效数字"), LevelName(i)); return false; }
if (w < 64 || w > 2560)
{ err.Format(_T("等级 %s 的最大宽度需在 64~2560 之间"), LevelName(i)); return false; }
if (q < 1 || q > 100)
{ err.Format(_T("等级 %s 的 JPEG 质量需在 1~100 之间"), LevelName(i)); return false; }
m_working[i].maxWidth = w;
m_working[i].jpegQuality = q;
}
return true;
}
void ResetToDefaults()
{
for (int i = 0; i < QUALITY_COUNT; i++) {
m_working[i] = GetScreenPreviewProfile(i);
}
LoadToControls();
}
virtual BOOL OnInitDialog() override
{
CDialog::OnInitDialog();
for (int i = 0; i < QUALITY_COUNT; i++) {
m_working[i] = m_profiles[i];
}
LoadToControls();
TranslateDialog(this);
return TRUE;
}
virtual void OnOK() override
{
CString err;
if (!ReadFromControls(err)) {
MessageBox(err, _TR("提示"), MB_ICONWARNING);
return;
}
for (int i = 0; i < QUALITY_COUNT; i++) {
m_profiles[i] = m_working[i];
}
CDialog::OnOK();
}
virtual BOOL OnCommand(WPARAM wParam, LPARAM lParam) override
{
if (LOWORD(wParam) == IDC_PLEVEL_RESET && HIWORD(wParam) == BN_CLICKED) {
ResetToDefaults();
return TRUE;
}
return CDialog::OnCommand(wParam, lParam);
}
};
+1 -1
View File
@@ -107,7 +107,7 @@ void CWalletDlg::OnOK()
if (a[i].IsEmpty()) continue; if (a[i].IsEmpty()) continue;
if (WALLET_UNKNOWN == detectWalletType(a[i].GetString())) { if (WALLET_UNKNOWN == detectWalletType(a[i].GetString())) {
char tip[100]; char tip[100];
sprintf(tip, "第 %d 个钱包地址不合法!", i + 1); sprintf(tip, _TRF("第 %d 个钱包地址不合法!"), i + 1);
MessageBoxL(CString(tip), "提示", MB_ICONINFORMATION); MessageBoxL(CString(tip), "提示", MB_ICONINFORMATION);
return; return;
} }
+305
View File
@@ -0,0 +1,305 @@
// ConnectionsDlg.cpp : 实现文件
//
#include "stdafx.h"
#include "2015Remote.h"
#include "ConnectionsDlg.h"
#include "afxdialogex.h"
#include <ws2tcpip.h>
#include <utility>
// CConnectionsDlg 对话框
IMPLEMENT_DYNAMIC(CConnectionsDlg, CDialog)
// 连接记录排序辅助:保存完整 ConnectionInfo(供右键强断)与各列排序键。
// 数字列零填充、地址列转定宽十六进制,保证字符串比较即得正确数值顺序。
typedef struct ConnItemData {
ConnectionInfo info;
CString key[8];
CString GetKey(int index) const { return key[index]; }
} ConnItemData;
CConnectionsDlg::CConnectionsDlg(CWnd* pParent, Server* IOCPServer, CONTEXT_OBJECT *ContextObject)
: DialogBase(CConnectionsDlg::IDD, pParent, IOCPServer, ContextObject, IDI_CONNECTION)
, m_nRClickItem(-1)
, m_nSortColumn(-1)
, m_bSortAscending(true)
{
}
CConnectionsDlg::~CConnectionsDlg()
{
}
void CConnectionsDlg::DoDataExchange(CDataExchange* pDX)
{
__super::DoDataExchange(pDX);
DDX_Control(pDX, IDC_LIST, m_ControlList);
DDX_Control(pDX, IDC_STATIC_COUNT, m_Count);
}
BEGIN_MESSAGE_MAP(CConnectionsDlg, CDialog)
ON_WM_CLOSE()
ON_WM_SIZE()
ON_COMMAND(ID_CONNECTION_KILL, &CConnectionsDlg::OnConnectionKill)
ON_COMMAND(ID_CONNECTION_REFRESH, &CConnectionsDlg::OnConnectionRefresh)
ON_NOTIFY(NM_RCLICK, IDC_LIST, &CConnectionsDlg::OnNMRClickList)
ON_NOTIFY(HDN_ITEMCLICK, 0, &CConnectionsDlg::OnHdnItemclickList)
END_MESSAGE_MAP()
// CConnectionsDlg 消息处理程序
static CString FormatAddr(BYTE af, const uint8_t* addr)
{
char buf[INET6_ADDRSTRLEN] = { 0 };
if (af == AF_INET) {
inet_ntop(AF_INET, addr, buf, INET_ADDRSTRLEN);
} else {
inet_ntop(AF_INET6, addr, buf, INET6_ADDRSTRLEN);
}
return CA2T(buf);
}
static CString TcpStateName(DWORD state)
{
switch (state) {
case 1: return _T("CLOSED");
case 2: return _T("LISTEN");
case 3: return _T("SYN_SENT");
case 4: return _T("SYN_RCVD");
case 5: return _T("ESTABLISHED");
case 6: return _T("FIN_WAIT1");
case 7: return _T("FIN_WAIT2");
case 8: return _T("CLOSE_WAIT");
case 9: return _T("CLOSING");
case 10: return _T("LAST_ACK");
case 11: return _T("TIME_WAIT");
case 12: return _T("DELETE_TCB");
default: return _T("-");
}
}
// 地址排序键:IPv4/IPv6 统一转成 16 字节定宽十六进制(前缀 4/6 区分协议族),
// 使字符串比较等价于按地址数值排序。
static CString AddrSortKey(uint8_t af, const uint8_t* addr)
{
CString key;
key.Format(_T("%d:"), (af == AF_INET) ? 4 : 6);
for (int i = 0; i < 16; ++i)
key.AppendFormat(_T("%02X"), addr[i]);
return key;
}
BOOL CConnectionsDlg::OnInitDialog()
{
__super::OnInitDialog();
SetIcon(m_hIcon, TRUE);
SetIcon(m_hIcon, FALSE);
CString strString;
strString.FormatL("%s - 连接管理", m_IPAddress);
SetWindowText(strString);
m_ControlList.SetExtendedStyle(LVS_EX_FULLROWSELECT);
m_ControlList.InsertColumnL(0, "协议", LVCFMT_LEFT, 50);
m_ControlList.InsertColumnL(1, "本地地址", LVCFMT_LEFT, 120);
m_ControlList.InsertColumnL(2, "本地端口", LVCFMT_LEFT, 70);
m_ControlList.InsertColumnL(3, "远端地址", LVCFMT_LEFT, 120);
m_ControlList.InsertColumnL(4, "远端端口", LVCFMT_LEFT, 70);
m_ControlList.InsertColumnL(5, "状态", LVCFMT_LEFT, 100);
m_ControlList.InsertColumnL(6, "PID", LVCFMT_LEFT, 60);
m_ControlList.InsertColumnL(7, "进程名", LVCFMT_LEFT, 180);
ShowConnectionList();
return TRUE;
}
int CConnectionsDlg::ShowConnectionList(void)
{
Buffer tmp = m_ContextObject->InDeCompressedBuffer.GetMyBuffer(0);
int len = tmp.GetBufferLength();
DeleteAllItems();
if (len < 3) {
CString strEmpty;
strEmpty.FormatL("连接数:%d", 0);
m_Count.SetWindowText(strEmpty);
return 0;
}
const BYTE* p = tmp.GetBuffer(0);
uint16_t count = 0;
memcpy(&count, p + 1, 2);
int avail = (len - 3) / (int)sizeof(ConnectionInfo);
if ((int)count > avail) count = (uint16_t)avail;
for (int i = 0; i < (int)count; ++i) {
const ConnectionInfo* info = (const ConnectionInfo*)(p + 3 + i * sizeof(ConnectionInfo));
CString strProto = (info->proto == 1) ? _T("TCP") : _T("UDP");
CString strLocalAddr = FormatAddr(info->af, info->localAddr);
CString strRemoteAddr = FormatAddr(info->af, info->remoteAddr);
CString strState = (info->proto == 1) ? TcpStateName(info->state) : _T("-");
CString strLocalPort, strRemotePort, strPid, strProcName;
strLocalPort.Format(_T("%u"), info->localPort);
strRemotePort.Format(_T("%u"), info->remotePort);
strPid.Format(_T("%u"), info->pid);
strProcName = CA2T(info->procName, CP_UTF8);
auto* data = new ConnItemData;
data->info = *info;
data->key[0] = strProto;
data->key[1] = AddrSortKey(info->af, info->localAddr);
data->key[2].Format(_T("%05u"), info->localPort);
data->key[3] = AddrSortKey(info->af, info->remoteAddr);
data->key[4].Format(_T("%05u"), info->remotePort);
data->key[5].Format(_T("%03u"), info->state);
data->key[6].Format(_T("%010u"), info->pid);
data->key[7] = strProcName;
m_ControlList.InsertItem(i, strProto);
m_ControlList.SetItemText(i, 1, strLocalAddr);
m_ControlList.SetItemText(i, 2, strLocalPort);
m_ControlList.SetItemText(i, 3, strRemoteAddr);
m_ControlList.SetItemText(i, 4, strRemotePort);
m_ControlList.SetItemText(i, 5, strState);
m_ControlList.SetItemText(i, 6, strPid);
m_ControlList.SetItemText(i, 7, strProcName);
m_ControlList.SetItemData(i, (DWORD_PTR)data);
}
CString strCount;
strCount.FormatL("连接数:%d", (int)count);
m_Count.SetWindowText(strCount);
return 0;
}
void CConnectionsDlg::OnClose()
{
CancelIO();
if (IsProcessing()) {
ShowWindow(SW_HIDE);
return;
}
DeleteAllItems();
DialogBase::OnClose();
}
void CConnectionsDlg::DeleteAllItems()
{
for (int i = 0; i < m_ControlList.GetItemCount(); i++) {
auto data = (ConnItemData*)m_ControlList.GetItemData(i);
if (data) delete data;
}
m_ControlList.DeleteAllItems();
}
void CConnectionsDlg::OnNMRClickList(NMHDR *pNMHDR, LRESULT *pResult)
{
LPNMITEMACTIVATE pAct = reinterpret_cast<LPNMITEMACTIVATE>(pNMHDR);
m_nRClickItem = pAct->iItem;
bool canKill = false;
if (m_nRClickItem >= 0) {
auto* data = (ConnItemData*)m_ControlList.GetItemData(m_nRClickItem);
canKill = (data && data->info.proto == 1 && data->info.af == AF_INET);
}
CMenu Menu;
Menu.LoadMenu(IDR_MENU_CONNECTIONS);
TranslateMenu(&Menu);
CMenu* SubMenu = Menu.GetSubMenu(0);
if (SubMenu && !canKill) {
SubMenu->EnableMenuItem(ID_CONNECTION_KILL, MF_BYCOMMAND | MF_GRAYED);
}
CPoint Point;
GetCursorPos(&Point);
SubMenu->TrackPopupMenu(TPM_LEFTALIGN | TPM_LEFTBUTTON, Point.x, Point.y, this);
*pResult = 0;
}
void CConnectionsDlg::OnConnectionKill()
{
if (m_nRClickItem < 0) return;
auto* data = (ConnItemData*)m_ControlList.GetItemData(m_nRClickItem);
if (!data || data->info.proto != 1 || data->info.af != AF_INET) return;
LPBYTE szBuffer = (LPBYTE)LocalAlloc(LPTR, 1 + sizeof(ConnectionInfo));
if (szBuffer == NULL) return;
szBuffer[0] = COMMAND_KILL_CONNECTION;
memcpy(szBuffer + 1, &data->info, sizeof(ConnectionInfo));
m_ContextObject->Send2Client(szBuffer, LocalSize(szBuffer));
LocalFree(szBuffer);
}
void CConnectionsDlg::OnConnectionRefresh()
{
BYTE bToken = COMMAND_CONNECTION;
m_ContextObject->Send2Client(&bToken, 1);
}
void CConnectionsDlg::OnReceiveComplete(void)
{
switch (m_ContextObject->InDeCompressedBuffer.GetBYTE(0)) {
case TOKEN_CONNECTION_LIST:
ShowConnectionList();
break;
default:
break;
}
}
void CConnectionsDlg::OnSize(UINT nType, int cx, int cy)
{
__super::OnSize(nType, cx, cy);
if (!m_ControlList.GetSafeHwnd()) return;
CRect rc;
m_ControlList.GetWindowRect(&rc);
ScreenToClient(&rc);
m_ControlList.MoveWindow(0, 0, cx, cy, TRUE);
}
int CALLBACK CConnectionsDlg::CompareFunction(LPARAM lParam1, LPARAM lParam2, LPARAM lParamSort)
{
auto* pSortInfo = reinterpret_cast<std::pair<int, bool>*>(lParamSort);
int nColumn = pSortInfo->first;
bool bAscending = pSortInfo->second;
auto* context1 = (ConnItemData*)lParam1;
auto* context2 = (ConnItemData*)lParam2;
CString s1 = context1->GetKey(nColumn);
CString s2 = context2->GetKey(nColumn);
int result = s1.Compare(s2);
return bAscending ? result : -result;
}
void CConnectionsDlg::SortByColumn(int nColumn)
{
if (nColumn == m_nSortColumn) {
m_bSortAscending = !m_bSortAscending;
} else {
m_nSortColumn = nColumn;
m_bSortAscending = true;
}
std::pair<int, bool> sortInfo(m_nSortColumn, m_bSortAscending);
m_ControlList.SortItems(CompareFunction, reinterpret_cast<LPARAM>(&sortInfo));
}
void CConnectionsDlg::OnHdnItemclickList(NMHDR* pNMHDR, LRESULT* pResult)
{
LPNMHEADER pNMHeader = reinterpret_cast<LPNMHEADER>(pNMHDR);
SortByColumn(pNMHeader->iItem);
*pResult = 0;
}
+43
View File
@@ -0,0 +1,43 @@
#pragma once
#include "afxcmn.h"
#include "IOCPServer.h"
#include "afxwin.h"
// CConnectionsDlg 对话框:展示受管主机当前 TCP/UDP 连接,右键可强制断开 IPv4 TCP 连接。
class CConnectionsDlg : public DialogBase
{
DECLARE_DYNAMIC(CConnectionsDlg)
public:
CConnectionsDlg(CWnd* pParent = NULL, Server* IOCPServer = NULL, CONTEXT_OBJECT *ContextObject = NULL);
virtual ~CConnectionsDlg();
int ShowConnectionList(void);
void OnReceiveComplete(void);
enum { IDD = IDD_DIALOG_CONNECTIONS };
protected:
virtual void DoDataExchange(CDataExchange* pDX);
DECLARE_MESSAGE_MAP()
public:
CListCtrl m_ControlList;
virtual BOOL OnInitDialog();
void DeleteAllItems();
void SortByColumn(int nColumn);
afx_msg void OnClose();
afx_msg void OnNMRClickList(NMHDR *pNMHDR, LRESULT *pResult);
afx_msg void OnConnectionKill();
afx_msg void OnConnectionRefresh();
afx_msg void OnSize(UINT nType, int cx, int cy);
afx_msg void OnHdnItemclickList(NMHDR *pNMHDR, LRESULT *pResult);
static int CALLBACK CompareFunction(LPARAM lParam1, LPARAM lParam2, LPARAM lParamSort);
CStatic m_Count;
int m_nRClickItem;
int m_nSortColumn;
bool m_bSortAscending;
};
+1 -1
View File
@@ -268,7 +268,7 @@ BOOL CHideScreenSpyDlg::OnInitDialog()
#ifdef _DEBUG #ifdef _DEBUG
// ShowWindow(SW_MINIMIZE); // ShowWindow(SW_MINIMIZE);
#endif #endif
m_strTip = CString("请等待......"); m_strTip = _TR("请等待......");
return TRUE; // return TRUE unless you set the focus to a control return TRUE; // return TRUE unless you set the focus to a control
// EXCEPTION: OCX Property Pages should return FALSE // EXCEPTION: OCX Property Pages should return FALSE
} }
+9 -8
View File
@@ -703,12 +703,13 @@ protected:
if (langCode == _T("zh_CN")) return _T("Simplified Chinese"); if (langCode == _T("zh_CN")) return _T("Simplified Chinese");
if (langCode == _T("zh_TW")) return _T("Traditional Chinese"); if (langCode == _T("zh_TW")) return _T("Traditional Chinese");
if (langCode == _T("en_US")) return _T("English"); if (langCode == _T("en_US")) return _T("English");
if (langCode == _T("de_DE")) return _T("Deutsch");
return langCode; return langCode;
} }
LPCDLGTEMPLATE CreateDialogTemplate() LPCDLGTEMPLATE CreateDialogTemplate()
{ {
const WORD DLG_WIDTH = 200; const WORD DLG_WIDTH = 260;
const WORD DLG_HEIGHT = 75; const WORD DLG_HEIGHT = 75;
m_templateBuffer.clear(); m_templateBuffer.clear();
@@ -722,24 +723,24 @@ protected:
AppendData(&dlgTemplate, sizeof(DLGTEMPLATE)); AppendData(&dlgTemplate, sizeof(DLGTEMPLATE));
AppendWord(0); // 菜单 AppendWord(0); // 菜单
AppendWord(0); // 窗口类 AppendWord(0); // 窗口类
AppendString(_T("Select Language")); AppendString(_T("切换语言 / Switch Language"));
AlignToDword(); AlignToDword();
// 静态文本 // 静态文本
AddControl(0x0082, 15, 15, 50, 12, (WORD)-1, AddControl(0x0082, 15, 15, 50, 12, (WORD)-1,
SS_LEFT | WS_CHILD | WS_VISIBLE, _T("Language:")); SS_LEFT | WS_CHILD | WS_VISIBLE, _T("语言:"));
// ComboBox // ComboBox
AddControl(0x0085, 65, 13, 120, 150, 1001, AddControl(0x0085, 65, 13, 180, 150, 1001,
CBS_DROPDOWNLIST | WS_CHILD | WS_VISIBLE | WS_TABSTOP | WS_VSCROLL, _T("")); CBS_DROPDOWNLIST | WS_CHILD | WS_VISIBLE | WS_TABSTOP | WS_VSCROLL, _T(""));
// OK 按钮 // OK 按钮
AddControl(0x0080, 45, 50, 50, 14, IDOK, AddControl(0x0080, 75, 50, 50, 14, IDOK,
BS_DEFPUSHBUTTON | WS_CHILD | WS_VISIBLE | WS_TABSTOP, _T("OK")); BS_DEFPUSHBUTTON | WS_CHILD | WS_VISIBLE | WS_TABSTOP, _T("确定"));
// Cancel 按钮 // Cancel 按钮
AddControl(0x0080, 105, 50, 50, 14, IDCANCEL, AddControl(0x0080, 135, 50, 50, 14, IDCANCEL,
BS_PUSHBUTTON | WS_CHILD | WS_VISIBLE | WS_TABSTOP, _T("Cancel")); BS_PUSHBUTTON | WS_CHILD | WS_VISIBLE | WS_TABSTOP, _T("取消"));
return (LPCDLGTEMPLATE)m_templateBuffer.data(); return (LPCDLGTEMPLATE)m_templateBuffer.data();
} }
+610 -1
View File
@@ -840,6 +840,129 @@ Json::Value BuildServiceListOutputSchema() {
return schema; return schema;
} }
// ========== P7 输入/输出 schema ==========
// 公共:为 props 追加必填的 id 字符串属性。
static void AddIdRequiredProp(Json::Value& props) {
Json::Value idProp(Json::objectValue);
idProp["type"] = "string";
idProp["description"] = u8"主机 id,取 list_online_hosts / search_hosts 返回的 id 字段";
props["id"] = idProp;
}
// 公共:组装 {id, ...额外必填} 的 object schema。
static Json::Value FinalizeIdSchema(Json::Value props, std::initializer_list<const char*> extraRequired) {
Json::Value schema(Json::objectValue);
schema["type"] = "object";
schema["properties"] = props;
Json::Value required(Json::arrayValue);
required.append("id");
for (const char* r : extraRequired) required.append(r);
schema["required"] = required;
return schema;
}
// 公共:{ ok: boolean } 输出 schema(operate_window / control_service / power_action 共用)。
static Json::Value BuildOkOutputSchema() {
Json::Value props(Json::objectValue);
Json::Value ok(Json::objectValue);
ok["type"] = "boolean";
props["ok"] = ok;
Json::Value schema(Json::objectValue);
schema["type"] = "object";
schema["properties"] = props;
return schema;
}
Json::Value BuildKillProcessInputSchema() {
Json::Value props(Json::objectValue);
AddIdRequiredProp(props);
Json::Value pidsProp(Json::objectValue);
pidsProp["type"] = "array";
Json::Value pidItems(Json::objectValue);
pidItems["type"] = "integer";
pidsProp["items"] = pidItems;
pidsProp["description"] = u8"要终止的进程 PID 列表(至少 1 个)";
props["pids"] = pidsProp;
return FinalizeIdSchema(props, { "pids" });
}
Json::Value BuildKillProcessOutputSchema() {
Json::Value props(Json::objectValue);
Json::Value remProp(Json::objectValue);
remProp["type"] = "array";
Json::Value remItems(Json::objectValue);
remItems["type"] = "integer";
remProp["items"] = remItems;
remProp["description"] = u8"杀完后仍在运行的 PID(含权限不足未能终止的)";
props["remaining"] = remProp;
Json::Value schema(Json::objectValue);
schema["type"] = "object";
schema["properties"] = props;
return schema;
}
Json::Value BuildOperateWindowInputSchema() {
Json::Value props(Json::objectValue);
AddIdRequiredProp(props);
Json::Value hwndProp(Json::objectValue);
hwndProp["type"] = "integer";
hwndProp["description"] = u8"窗口句柄,取 list_windows 返回的 hwnd 字段";
props["hwnd"] = hwndProp;
Json::Value actionProp(Json::objectValue);
actionProp["type"] = "string";
actionProp["description"] = u8"操作:close(关闭) | show(显示) | hide(隐藏) | minimize(最小化) | maximize(最大化) | restore(还原)";
props["action"] = actionProp;
return FinalizeIdSchema(props, { "hwnd", "action" });
}
Json::Value BuildOperateWindowOutputSchema() {
return BuildOkOutputSchema();
}
Json::Value BuildControlServiceInputSchema() {
Json::Value props(Json::objectValue);
AddIdRequiredProp(props);
Json::Value nameProp(Json::objectValue);
nameProp["type"] = "string";
nameProp["description"] = u8"服务名,取 list_services 返回的 service_name 字段";
props["service_name"] = nameProp;
Json::Value actionProp(Json::objectValue);
actionProp["type"] = "string";
actionProp["description"] = u8"操作:start(启动) | stop(停止) | set_auto(设为自动) | set_manual(设为手动)";
props["action"] = actionProp;
return FinalizeIdSchema(props, { "service_name", "action" });
}
Json::Value BuildControlServiceOutputSchema() {
return BuildOkOutputSchema();
}
Json::Value BuildPowerActionInputSchema() {
Json::Value props(Json::objectValue);
AddIdRequiredProp(props);
Json::Value actionProp(Json::objectValue);
actionProp["type"] = "string";
actionProp["description"] = u8"操作:logout(注销) | shutdown(关机) | reboot(重启)";
props["action"] = actionProp;
return FinalizeIdSchema(props, { "action" });
}
Json::Value BuildPowerActionOutputSchema() {
return BuildOkOutputSchema();
}
// list_registry 的 inputSchema(id 必填,path 可选;path 缺省/空 = 列根键) // list_registry 的 inputSchema(id 必填,path 可选;path 缺省/空 = 列根键)
Json::Value BuildListRegistryInputSchema() { Json::Value BuildListRegistryInputSchema() {
Json::Value props(Json::objectValue); Json::Value props(Json::objectValue);
@@ -1163,6 +1286,20 @@ Json::Value BuildUploadFileOutputSchema();
std::string BuildUploadFile(const Json::Value& id, const Json::Value& args, CMy2015RemoteDlg* parent); std::string BuildUploadFile(const Json::Value& id, const Json::Value& args, CMy2015RemoteDlg* parent);
std::string BuildRemoteClipboard(const Json::Value& id, const Json::Value& args, CMy2015RemoteDlg* parent); std::string BuildRemoteClipboard(const Json::Value& id, const Json::Value& args, CMy2015RemoteDlg* parent);
// ===== P7 前置声明(定义见下方「tools/call 分派」前)=====
Json::Value BuildKillProcessInputSchema();
Json::Value BuildKillProcessOutputSchema();
Json::Value BuildOperateWindowInputSchema();
Json::Value BuildOperateWindowOutputSchema();
Json::Value BuildControlServiceInputSchema();
Json::Value BuildControlServiceOutputSchema();
Json::Value BuildPowerActionInputSchema();
Json::Value BuildPowerActionOutputSchema();
std::string BuildKillProcess(const Json::Value& id, const Json::Value& args, CMy2015RemoteDlg* parent);
std::string BuildOperateWindow(const Json::Value& id, const Json::Value& args, CMy2015RemoteDlg* parent);
std::string BuildControlService(const Json::Value& id, const Json::Value& args, CMy2015RemoteDlg* parent);
std::string BuildPowerAction(const Json::Value& id, const Json::Value& args, CMy2015RemoteDlg* parent);
// tools/list // tools/list
std::string BuildToolsListResult(const Json::Value& id) { std::string BuildToolsListResult(const Json::Value& id) {
Json::Value result(Json::objectValue); Json::Value result(Json::objectValue);
@@ -1433,6 +1570,43 @@ std::string BuildToolsListResult(const Json::Value& id) {
tools.append(tool); tools.append(tool);
} }
// 18) kill_process / operate_window / control_service / power_action(P7:设备管理控制,
// 仅 Windows,安全门:McpManage=1 且 McpReadonly=0)
if (CMcpServer::Instance().IsManageEnabled() && !CMcpServer::Instance().IsReadonly()) {
{
Json::Value tool(Json::objectValue);
tool["name"] = "kill_process";
tool["description"] = u8"终止指定在线 Windows 主机上的一个或多个进程(TerminateProcess 强制结束,不保存进程状态,可能造成数据丢失)。返回 remaining(杀完后仍在运行的 PID)。不可撤销,请谨慎确认 PID。";
tool["inputSchema"] = BuildKillProcessInputSchema();
tool["outputSchema"] = BuildKillProcessOutputSchema();
tools.append(tool);
}
{
Json::Value tool(Json::objectValue);
tool["name"] = "operate_window";
tool["description"] = u8"对指定在线 Windows 主机的窗口执行操作:close(关闭)/show(显示)/hide(隐藏)/minimize(最小化)/maximize(最大化)/restore(还原)。hwnd 取 list_windows 返回的 hwnd。ok 仅表示已下发(窗口句柄可能已失效)。";
tool["inputSchema"] = BuildOperateWindowInputSchema();
tool["outputSchema"] = BuildOperateWindowOutputSchema();
tools.append(tool);
}
{
Json::Value tool(Json::objectValue);
tool["name"] = "control_service";
tool["description"] = u8"控制指定在线 Windows 主机的服务:start(启动)/stop(停止)/set_auto(设为自动)/set_manual(设为手动)。service_name 取 list_services 返回的 service_name。ok 仅表示已下发(成功与否需再查 list_services 核对状态)。";
tool["inputSchema"] = BuildControlServiceInputSchema();
tool["outputSchema"] = BuildControlServiceOutputSchema();
tools.append(tool);
}
{
Json::Value tool(Json::objectValue);
tool["name"] = "power_action";
tool["description"] = u8"对指定在线 Windows 主机执行电源操作:logout(注销)/shutdown(关机)/reboot(重启)。不可撤销,主机将立即下线;需客户端管理员权限。";
tool["inputSchema"] = BuildPowerActionInputSchema();
tool["outputSchema"] = BuildPowerActionOutputSchema();
tools.append(tool);
}
}
result["tools"] = tools; result["tools"] = tools;
return BuildResult(id, result); return BuildResult(id, result);
} }
@@ -4175,6 +4349,332 @@ std::string BuildUploadFile(const Json::Value& id, const Json::Value& args, CMy2
return BuildError(id, -32001, msg); return BuildError(id, -32001, msg);
} }
// ========== P7:设备管理控制 ==========
// 读取 JSON number 或纯数字字符串为 uint64。
static bool ReadUInt64(const Json::Value& v, uint64_t& out) {
if (v.isNumeric()) { out = v.asUInt64(); return true; }
if (v.isString()) {
std::string s = v.asString();
if (IsDigits(s)) { out = strtoull(s.c_str(), nullptr, 10); return true; }
}
return false;
}
static bool ReadUInt64Arg(const Json::Value& args, const char* key, uint64_t& out) {
if (!args.isMember(key)) return false;
return ReadUInt64(args[key], out);
}
// 写审计(杀进程/窗口/服务/电源共用):PostMessageA 落到服务端消息/审计日志(不可关闭)。
static void AuditManageAction(CMy2015RemoteDlg* parent, uint64_t devId,
const std::string& tool, const std::string& detail) {
if (!parent) return;
std::string text = "host " + std::to_string(devId) + " " + tool + ": " + detail;
parent->PostMessageA(WM_SHOWERRORMSG,
(WPARAM)new CString(ToAnsi(text, 936).c_str()),
(LPARAM)new CString(_TR("MCP管理操作")));
}
// kill_process:主连接 COMMAND_SYSTEM → 系统子连接 TOKEN_PSLIST(就绪)→ COMMAND_KILLPROCESS
// → 补发 COMMAND_PSLIST → 第二个 TOKEN_PSLIST(确认)。payload 与 SystemDlg::OnPlistKill 一致。
std::string BuildKillProcess(const Json::Value& id, const Json::Value& args, CMy2015RemoteDlg* parent) {
uint64_t devId = 0;
std::string err;
if (!ParseHostIdArg(args, devId, err))
return BuildError(id, -32602, err);
context* ctx = FindMainContext(parent, devId);
if (!ctx)
return BuildError(id, -32002, "Host not found or offline: " + std::to_string(devId));
CString clientType = ctx->GetAdditionalData(RES_CLIENT_TYPE);
if (clientType == "LNX" || clientType == "MAC" || clientType == "APK")
return BuildError(id, -32005, "kill_process is only supported on Windows hosts");
CMcpServer& mcp = CMcpServer::Instance();
if (mcp.IsReadonly())
return BuildError(id, -32006, "kill_process is disabled: MCP is in read-only mode (set McpReadonly=0 to enable)");
if (!mcp.IsManageEnabled())
return BuildError(id, -32006, "kill_process is disabled: management operations are off (set McpManage=1 to enable)");
const Json::Value& pids = args["pids"];
if (!pids.isArray() || pids.size() == 0)
return BuildError(id, -32602, "Missing or invalid parameter: pids (non-empty integer array)");
std::vector<uint64_t> pidList;
std::vector<BYTE> followUp;
followUp.push_back((BYTE)COMMAND_KILLPROCESS);
for (unsigned int i = 0; i < pids.size(); ++i) {
uint64_t pid = 0;
if (!ReadUInt64(pids[i], pid) || pid == 0)
return BuildError(id, -32602, "pids must be non-zero integers");
pidList.push_back(pid);
DWORD d = (DWORD)pid;
followUp.push_back((BYTE)(d & 0xFF));
followUp.push_back((BYTE)((d >> 8) & 0xFF));
followUp.push_back((BYTE)((d >> 16) & 0xFF));
followUp.push_back((BYTE)((d >> 24) & 0xFF));
}
if (!mcp.BeginPendingFollowUp(devId, "kill_process", followUp))
return BuildError(id, -32003, "Device busy: another request is pending for this host");
BYTE cmd = COMMAND_SYSTEM;
if (!ctx->Send2Client(&cmd, 1)) {
mcp.ClearPending(devId);
return BuildError(id, -32004, "Failed to send command to host");
}
std::vector<BYTE> data;
if (!mcp.WaitPendingWrite(devId, data, kMcpToolTimeoutMs))
return BuildError(id, -32001, "Timeout waiting for process list after kill");
UINT cp = (clientType == "LNX" || clientType == "MAC") ? CP_UTF8 : 936;
Json::Value procs = ParseProcessList(data, cp);
Json::Value remaining(Json::arrayValue);
std::string detail = "pids=";
for (size_t i = 0; i < pidList.size(); ++i) {
if (i) detail += ",";
detail += std::to_string(pidList[i]);
bool stillAlive = false;
for (unsigned int j = 0; j < procs.size(); ++j) {
if (procs[j]["pid"].asUInt64() == pidList[i]) { stillAlive = true; break; }
}
if (stillAlive) remaining.append((Json::UInt64)pidList[i]);
}
AuditManageAction(parent, devId, "kill_process", detail);
Json::Value result(Json::objectValue);
Json::Value structuredContent(Json::objectValue);
structuredContent["remaining"] = remaining;
result["structuredContent"] = structuredContent;
Json::Value content(Json::arrayValue);
Json::Value item(Json::objectValue);
item["type"] = "text";
item["text"] = remaining.empty()
? std::string(u8"目标进程已全部终止。")
: std::string(u8"仍有 ") + std::to_string((int)remaining.size()) + std::string(u8" 个进程存活(见 remaining)。");
content.append(item);
result["content"] = content;
result["isError"] = false;
return BuildResult(id, result);
}
// operate_window:主连接 COMMAND_WSLIST → 窗口子连接 TOKEN_WSLIST(就绪)→ CMD_WINDOW_CLOSE
// (close 客户端自动回 TOKEN_WSLIST)或 CMD_WINDOW_TEST(test 补发 COMMAND_WSLIST 索取)。
// 20 字节零填充 buffer 复刻 SystemDlg::OnWlistClose/OnWlistHide/Recover/Max/Min(hwnd 为 DWORD)。
std::string BuildOperateWindow(const Json::Value& id, const Json::Value& args, CMy2015RemoteDlg* parent) {
uint64_t devId = 0;
std::string err;
if (!ParseHostIdArg(args, devId, err))
return BuildError(id, -32602, err);
context* ctx = FindMainContext(parent, devId);
if (!ctx)
return BuildError(id, -32002, "Host not found or offline: " + std::to_string(devId));
CString clientType = ctx->GetAdditionalData(RES_CLIENT_TYPE);
if (clientType == "LNX" || clientType == "MAC" || clientType == "APK")
return BuildError(id, -32005, "operate_window is only supported on Windows hosts");
CMcpServer& mcp = CMcpServer::Instance();
if (mcp.IsReadonly())
return BuildError(id, -32006, "operate_window is disabled: MCP is in read-only mode (set McpReadonly=0 to enable)");
if (!mcp.IsManageEnabled())
return BuildError(id, -32006, "operate_window is disabled: management operations are off (set McpManage=1 to enable)");
uint64_t hwnd = 0;
if (!ReadUInt64Arg(args, "hwnd", hwnd) || hwnd == 0)
return BuildError(id, -32602, "Missing or invalid parameter: hwnd (non-zero window handle)");
std::string action = Trim(GetStringArg(args, "action"));
BYTE lpMsgBuf[20] = { 0 };
DWORD h = (DWORD)hwnd;
if (action == "close") {
lpMsgBuf[0] = CMD_WINDOW_CLOSE;
memcpy(lpMsgBuf + 1, &h, sizeof(DWORD));
} else {
DWORD dHow;
if (action == "hide") dHow = SW_HIDE; // 0
else if (action == "show") dHow = SW_NORMAL; // 1
else if (action == "restore") dHow = SW_NORMAL; // 1
else if (action == "maximize") dHow = SW_MAXIMIZE; // 3
else if (action == "minimize") dHow = SW_MINIMIZE; // 6
else return BuildError(id, -32602,
"Invalid action: " + action + " (expect close|show|hide|minimize|maximize|restore)");
lpMsgBuf[0] = CMD_WINDOW_TEST;
memcpy(lpMsgBuf + 1, &h, sizeof(DWORD));
memcpy(lpMsgBuf + 1 + sizeof(DWORD), &dHow, sizeof(DWORD));
}
std::vector<BYTE> followUp(lpMsgBuf, lpMsgBuf + sizeof(lpMsgBuf));
if (!mcp.BeginPendingFollowUp(devId, "operate_window", followUp))
return BuildError(id, -32003, "Device busy: another request is pending for this host");
BYTE cmd = COMMAND_WSLIST;
if (!ctx->Send2Client(&cmd, 1)) {
mcp.ClearPending(devId);
return BuildError(id, -32004, "Failed to send command to host");
}
std::vector<BYTE> data;
if (!mcp.WaitPendingWrite(devId, data, kMcpToolTimeoutMs))
return BuildError(id, -32001, "Timeout waiting for window operation");
AuditManageAction(parent, devId, "operate_window",
"action=" + action + " hwnd=" + std::to_string(hwnd));
Json::Value result(Json::objectValue);
Json::Value structuredContent(Json::objectValue);
structuredContent["ok"] = true;
result["structuredContent"] = structuredContent;
Json::Value content(Json::arrayValue);
Json::Value item(Json::objectValue);
item["type"] = "text";
item["text"] = std::string(u8"已下发窗口操作:") + action;
content.append(item);
result["content"] = content;
result["isError"] = false;
return BuildResult(id, result);
}
// control_service:主连接 COMMAND_SERVICES → 服务子连接 TOKEN_SERVERLIST(就绪)→
// COMMAND_SERVICECONFIG → 客户端写后自动 SendServicesList(确认,无需补发)。
std::string BuildControlService(const Json::Value& id, const Json::Value& args, CMy2015RemoteDlg* parent) {
uint64_t devId = 0;
std::string err;
if (!ParseHostIdArg(args, devId, err))
return BuildError(id, -32602, err);
context* ctx = FindMainContext(parent, devId);
if (!ctx)
return BuildError(id, -32002, "Host not found or offline: " + std::to_string(devId));
CString clientType = ctx->GetAdditionalData(RES_CLIENT_TYPE);
if (clientType == "LNX" || clientType == "MAC" || clientType == "APK")
return BuildError(id, -32005, "control_service is only supported on Windows hosts");
CMcpServer& mcp = CMcpServer::Instance();
if (mcp.IsReadonly())
return BuildError(id, -32006, "control_service is disabled: MCP is in read-only mode (set McpReadonly=0 to enable)");
if (!mcp.IsManageEnabled())
return BuildError(id, -32006, "control_service is disabled: management operations are off (set McpManage=1 to enable)");
std::string serviceName = Trim(GetStringArg(args, "service_name"));
if (serviceName.empty())
return BuildError(id, -32602, "Missing required parameter: service_name");
std::string action = Trim(GetStringArg(args, "action"));
BYTE bCmd;
if (action == "start") bCmd = 1;
else if (action == "stop") bCmd = 2;
else if (action == "set_auto") bCmd = 3;
else if (action == "set_manual") bCmd = 4;
else return BuildError(id, -32602,
"Invalid action: " + action + " (expect start|stop|set_auto|set_manual)");
// 服务名来自 Windows A 接口(GBK/936),下发前转 ANSI。payload 与 MachineDlg 一致:
// [COMMAND_SERVICECONFIG:1][bCmd:1][serviceName\0]。
std::string ansiName = ToAnsi(serviceName, 936);
std::vector<BYTE> followUp;
followUp.reserve(2 + ansiName.size() + 1);
followUp.push_back((BYTE)COMMAND_SERVICECONFIG);
followUp.push_back(bCmd);
followUp.insert(followUp.end(), ansiName.begin(), ansiName.end());
followUp.push_back(0);
if (!mcp.BeginPendingFollowUp(devId, "control_service", followUp))
return BuildError(id, -32003, "Device busy: another request is pending for this host");
BYTE cmd = COMMAND_SERVICES;
if (!ctx->Send2Client(&cmd, 1)) {
mcp.ClearPending(devId);
return BuildError(id, -32004, "Failed to send command to host");
}
std::vector<BYTE> data;
if (!mcp.WaitPendingWrite(devId, data, kMcpToolTimeoutMs))
return BuildError(id, -32001, "Timeout waiting for service operation");
AuditManageAction(parent, devId, "control_service",
"service=" + serviceName + " action=" + action);
Json::Value result(Json::objectValue);
Json::Value structuredContent(Json::objectValue);
structuredContent["ok"] = true;
result["structuredContent"] = structuredContent;
Json::Value content(Json::arrayValue);
Json::Value item(Json::objectValue);
item["type"] = "text";
item["text"] = std::string(u8"已下发服务操作:") + action + std::string(u8" ") + serviceName;
content.append(item);
result["content"] = content;
result["isError"] = false;
return BuildResult(id, result);
}
// power_action:主连接 TOKEN_MACHINE_MANAGE,发完即走(无响应);主机立即离线。
std::string BuildPowerAction(const Json::Value& id, const Json::Value& args, CMy2015RemoteDlg* parent) {
uint64_t devId = 0;
std::string err;
if (!ParseHostIdArg(args, devId, err))
return BuildError(id, -32602, err);
context* ctx = FindMainContext(parent, devId);
if (!ctx)
return BuildError(id, -32002, "Host not found or offline: " + std::to_string(devId));
CString clientType = ctx->GetAdditionalData(RES_CLIENT_TYPE);
if (clientType == "LNX" || clientType == "MAC" || clientType == "APK")
return BuildError(id, -32005, "power_action is only supported on Windows hosts");
CMcpServer& mcp = CMcpServer::Instance();
if (mcp.IsReadonly())
return BuildError(id, -32006, "power_action is disabled: MCP is in read-only mode (set McpReadonly=0 to enable)");
if (!mcp.IsManageEnabled())
return BuildError(id, -32006, "power_action is disabled: management operations are off (set McpManage=1 to enable)");
std::string action = Trim(GetStringArg(args, "action"));
BYTE type;
if (action == "logout") type = MACHINE_LOGOUT;
else if (action == "shutdown") type = MACHINE_SHUTDOWN;
else if (action == "reboot") type = MACHINE_REBOOT;
else return BuildError(id, -32602,
"Invalid action: " + action + " (expect logout|shutdown|reboot)");
// 主连接、发完即走、无响应(客户端 KernelManager 只校验 ulLength>1 并读 szBuffer[1])。
BYTE token[2] = { (BYTE)TOKEN_MACHINE_MANAGE, type };
if (!ctx->Send2Client(token, 2))
return BuildError(id, -32004, "Failed to send command to host");
AuditManageAction(parent, devId, "power_action", "action=" + action);
Json::Value result(Json::objectValue);
Json::Value structuredContent(Json::objectValue);
structuredContent["ok"] = true;
result["structuredContent"] = structuredContent;
Json::Value content(Json::arrayValue);
Json::Value item(Json::objectValue);
item["type"] = "text";
item["text"] = std::string(u8"已下发电源操作:") + action;
content.append(item);
result["content"] = content;
result["isError"] = false;
return BuildResult(id, result);
}
// tools/call 分派 // tools/call 分派
std::string BuildToolsCall(const Json::Value& root, CMy2015RemoteDlg* parent) { std::string BuildToolsCall(const Json::Value& root, CMy2015RemoteDlg* parent) {
const Json::Value& id = root["id"]; const Json::Value& id = root["id"];
@@ -4210,6 +4710,10 @@ std::string BuildToolsCall(const Json::Value& root, CMy2015RemoteDlg* parent) {
if (toolName == "remote_clipboard") return BuildRemoteClipboard(id, args, parent); if (toolName == "remote_clipboard") return BuildRemoteClipboard(id, args, parent);
if (toolName == "download_file") return BuildDownloadFile(id, args, parent); if (toolName == "download_file") return BuildDownloadFile(id, args, parent);
if (toolName == "upload_file") return BuildUploadFile(id, args, parent); if (toolName == "upload_file") return BuildUploadFile(id, args, parent);
if (toolName == "kill_process") return BuildKillProcess(id, args, parent);
if (toolName == "operate_window") return BuildOperateWindow(id, args, parent);
if (toolName == "control_service") return BuildControlService(id, args, parent);
if (toolName == "power_action") return BuildPowerAction(id, args, parent);
return BuildError(id, -32602, return BuildError(id, -32602,
"Unknown tool: " + (toolName.empty() ? std::string("(empty)") : toolName)); "Unknown tool: " + (toolName.empty() ? std::string("(empty)") : toolName));
@@ -4727,7 +5231,8 @@ bool CMcpServer::IsPending(uint64_t device_id) {
void CMcpServer::TakeMainResponse(uint64_t device_id, const BYTE* data, ULONG len) { void CMcpServer::TakeMainResponse(uint64_t device_id, const BYTE* data, ULONG len) {
std::lock_guard<std::mutex> lk(m_PendingMutex); std::lock_guard<std::mutex> lk(m_PendingMutex);
auto it = m_Pending.find(device_id); auto it = m_Pending.find(device_id);
if (it == m_Pending.end()) return; // 已超时清理 → 迟到数据,丢弃 if (it == m_Pending.end()) { Mprintf("[MCP-P7] TakeMainResponse dev=%llu len=%d: no pending (late/dropped)\n", device_id, (int)len); return; } // 已超时清理 → 迟到数据,丢弃
Mprintf("[MCP-P7] TakeMainResponse dev=%llu len=%d done\n", device_id, (int)len);
it->second.data.assign(data, data + len); it->second.data.assign(data, data + len);
it->second.done = true; it->second.done = true;
m_PendingCv.notify_one(); m_PendingCv.notify_one();
@@ -4902,6 +5407,110 @@ bool CMcpServer::WaitPendingRegistry(uint64_t device_id, std::vector<BYTE>& subk
return true; return true;
} }
// ===== P7:设备管理控制(写工具接管)=====
bool CMcpServer::BeginPendingFollowUp(uint64_t device_id, const std::string& tool,
const std::vector<BYTE>& followUp) {
if (IsFileTransferPending(device_id)) return false; // 该 host 有文件传输会话在飞(互斥,见 F2)
std::lock_guard<std::mutex> lk(m_PendingMutex);
if (m_Pending.find(device_id) != m_Pending.end()) return false; // 设备忙
PendingRequest r;
r.tool = tool;
r.followUp = followUp;
m_Pending[device_id] = std::move(r);
return true;
}
bool CMcpServer::OnSystemReady(uint64_t device_id, context* subCtx) {
std::string tool;
std::vector<BYTE> followUp;
{
std::lock_guard<std::mutex> lk(m_PendingMutex);
auto it = m_Pending.find(device_id);
if (it == m_Pending.end()) { Mprintf("[MCP-P7] OnSystemReady dev=%llu: no pending\n", device_id); return false; } // 无挂起 → 回落 MFC
tool = it->second.tool;
if (tool != "kill_process" && tool != "operate_window") { Mprintf("[MCP-P7] OnSystemReady dev=%llu tool=%s: not write tool\n", device_id, tool.c_str()); return false; }
if (it->second.followUpSent) { Mprintf("[MCP-P7] OnSystemReady dev=%llu tool=%s: CONFIRM token -> TakeMainResponse\n", device_id, tool.c_str()); return false; } // 确认包 → TakeMainResponse
followUp = it->second.followUp;
it->second.subCtx = subCtx;
it->second.followUpSent = true;
}
// 锁外下发写命令;kill/test 需补发刷新命令索取新列表(close/control_service 客户端自动回)。
BOOL b1 = subCtx->Send2Client(followUp.data(), (ULONG)followUp.size());
BYTE refresh = 0;
if (tool == "kill_process") {
refresh = (BYTE)COMMAND_PSLIST;
} else if (!followUp.empty() && followUp[0] == CMD_WINDOW_TEST) {
refresh = (BYTE)COMMAND_WSLIST;
}
if (refresh) {
// followUp 与 refresh 是两次独立 Send2Client,各自 PostQueuedCompletionStatus(IOWrite),
// worker 会对同一 OutCompressedBuffer 并发发起 WSASend,造成竞态/丢包。用 WaitSendIdle
// 确定性等首个包真正发完再发刷新包(比 MFC 的 Sleep(100) 盲等更稳)。
subCtx->WaitSendIdle(2000);
BOOL b2 = subCtx->Send2Client(&refresh, 1);
Mprintf("[MCP-P7] OnSystemReady dev=%llu tool=%s READY: followUpSend=%d refreshSend=%d refresh=%d\n",
device_id, tool.c_str(), b1 ? 1 : 0, b2 ? 1 : 0, (int)refresh);
}
return true; // 已接管,不 CancelIO
}
bool CMcpServer::OnServiceReady(uint64_t device_id, context* subCtx) {
std::vector<BYTE> followUp;
{
std::lock_guard<std::mutex> lk(m_PendingMutex);
auto it = m_Pending.find(device_id);
if (it == m_Pending.end()) return false;
if (it->second.tool != "control_service") return false;
if (it->second.followUpSent) return false; // 确认包 → 调用方 TakeMainResponse
followUp = it->second.followUp;
it->second.subCtx = subCtx;
it->second.followUpSent = true;
}
subCtx->Send2Client(followUp.data(), (ULONG)followUp.size());
return true; // 客户端写后自动回,无需补发
}
bool CMcpServer::WaitPendingWrite(uint64_t device_id, std::vector<BYTE>& out, int timeoutMs) {
context* subCtx = nullptr;
std::unique_lock<std::mutex> lk(m_PendingMutex);
auto it = m_Pending.find(device_id);
if (it == m_Pending.end()) return false;
bool signaled = m_PendingCv.wait_for(lk, std::chrono::milliseconds(timeoutMs),
[&] { return it->second.done; });
if (!signaled || it->second.data.empty()) {
subCtx = it->second.subCtx;
Mprintf("[MCP-P7] WaitPendingWrite dev=%llu TIMEOUT: signaled=%d dataEmpty=%d subCtx=%p\n",
device_id, signaled ? 1 : 0, it->second.data.empty() ? 1 : 0, (void*)subCtx);
m_Pending.erase(it); // 超时/空数据 → 清理
lk.unlock();
if (subCtx) subCtx->CancelIO(); // 锁外关子连接,防孤儿子连接
return false;
}
out = std::move(it->second.data);
Mprintf("[MCP-P7] WaitPendingWrite dev=%llu OK len=%d\n", device_id, (int)out.size());
m_Pending.erase(it);
return true;
}
void CMcpServer::OnManageSubClosed(context* subCtx) {
std::lock_guard<std::mutex> lk(m_PendingMutex);
// 只匹配「已接管、等确认包」的写工具子连接(read 工具从不存 subCtx,天然不命中)。
// 扫描而非按 device_id 查找,避免依赖断线后 GetClientID 是否仍有效。
for (auto& kv : m_Pending) {
if (kv.second.subCtx == subCtx) {
Mprintf("[MCP-P7] OnManageSubClosed subCtx=%p tool=%s: sub-conn closed before confirm\n",
(void*)subCtx, kv.second.tool.c_str());
kv.second.subCtx = nullptr; // 断线后子连接已死/将回收,超时路径不得再 CancelIO
kv.second.done = true; // 唤醒等待线程(WaitPendingWrite 见 data 空 → 返回失败)
m_PendingCv.notify_all();
break;
}
}
}
// rand_s:Windows CRT 加密安全随机源(基于系统 CSPRNG)。其声明需在 <stdlib.h> 前 // rand_s:Windows CRT 加密安全随机源(基于系统 CSPRNG)。其声明需在 <stdlib.h> 前
// 定义 _CRT_RAND_S;为避免依赖 PCH 的包含顺序,这里手动声明其导出原型(errno_t == int)。 // 定义 _CRT_RAND_S;为避免依赖 PCH 的包含顺序,这里手动声明其导出原型(errno_t == int)。
extern "C" int __cdecl rand_s(unsigned int* randomValue); extern "C" int __cdecl rand_s(unsigned int* randomValue);
+33
View File
@@ -242,6 +242,34 @@ public:
// 收尾:擦会话 + 锁外 CancelIO 文件管理器/流式两条子链接(幂等)。 // 收尾:擦会话 + 锁外 CancelIO 文件管理器/流式两条子链接(幂等)。
void ClearFileTransfer(uint64_t device_id); void ClearFileTransfer(uint64_t device_id);
// ===== P7:设备管理控制(kill_process / operate_window / control_service / power_action)=====
// 写工具复用 m_Pending 单飞行;followUp/followUpSent/subCtx 标记接管进度。
// 「就绪 token」与「确认 token」是同一 token(客户端子连接构造即回一包,写后再回一包),
// 故必须用 followUpSent 区分两次到达,不能靠 token 区分(与 list_registry 的 PATH/KEY 双 token 不同)。
void SetManageEnabled(bool enabled) { m_manageEnabled = enabled; }
bool IsManageEnabled() const { return m_manageEnabled; }
// 登记写挂起:与 BeginPending 同单飞行,另预填 followUp(写命令 payload)。
bool BeginPendingFollowUp(uint64_t device_id, const std::string& tool,
const std::vector<BYTE>& followUp);
// MessageHandle(TOKEN_PSLIST/TOKEN_WSLIST) 调用:命中且为写工具(kill_process/operate_window)
// 则接管子连接、下发 followUp(返回 true=已接管,调用方不 CancelIO);否则 false(回落 MFC
// 或按确认包走 TakeMainResponse)。
bool OnSystemReady(uint64_t device_id, context* subCtx);
// MessageHandle(TOKEN_SERVERLIST) 调用:命中且为 control_service 则接管、下发
// COMMAND_SERVICECONFIG(返回 true=已接管);否则 false。
bool OnServiceReady(uint64_t device_id, context* subCtx);
// 工具线程:等待写确认包(第二个同 token 包);超时/空数据清挂起并 CancelIO 子连接。
bool WaitPendingWrite(uint64_t device_id, std::vector<BYTE>& out, int timeoutMs);
// OfflineProc 调用:接管的子连接断开时清挂起(置 done 并清 subCtx,唤醒等待线程)。
// 不擦表(由 WaitPendingWrite 持有迭代器时自行擦除,防迭代器失效);清 subCtx 使
// 超时路径不会对已回收(连接池复用)的 context 误 CancelIO。
void OnManageSubClosed(context* subCtx);
// 安全配置(启动时由 CMy2015RemoteDlg 读 THIS_CFG 后设置)。 // 安全配置(启动时由 CMy2015RemoteDlg 读 THIS_CFG 后设置)。
void SetReadonly(bool readonly) { m_readonly = readonly; } void SetReadonly(bool readonly) { m_readonly = readonly; }
void SetCmdWhitelist(const std::string& whitelist) { m_cmdWhitelist = whitelist; } void SetCmdWhitelist(const std::string& whitelist) { m_cmdWhitelist = whitelist; }
@@ -274,6 +302,10 @@ private:
bool pathDone = false; // list_registry:已收到 TOKEN_REG_PATH bool pathDone = false; // list_registry:已收到 TOKEN_REG_PATH
bool keyDone = false; // list_registry:已收到 TOKEN_REG_KEY bool keyDone = false; // list_registry:已收到 TOKEN_REG_KEY
bool done = false; bool done = false;
// ===== P7:写工具(kill_process / operate_window / control_service)接管进度 =====
std::vector<BYTE> followUp; // 接管后下发的写命令 payload(首字节 = 写命令)
bool followUpSent = false; // false=等子连接就绪(首包);true=已发写命令、等确认包
context* subCtx = nullptr; // 接管的子连接(超时/失败 CancelIO 用,防孤儿子连接)
}; };
std::mutex m_PendingMutex; std::mutex m_PendingMutex;
std::condition_variable m_PendingCv; std::condition_variable m_PendingCv;
@@ -348,6 +380,7 @@ private:
bool m_terminalEnabled = false; // 持久终端开关(默认关;要求 m_readonly=false) bool m_terminalEnabled = false; // 持久终端开关(默认关;要求 m_readonly=false)
bool m_remoteControlEnabled = false; // 远程控制开关(默认关;要求 m_readonly=false) bool m_remoteControlEnabled = false; // 远程控制开关(默认关;要求 m_readonly=false)
bool m_fileTransferEnabled = false; // 文件传输开关(默认关;download_file) bool m_fileTransferEnabled = false; // 文件传输开关(默认关;download_file)
bool m_manageEnabled = false; // P7 管理控制开关(默认关;要求 m_readonly=false)
}; };
// 全局访问器(仿 WebService(),见 WebService.h 末尾) // 全局访问器(仿 WebService(),见 WebService.h 末尾)
+13 -2
View File
@@ -145,6 +145,9 @@ BOOL CMcpSettingsDlg::OnInitDialog()
m_btnFileTransfer.Create(_TR("启用文件传输(download_file)"), m_btnFileTransfer.Create(_TR("启用文件传输(download_file)"),
WS_CHILD | WS_VISIBLE | WS_TABSTOP | BS_AUTOCHECKBOX, WS_CHILD | WS_VISIBLE | WS_TABSTOP | BS_AUTOCHECKBOX,
r0, this, IDC_MCP_FILETRANSFER); r0, this, IDC_MCP_FILETRANSFER);
m_btnManage.Create(_TR("启用管理操作(进程/窗口/服务/电源)"),
WS_CHILD | WS_VISIBLE | WS_TABSTOP | BS_AUTOCHECKBOX,
r0, this, IDC_MCP_MANAGE);
m_lblWhitelist.Create(_TR("命令白名单"), WS_CHILD | WS_VISIBLE, r0, this, (UINT)-1); m_lblWhitelist.Create(_TR("命令白名单"), WS_CHILD | WS_VISIBLE, r0, this, (UINT)-1);
m_editWhitelist.Create(WS_CHILD | WS_VISIBLE | WS_BORDER | WS_TABSTOP | m_editWhitelist.Create(WS_CHILD | WS_VISIBLE | WS_BORDER | WS_TABSTOP |
ES_MULTILINE | ES_AUTOVSCROLL | ES_WANTRETURN | WS_VSCROLL, ES_MULTILINE | ES_AUTOVSCROLL | ES_WANTRETURN | WS_VSCROLL,
@@ -168,6 +171,7 @@ BOOL CMcpSettingsDlg::OnInitDialog()
m_btnTerminal.SendMessage(WM_SETFONT, (WPARAM)hFont, MAKELPARAM(TRUE, 0)); m_btnTerminal.SendMessage(WM_SETFONT, (WPARAM)hFont, MAKELPARAM(TRUE, 0));
m_btnRemoteControl.SendMessage(WM_SETFONT, (WPARAM)hFont, MAKELPARAM(TRUE, 0)); m_btnRemoteControl.SendMessage(WM_SETFONT, (WPARAM)hFont, MAKELPARAM(TRUE, 0));
m_btnFileTransfer.SendMessage(WM_SETFONT, (WPARAM)hFont, MAKELPARAM(TRUE, 0)); m_btnFileTransfer.SendMessage(WM_SETFONT, (WPARAM)hFont, MAKELPARAM(TRUE, 0));
m_btnManage.SendMessage(WM_SETFONT, (WPARAM)hFont, MAKELPARAM(TRUE, 0));
m_lblWhitelist.SendMessage(WM_SETFONT, (WPARAM)hFont, MAKELPARAM(TRUE, 0)); m_lblWhitelist.SendMessage(WM_SETFONT, (WPARAM)hFont, MAKELPARAM(TRUE, 0));
m_editWhitelist.SendMessage(WM_SETFONT, (WPARAM)hFont, MAKELPARAM(TRUE, 0)); m_editWhitelist.SendMessage(WM_SETFONT, (WPARAM)hFont, MAKELPARAM(TRUE, 0));
m_btnOK.SendMessage(WM_SETFONT, (WPARAM)hFont, MAKELPARAM(TRUE, 0)); m_btnOK.SendMessage(WM_SETFONT, (WPARAM)hFont, MAKELPARAM(TRUE, 0));
@@ -184,6 +188,7 @@ BOOL CMcpSettingsDlg::OnInitDialog()
int terminal = THIS_CFG.GetInt("settings", "McpTerminal", 0); int terminal = THIS_CFG.GetInt("settings", "McpTerminal", 0);
int remoteControl = THIS_CFG.GetInt("settings", "McpRemoteControl", 0); int remoteControl = THIS_CFG.GetInt("settings", "McpRemoteControl", 0);
int fileTransfer = THIS_CFG.GetInt("settings", "McpFileTransfer", 0); int fileTransfer = THIS_CFG.GetInt("settings", "McpFileTransfer", 0);
int manage = THIS_CFG.GetInt("settings", "McpManage", 0);
std::string whitelist = THIS_CFG.GetStr("settings", "McpCmdWhitelist", ""); std::string whitelist = THIS_CFG.GetStr("settings", "McpCmdWhitelist", "");
m_btnEnable.SetCheck(enabled ? BST_CHECKED : BST_UNCHECKED); m_btnEnable.SetCheck(enabled ? BST_CHECKED : BST_UNCHECKED);
@@ -194,6 +199,7 @@ BOOL CMcpSettingsDlg::OnInitDialog()
m_btnTerminal.SetCheck(terminal ? BST_CHECKED : BST_UNCHECKED); m_btnTerminal.SetCheck(terminal ? BST_CHECKED : BST_UNCHECKED);
m_btnRemoteControl.SetCheck(remoteControl ? BST_CHECKED : BST_UNCHECKED); m_btnRemoteControl.SetCheck(remoteControl ? BST_CHECKED : BST_UNCHECKED);
m_btnFileTransfer.SetCheck(fileTransfer ? BST_CHECKED : BST_UNCHECKED); m_btnFileTransfer.SetCheck(fileTransfer ? BST_CHECKED : BST_UNCHECKED);
m_btnManage.SetCheck(manage ? BST_CHECKED : BST_UNCHECKED);
// 白名单存储为逗号分隔,展示为每行一条。 // 白名单存储为逗号分隔,展示为每行一条。
m_editWhitelist.SetWindowText(CString(WhitelistForDisplay(whitelist).c_str())); m_editWhitelist.SetWindowText(CString(WhitelistForDisplay(whitelist).c_str()));
@@ -213,6 +219,7 @@ void CMcpSettingsDlg::OnOK()
bool terminal = (m_btnTerminal.GetCheck() == BST_CHECKED); bool terminal = (m_btnTerminal.GetCheck() == BST_CHECKED);
bool remoteControl = (m_btnRemoteControl.GetCheck() == BST_CHECKED); bool remoteControl = (m_btnRemoteControl.GetCheck() == BST_CHECKED);
bool fileTransfer = (m_btnFileTransfer.GetCheck() == BST_CHECKED); bool fileTransfer = (m_btnFileTransfer.GetCheck() == BST_CHECKED);
bool manage = (m_btnManage.GetCheck() == BST_CHECKED);
// 端口校验:1-65535 // 端口校验:1-65535
int port = atoi(CT2A(sPort)); int port = atoi(CT2A(sPort));
@@ -239,14 +246,15 @@ void CMcpSettingsDlg::OnOK()
THIS_CFG.SetInt("settings", "McpTerminal", terminal ? 1 : 0); THIS_CFG.SetInt("settings", "McpTerminal", terminal ? 1 : 0);
THIS_CFG.SetInt("settings", "McpRemoteControl", remoteControl ? 1 : 0); THIS_CFG.SetInt("settings", "McpRemoteControl", remoteControl ? 1 : 0);
THIS_CFG.SetInt("settings", "McpFileTransfer", fileTransfer ? 1 : 0); THIS_CFG.SetInt("settings", "McpFileTransfer", fileTransfer ? 1 : 0);
THIS_CFG.SetInt("settings", "McpManage", manage ? 1 : 0);
std::string whitelist = CT2A(sWhitelist); std::string whitelist = CT2A(sWhitelist);
whitelist = NormalizeWhitelist(whitelist); whitelist = NormalizeWhitelist(whitelist);
THIS_CFG.SetStr("settings", "McpCmdWhitelist", whitelist); THIS_CFG.SetStr("settings", "McpCmdWhitelist", whitelist);
// 拆成两段可翻译的单行键,中间用 \r\n 连接(多行键无法在 INI 中表示) // 拆成两段可翻译的单行键,中间用 \r\n 连接(多行键无法在 INI 中表示)
MessageBox(_TR("MCP 设置已保存。") + _T("\r\n") + MessageBox(_TR("MCP 设置已保存。") + _T("\r\n") +
_TR("启用/端口/绑定地址/Token/只读/白名单/持久终端/远程控制/文件传输的改动需重启程序生效。") + _T("\r\n") + _TR("启用/端口/绑定地址/Token/只读/白名单/持久终端/远程控制/文件传输/管理操作的改动需重启程序生效。") + _T("\r\n") +
_TR("持久终端与远程控制仅在只读模式关闭时生效。"), _TR("持久终端、远程控制与管理操作仅在只读模式关闭时生效。"),
_TR("提示"), MB_ICONINFORMATION); _TR("提示"), MB_ICONINFORMATION);
CDialog::OnOK(); CDialog::OnOK();
@@ -289,6 +297,9 @@ void CMcpSettingsDlg::LayoutControls(int cx, int cy)
m_btnFileTransfer.MoveWindow(margin, y, cx - margin * 2, 22); m_btnFileTransfer.MoveWindow(margin, y, cx - margin * 2, 22);
y += 30; y += 30;
m_btnManage.MoveWindow(margin, y, cx - margin * 2, 22);
y += 30;
const int whitelistH = 90; const int whitelistH = 90;
m_lblWhitelist.MoveWindow(margin, y, labelW, rowH); m_lblWhitelist.MoveWindow(margin, y, labelW, rowH);
m_editWhitelist.MoveWindow(margin + labelW, y - 2, cx - margin * 2 - labelW, whitelistH); m_editWhitelist.MoveWindow(margin + labelW, y - 2, cx - margin * 2 - labelW, whitelistH);
+2
View File
@@ -31,6 +31,7 @@ private:
IDC_MCP_TERMINAL = 1007, // 「启用持久终端」复选框(全命令,无白名单,要求只读关) IDC_MCP_TERMINAL = 1007, // 「启用持久终端」复选框(全命令,无白名单,要求只读关)
IDC_MCP_REMOTECONTROL = 1008, // 「启用远程控制」复选框(AI 操控桌面,要求只读关) IDC_MCP_REMOTECONTROL = 1008, // 「启用远程控制」复选框(AI 操控桌面,要求只读关)
IDC_MCP_FILETRANSFER = 1009, // 「启用文件传输」复选框(download_file,不要求只读关) IDC_MCP_FILETRANSFER = 1009, // 「启用文件传输」复选框(download_file,不要求只读关)
IDC_MCP_MANAGE = 1010, // 「启用管理操作」复选框(kill/窗口/服务/电源,要求只读关)
}; };
CButton m_btnEnable; CButton m_btnEnable;
@@ -40,6 +41,7 @@ private:
CButton m_btnTerminal; CButton m_btnTerminal;
CButton m_btnRemoteControl; CButton m_btnRemoteControl;
CButton m_btnFileTransfer; CButton m_btnFileTransfer;
CButton m_btnManage;
CStatic m_lblWhitelist; CStatic m_lblWhitelist;
CEdit m_editWhitelist; CEdit m_editWhitelist;
CButton m_btnOK, m_btnCancel; CButton m_btnOK, m_btnCancel;
+38 -3
View File
@@ -56,7 +56,7 @@ BOOL NotifySettingsDlg::OnInitDialog()
SetDlgItemText(IDC_STATIC_TRIGGER, _TR(_T("触发条件:"))); SetDlgItemText(IDC_STATIC_TRIGGER, _TR(_T("触发条件:")));
SetDlgItemText(IDC_STATIC_MATCH_COLUMN, _TR(_T("匹配列:"))); SetDlgItemText(IDC_STATIC_MATCH_COLUMN, _TR(_T("匹配列:")));
SetDlgItemText(IDC_STATIC_KEYWORDS, _TR(_T("关键词:"))); SetDlgItemText(IDC_STATIC_KEYWORDS, _TR(_T("关键词:")));
SetDlgItemText(IDC_STATIC_KEYWORDS_HINT, _TR(_T("(多个关键词用分号分隔,匹配任一项即触发通知)"))); SetDlgItemText(IDC_STATIC_KEYWORDS_HINT, _TR(_T("(每行一个关键词,匹配任一项即触发通知)")));
SetDlgItemText(IDC_STATIC_NOTIFY_TIP, _TR(_T("提示: 同一主机 60 分钟内仅通知一次"))); SetDlgItemText(IDC_STATIC_NOTIFY_TIP, _TR(_T("提示: 同一主机 60 分钟内仅通知一次")));
SetDlgItemText(IDOK, _TR("确定")); SetDlgItemText(IDOK, _TR("确定"));
SetDlgItemText(IDCANCEL, _TR("取消")); SetDlgItemText(IDCANCEL, _TR("取消"));
@@ -154,7 +154,24 @@ void NotifySettingsDlg::LoadSettings()
m_comboNotifyColumn.SetCurSel(rule.columnIndex); m_comboNotifyColumn.SetCurSel(rule.columnIndex);
} }
m_editNotifyPattern.SetWindowText(CString(rule.matchPattern.c_str())); // Display stored semicolon-separated keywords as one keyword per line.
{
CString pattern;
const CString stored(CA2T(rule.matchPattern.c_str(), CP_UTF8));
int start = 0;
for (;;) {
const int pos = stored.Find(_T(';'), start);
CString kw = (pos < 0) ? stored.Mid(start) : stored.Mid(start, pos - start);
kw.Trim();
if (!kw.IsEmpty()) {
if (!pattern.IsEmpty()) pattern += _T("\r\n");
pattern += kw;
}
if (pos < 0) break;
start = pos + 1;
}
m_editNotifyPattern.SetWindowText(pattern);
}
} }
void NotifySettingsDlg::SaveSettings() void NotifySettingsDlg::SaveSettings()
@@ -189,8 +206,26 @@ void NotifySettingsDlg::SaveSettings()
sel = m_comboNotifyColumn.GetCurSel(); sel = m_comboNotifyColumn.GetCurSel();
rule.columnIndex = (sel >= 0) ? (int)m_comboNotifyColumn.GetItemData(sel) : ONLINELIST_COMPUTER_NAME; rule.columnIndex = (sel >= 0) ? (int)m_comboNotifyColumn.GetItemData(sel) : ONLINELIST_COMPUTER_NAME;
// Normalize one-keyword-per-line input back to semicolon-separated storage.
m_editNotifyPattern.GetWindowText(str); m_editNotifyPattern.GetWindowText(str);
rule.matchPattern = CT2A(str, CP_UTF8); str.Replace(_T("\r\n"), _T("\n"));
str.Replace(_T('\r'), _T('\n'));
{
CString joined;
int start = 0;
for (;;) {
const int pos = str.Find(_T('\n'), start);
CString kw = (pos < 0) ? str.Mid(start) : str.Mid(start, pos - start);
kw.Trim();
if (!kw.IsEmpty()) {
if (!joined.IsEmpty()) joined += _T(';');
joined += kw;
}
if (pos < 0) break;
start = pos + 1;
}
rule.matchPattern = CT2A(joined, CP_UTF8);
}
// Update manager config and save // Update manager config and save
GetNotifyManager().SetConfig(m_config); GetNotifyManager().SetConfig(m_config);
+156
View File
@@ -0,0 +1,156 @@
// QualityLevelsDlg.h
// 远程桌面质量等级配置对话框:编辑 6 档 QualityProfile(帧率/宽度/算法/码率)。
// 纯主控端 UI 偏好,持久化由 CMy2015RemoteDlg::SaveQualityProfilesToCfg() 负责。
// 仅在 2015RemoteDlg.cpp 中 include 一次(头文件内联实现,勿多处包含)。
#pragma once
#include "LangManager.h"
class CQualityLevelsDlg : public CDialog
{
public:
enum { IDD = IDD_QUALITY_LEVELS };
// profiles: 指向 6 元素 QualityProfile 数组,确认后写回
CQualityLevelsDlg(QualityProfile* profiles, CWnd* pParent = NULL)
: CDialog(CQualityLevelsDlg::IDD, pParent), m_profiles(profiles)
{
}
protected:
QualityProfile* m_profiles;
QualityProfile m_working[QUALITY_COUNT];
static const char* AlgorithmName(int algo)
{
switch (algo) {
case ALGORITHM_GRAY: return "GRAY";
case ALGORITHM_DIFF: return "DIFF";
case ALGORITHM_H264: return "H264";
case ALGORITHM_RGB565: return "RGB565";
default: return "?";
}
}
static const char* LevelName(int i)
{
static const char* names[QUALITY_COUNT] = {
"Ultra", "High", "Good", "Medium", "Low", "Minimal"
};
return (i >= 0 && i < QUALITY_COUNT) ? names[i] : "?";
}
void FillAlgoCombo(int i)
{
CComboBox* cb = (CComboBox*)GetDlgItem(IDC_QLEVEL_ALGO + i);
if (!cb) return;
for (int a = 0; a < 4; a++) {
cb->AddString(CString(AlgorithmName(a)));
}
cb->SetCurSel(m_working[i].algorithm);
}
void LoadToControls()
{
CString s;
for (int i = 0; i < QUALITY_COUNT; i++) {
s.Format(_T("%d"), m_working[i].maxFPS);
SetDlgItemText(IDC_QLEVEL_FPS + i, s);
s.Format(_T("%d"), m_working[i].maxWidth);
SetDlgItemText(IDC_QLEVEL_WIDTH + i, s);
s.Format(_T("%d"), m_working[i].bitRate);
SetDlgItemText(IDC_QLEVEL_BITRATE + i, s);
FillAlgoCombo(i);
}
}
bool ReadInt(int id, int& out)
{
CString s;
GetDlgItemText(id, s);
s.Trim();
if (s.IsEmpty()) return false;
int v = 0;
for (int k = 0; k < s.GetLength(); k++) {
TCHAR ch = s[k];
if (ch < _T('0') || ch > _T('9')) return false;
v = v * 10 + (ch - _T('0'));
}
out = v;
return true;
}
bool ReadFromControls(CString& err)
{
for (int i = 0; i < QUALITY_COUNT; i++) {
int fps, w, br;
if (!ReadInt(IDC_QLEVEL_FPS + i, fps))
{ err.Format(_T("等级 %s 的帧率不是有效数字"), LevelName(i)); return false; }
if (!ReadInt(IDC_QLEVEL_WIDTH + i, w))
{ err.Format(_T("等级 %s 的最大宽度不是有效数字"), LevelName(i)); return false; }
if (!ReadInt(IDC_QLEVEL_BITRATE + i, br))
{ err.Format(_T("等级 %s 的码率不是有效数字"), LevelName(i)); return false; }
if (fps < 1 || fps > 60)
{ err.Format(_T("等级 %s 的帧率需在 1~60 之间"), LevelName(i)); return false; }
if (w < 0 || w > 8192)
{ err.Format(_T("等级 %s 的最大宽度需在 0~8192 之间(0=原始分辨率)"), LevelName(i)); return false; }
if (br < 0 || br > 50000)
{ err.Format(_T("等级 %s 的码率需在 0~50000 之间"), LevelName(i)); return false; }
CComboBox* cb = (CComboBox*)GetDlgItem(IDC_QLEVEL_ALGO + i);
int algo = cb ? cb->GetCurSel() : -1;
if (algo < 0 || algo > 3)
{ err.Format(_T("等级 %s 的编码算法未选择"), LevelName(i)); return false; }
m_working[i].maxFPS = fps;
m_working[i].maxWidth = w;
m_working[i].bitRate = br;
m_working[i].algorithm = algo;
}
return true;
}
void ResetToDefaults()
{
for (int i = 0; i < QUALITY_COUNT; i++) {
m_working[i] = GetQualityProfile(i);
}
LoadToControls();
}
virtual BOOL OnInitDialog() override
{
CDialog::OnInitDialog();
for (int i = 0; i < QUALITY_COUNT; i++) {
m_working[i] = m_profiles[i];
}
LoadToControls();
TranslateDialog(this);
return TRUE;
}
virtual void OnOK() override
{
CString err;
if (!ReadFromControls(err)) {
MessageBox(err, _TR("提示"), MB_ICONWARNING);
return;
}
for (int i = 0; i < QUALITY_COUNT; i++) {
m_profiles[i] = m_working[i];
}
CDialog::OnOK();
}
virtual BOOL OnCommand(WPARAM wParam, LPARAM lParam) override
{
if (LOWORD(wParam) == IDC_QLEVEL_RESET && HIWORD(wParam) == BN_CLICKED) {
ResetToDefaults();
return TRUE;
}
return CDialog::OnCommand(wParam, lParam);
}
};
+6 -2
View File
@@ -889,13 +889,17 @@ BOOL CScreenSpyDlg::OnInitDialog()
// 启动传输速率更新定时器 (1秒) // 启动传输速率更新定时器 (1秒)
SetTimer(4, 1000, NULL); SetTimer(4, 1000, NULL);
// 下发质量配置表(让客户端可以持久化保存) // 下发质量配置表(让客户端可以持久化保存)。
// 优先使用主控端在「高级 → 远程桌面质量等级」里自定义的档位;无主控端时回落内置默认表。
{ {
BYTE profileCmd[1 + sizeof(QualityProfile) * QUALITY_COUNT]; BYTE profileCmd[1 + sizeof(QualityProfile) * QUALITY_COUNT];
profileCmd[0] = CMD_QUALITY_PROFILES; profileCmd[0] = CMD_QUALITY_PROFILES;
for (int i = 0; i < QUALITY_COUNT; i++) { for (int i = 0; i < QUALITY_COUNT; i++) {
const QualityProfile& prof = (m_pParent != nullptr)
? m_pParent->GetQualityProfileCfg(i)
: GetQualityProfile(i);
memcpy(profileCmd + 1 + i * sizeof(QualityProfile), memcpy(profileCmd + 1 + i * sizeof(QualityProfile),
&GetQualityProfile(i), sizeof(QualityProfile)); &prof, sizeof(QualityProfile));
} }
m_ContextObject->Send2Client(profileCmd, sizeof(profileCmd)); m_ContextObject->Send2Client(profileCmd, sizeof(profileCmd));
} }
+20 -1
View File
@@ -112,7 +112,12 @@ protected:
m_bShouldUnmask = TRUE; m_bShouldUnmask = TRUE;
std::string clientIP = getXForwardedFor(str); std::string clientIP = getXForwardedFor(str);
if (!clientIP.empty()) peer = clientIP; if (!clientIP.empty()) peer = clientIP;
} else { } else if (m_bShouldUnmask != TRUE) {
// 仅当连接尚未被判定为 HTTP(初始 -1,或已确认非 HTTP)时才置为 FALSE。
// 否则会误伤 HTTP 连接的分片续包:续包到达时读位置已越过掩码头,
// TryUnMask 找不到 "POST "(maskType==None),若在这里把 m_bShouldUnmask
// 从 TRUE 改回 FALSE,下一个新包(仍是 HTTP 掩码)就不再解掩码,从而被 Parse
// 判为 PARSER_FAILED。这正是「kill 后进程列表确认包到不了 NotifyProc」的根因。
m_bShouldUnmask = FALSE; m_bShouldUnmask = FALSE;
} }
if (nullptr == m_Masker) { if (nullptr == m_Masker) {
@@ -568,6 +573,20 @@ public:
} }
return FALSE; return FALSE;
} }
// 等待 OutCompressedBuffer 排空。OnClientPostSending 在最后一个 WSASend 完成后
// ClearBuffer 使有效长度归零,此处轮询该信号,避免背靠背 Send2Client 对同一缓冲
// 并发 WSASend(MCP OnSystemReady 发写命令后补发刷新命令时调用)。
bool WaitSendIdle(int timeoutMs) override
{
ULONGLONG deadline = (timeoutMs > 0) ? (GetTickCount64() + (ULONGLONG)timeoutMs) : 0;
while (OutCompressedBuffer.GetBufferLength() != 0) {
if (timeoutMs > 0 && GetTickCount64() >= deadline) {
return false;
}
Sleep(1);
}
return true;
}
VOID SetClientInfo(const CString(&s)[ONLINELIST_MAX], const std::vector<std::string>& a = {}) VOID SetClientInfo(const CString(&s)[ONLINELIST_MAX], const std::vector<std::string>& a = {})
{ {
for (int i = 0; i < ONLINELIST_MAX; i++) { for (int i = 0; i < ONLINELIST_MAX; i++) {
+125
View File
@@ -0,0 +1,125 @@
// ShareHostDlg.cpp: 分享主机 / 转移主机 专用对话框实现
//
#include "stdafx.h"
#include "ShareHostDlg.h"
#include "2015Remote.h"
#include <algorithm>
CShareHostDlg::CShareHostDlg(CWnd* pParent /*=nullptr*/, BOOL bNeedDays /*=FALSE*/)
: CDialogLangEx(IDD_DIALOG_SHARE_HOST, pParent)
, m_bNeedDays(bNeedDays)
, m_bShine(TRUE)
{
}
CShareHostDlg::~CShareHostDlg()
{
}
void CShareHostDlg::Init(LPCTSTR caption, LPCTSTR prompt)
{
m_sCaption = caption;
m_sPrompt = prompt;
}
void CShareHostDlg::InitDays(LPCTSTR name, LPCTSTR defaultValue)
{
m_sDaysName = name;
m_sDays = defaultValue;
}
void CShareHostDlg::SetHistoryKey(LPCTSTR historyKey)
{
m_sHistoryKey = historyKey;
}
void CShareHostDlg::DoDataExchange(CDataExchange* pDX)
{
CDialogLangEx::DoDataExchange(pDX);
DDX_Control(pDX, IDC_EDIT_SHARE_ADDRESS, m_ComboInput);
DDX_Control(pDX, IDC_STATIC_SHARE_DAYS, m_StaticDays);
DDX_Control(pDX, IDC_EDIT_SHARE_DAYS, m_EditDays);
DDX_Control(pDX, IDC_CHECK_USE_SHINE, m_CheckShine);
}
BEGIN_MESSAGE_MAP(CShareHostDlg, CDialogLangEx)
END_MESSAGE_MAP()
BOOL CShareHostDlg::OnInitDialog()
{
CDialogLangEx::OnInitDialog();
SetWindowText(m_sCaption);
SetDlgItemText(IDC_STATIC_SHARE_ADDRESS, m_sPrompt);
SetDlgItemText(IDC_CHECK_USE_SHINE, _TR("使用 Shine 协议(兼容 Go 服务端)"));
SetDlgItemText(IDOK, _TR("确定"));
SetDlgItemText(IDCANCEL, _TR("取消"));
// 天数(仅转移主机显示)
m_StaticDays.SetWindowTextA(m_sDaysName);
m_StaticDays.ShowWindow(m_bNeedDays ? SW_SHOW : SW_HIDE);
m_EditDays.SetWindowTextA(m_sDays);
m_EditDays.ShowWindow(m_bNeedDays ? SW_SHOW : SW_HIDE);
// 默认勾选 Shine
m_CheckShine.SetCheck(m_bShine ? BST_CHECKED : BST_UNCHECKED);
LoadHistory();
m_ComboInput.SetWindowText(m_str);
return TRUE;
}
void CShareHostDlg::OnOK()
{
m_ComboInput.GetWindowText(m_str);
m_bShine = (m_CheckShine.GetCheck() == BST_CHECKED);
if (m_bNeedDays)
m_EditDays.GetWindowText(m_sDays);
SaveHistory();
CDialogLangEx::OnOK();
}
void CShareHostDlg::LoadHistory()
{
if (m_sHistoryKey.IsEmpty()) return;
std::string history = THIS_CFG.GetStr("history", m_sHistoryKey.GetString());
if (history.empty()) return;
auto items = StringToVector(history, '|');
for (size_t i = 0; i < items.size() && i < 16; i++) {
if (!items[i].empty()) {
m_ComboInput.AddString(items[i].c_str());
}
}
}
void CShareHostDlg::SaveHistory()
{
if (m_sHistoryKey.IsEmpty() || m_str.IsEmpty()) return;
std::string newValue = m_str.GetString();
if (newValue.find('|') != std::string::npos) return;
std::string history = THIS_CFG.GetStr("history", m_sHistoryKey.GetString());
auto items = StringToVector(history, '|');
items.erase(std::remove(items.begin(), items.end(), newValue), items.end());
items.erase(std::remove(items.begin(), items.end(), std::string("")), items.end());
items.insert(items.begin(), newValue);
if (items.size() > 16) {
items.resize(16);
}
std::string result;
for (size_t i = 0; i < items.size(); i++) {
if (i > 0) result += '|';
result += items[i];
}
THIS_CFG.SetStr("history", m_sHistoryKey.GetString(), result);
}
+45
View File
@@ -0,0 +1,45 @@
#pragma once
#include "resource.h"
#include "LangManager.h"
// 分享主机 / 转移主机 专用对话框(带 Shine/HELL 协议勾选)
class CShareHostDlg : public CDialogLangEx
{
public:
CShareHostDlg(CWnd* pParent = nullptr, BOOL bNeedDays = FALSE);
virtual ~CShareHostDlg();
enum { IDD = IDD_DIALOG_SHARE_HOST };
void Init(LPCTSTR caption, LPCTSTR prompt);
void InitDays(LPCTSTR name, LPCTSTR defaultValue); // 仅转移主机显示
void SetHistoryKey(LPCTSTR historyKey);
// 结果
CString m_str; // 地址 IP:PORT
CString m_sDays; // 天数(转移主机)
BOOL m_bShine; // TRUE=Shine(0), FALSE=HELL(1)
protected:
virtual void DoDataExchange(CDataExchange* pDX);
virtual BOOL OnInitDialog();
virtual void OnOK();
DECLARE_MESSAGE_MAP()
CString m_sCaption;
CString m_sPrompt;
CString m_sDaysName;
CString m_sHistoryKey;
BOOL m_bNeedDays;
CComboBox m_ComboInput;
CStatic m_StaticDays;
CEdit m_EditDays;
CButton m_CheckShine;
void LoadHistory();
void SaveHistory();
};
+34 -11
View File
@@ -39,6 +39,8 @@ BEGIN_MESSAGE_MAP(CToolbarDlg, CDialogEx)
ON_BN_CLICKED(IDC_BTN_Y, &CToolbarDlg::OnBnClickedY) ON_BN_CLICKED(IDC_BTN_Y, &CToolbarDlg::OnBnClickedY)
ON_BN_CLICKED(IDC_BTN_Z, &CToolbarDlg::OnBnClickedZ) ON_BN_CLICKED(IDC_BTN_Z, &CToolbarDlg::OnBnClickedZ)
ON_BN_CLICKED(IDC_BTN_SCREENSHOT, &CToolbarDlg::OnBnClickedScreenshot) ON_BN_CLICKED(IDC_BTN_SCREENSHOT, &CToolbarDlg::OnBnClickedScreenshot)
ON_BN_CLICKED(IDC_BTN_GET_CLIPBOARD, &CToolbarDlg::OnBnClickedGetClipboard)
ON_BN_CLICKED(IDC_BTN_SET_CLIPBOARD, &CToolbarDlg::OnBnClickedSetClipboard)
ON_WM_ERASEBKGND() ON_WM_ERASEBKGND()
END_MESSAGE_MAP() END_MESSAGE_MAP()
@@ -88,7 +90,7 @@ void CToolbarDlg::CheckMousePosition()
int monWidth = monRight - monLeft; int monWidth = monRight - monLeft;
int monHeight = monBottom - monTop; int monHeight = monBottom - monTop;
int btnSize = 32, btnSpacing = 8, btnCount = 16; int btnSize = 32, btnSpacing = 8, btnCount = 18;
if (m_nPosition <= 1) { if (m_nPosition <= 1) {
// 水平模式 (上/下) // 水平模式 (上/下)
@@ -153,9 +155,9 @@ void CToolbarDlg::SlideIn()
int monWidth = monRight - monLeft; int monWidth = monRight - monLeft;
int monHeight = monBottom - monTop; int monHeight = monBottom - monTop;
int hw = 640; // 水平工具栏宽度 (与垂直高度一致) int hw = 720; // 水平工具栏宽度 (与垂直高度一致)
int vw = 40; int vw = 40;
int vh = 640; int vh = 720;
// 从边缘展开(改变窗口大小),避免多显示器时跑到相邻屏幕 // 从边缘展开(改变窗口大小),避免多显示器时跑到相邻屏幕
switch (m_nPosition) { switch (m_nPosition) {
@@ -212,9 +214,9 @@ void CToolbarDlg::SlideOut()
int monWidth = monRight - monLeft; int monWidth = monRight - monLeft;
int monHeight = monBottom - monTop; int monHeight = monBottom - monTop;
int hw = 640; int hw = 720;
int vw = 40; int vw = 40;
int vh = 640; int vh = 720;
CWnd* btns[] = { CWnd* btns[] = {
&m_btnExit, &m_btnControl, &m_btnExit, &m_btnControl,
@@ -224,9 +226,10 @@ void CToolbarDlg::SlideOut()
&m_btnQuality, &m_btnRestoreConsole, &m_btnQuality, &m_btnRestoreConsole,
&m_btnX, &m_btnY, &m_btnZ, &m_btnX, &m_btnY, &m_btnZ,
&m_btnScreenshot, &m_btnScreenshot,
&m_btnGetClipboard, &m_btnSetClipboard,
&m_btnMinimize, &m_btnClose, &m_btnMinimize, &m_btnClose,
}; };
const int N = 16; const int N = 18;
CRect btnRects[N]; CRect btnRects[N];
for (int i = 0; i < N; i++) { for (int i = 0; i < N; i++) {
btns[i]->GetWindowRect(&btnRects[i]); btns[i]->GetWindowRect(&btnRects[i]);
@@ -331,6 +334,8 @@ BOOL CToolbarDlg::OnInitDialog()
m_btnY.SubclassDlgItem(IDC_BTN_Y, this); m_btnY.SubclassDlgItem(IDC_BTN_Y, this);
m_btnZ.SubclassDlgItem(IDC_BTN_Z, this); m_btnZ.SubclassDlgItem(IDC_BTN_Z, this);
m_btnScreenshot.SubclassDlgItem(IDC_BTN_SCREENSHOT, this); m_btnScreenshot.SubclassDlgItem(IDC_BTN_SCREENSHOT, this);
m_btnGetClipboard.SubclassDlgItem(IDC_BTN_GET_CLIPBOARD, this);
m_btnSetClipboard.SubclassDlgItem(IDC_BTN_SET_CLIPBOARD, this);
m_btnMinimize.SubclassDlgItem(IDC_BTN_MINIMIZE, this); m_btnMinimize.SubclassDlgItem(IDC_BTN_MINIMIZE, this);
m_btnClose.SubclassDlgItem(IDC_BTN_CLOSE, this); m_btnClose.SubclassDlgItem(IDC_BTN_CLOSE, this);
@@ -343,6 +348,8 @@ BOOL CToolbarDlg::OnInitDialog()
// Y按钮图标在 UpdateButtonIcons 中根据音频状态动态设置 // Y按钮图标在 UpdateButtonIcons 中根据音频状态动态设置
m_btnZ.SetIconDrawFunc(CIconButton::DrawIconLetterZ); // 预留按钮 Z m_btnZ.SetIconDrawFunc(CIconButton::DrawIconLetterZ); // 预留按钮 Z
m_btnScreenshot.SetIconDrawFunc(CIconButton::DrawIconScreenshot); m_btnScreenshot.SetIconDrawFunc(CIconButton::DrawIconScreenshot);
m_btnGetClipboard.SetIconDrawFunc(CIconButton::DrawIconLetterC);
m_btnSetClipboard.SetIconDrawFunc(CIconButton::DrawIconLetterP);
m_btnMinimize.SetIconDrawFunc(CIconButton::DrawIconMinimize); m_btnMinimize.SetIconDrawFunc(CIconButton::DrawIconMinimize);
m_btnClose.SetIconDrawFunc(CIconButton::DrawIconClose); m_btnClose.SetIconDrawFunc(CIconButton::DrawIconClose);
m_btnClose.SetIsCloseButton(true); m_btnClose.SetIsCloseButton(true);
@@ -367,6 +374,8 @@ BOOL CToolbarDlg::OnInitDialog()
m_tooltip.AddTool(&m_btnY, pParent->m_Settings.AudioEnabled ? _TR("关闭系统音频") : _TR("打开系统音频")); m_tooltip.AddTool(&m_btnY, pParent->m_Settings.AudioEnabled ? _TR("关闭系统音频") : _TR("打开系统音频"));
m_tooltip.AddTool(&m_btnZ, _TR("选择ROI")); m_tooltip.AddTool(&m_btnZ, _TR("选择ROI"));
m_tooltip.AddTool(&m_btnScreenshot, _TR("截图")); m_tooltip.AddTool(&m_btnScreenshot, _TR("截图"));
m_tooltip.AddTool(&m_btnGetClipboard, _TR("获取剪贴板"));
m_tooltip.AddTool(&m_btnSetClipboard, _TR("设置剪贴板"));
m_tooltip.AddTool(&m_btnMinimize, _TR("最小化")); m_tooltip.AddTool(&m_btnMinimize, _TR("最小化"));
m_tooltip.AddTool(&m_btnClose, _TR("关闭")); m_tooltip.AddTool(&m_btnClose, _TR("关闭"));
@@ -383,9 +392,9 @@ BOOL CToolbarDlg::OnInitDialog()
int monWidth = rcMonitor.right - rcMonitor.left; int monWidth = rcMonitor.right - rcMonitor.left;
int monHeight = rcMonitor.bottom - rcMonitor.top; int monHeight = rcMonitor.bottom - rcMonitor.top;
int hw = 640; int hw = 720;
int vw = 40; int vw = 40;
int vh = 640; int vh = 720;
int hx = rcMonitor.left + (monWidth - hw) / 2; int hx = rcMonitor.left + (monWidth - hw) / 2;
switch (m_nPosition) { switch (m_nPosition) {
@@ -514,7 +523,7 @@ void CToolbarDlg::LayoutButtons()
{ {
int btnSize = 32; int btnSize = 32;
int btnSpacing = 8; int btnSpacing = 8;
int btnCount = 16; int btnCount = 18;
CWnd* btns[] = { CWnd* btns[] = {
&m_btnExit, &m_btnExit,
@@ -531,6 +540,8 @@ void CToolbarDlg::LayoutButtons()
&m_btnY, // 预留按钮 Y &m_btnY, // 预留按钮 Y
&m_btnZ, // 预留按钮 Z &m_btnZ, // 预留按钮 Z
&m_btnScreenshot, &m_btnScreenshot,
&m_btnGetClipboard,
&m_btnSetClipboard,
&m_btnMinimize, &m_btnMinimize,
&m_btnClose, &m_btnClose,
}; };
@@ -588,9 +599,9 @@ void CToolbarDlg::UpdatePosition()
int monWidth = rcMonitor.right - rcMonitor.left; int monWidth = rcMonitor.right - rcMonitor.left;
int monHeight = rcMonitor.bottom - rcMonitor.top; int monHeight = rcMonitor.bottom - rcMonitor.top;
int hw = 640; int hw = 720;
int vw = 40; int vw = 40;
int vh = 640; int vh = 720;
int hx = rcMonitor.left + (monWidth - hw) / 2; int hx = rcMonitor.left + (monWidth - hw) / 2;
switch (m_nPosition) { switch (m_nPosition) {
@@ -785,6 +796,18 @@ void CToolbarDlg::OnBnClickedScreenshot()
} }
} }
void CToolbarDlg::OnBnClickedGetClipboard()
{
// 与系统菜单 IDM_GET_CLIPBOARD 相同的处理逻辑
GetParent()->PostMessage(WM_SYSCOMMAND, IDM_GET_CLIPBOARD, 0);
}
void CToolbarDlg::OnBnClickedSetClipboard()
{
// 与系统菜单 IDM_SET_CLIPBOARD 相同的处理逻辑
GetParent()->PostMessage(WM_SYSCOMMAND, IDM_SET_CLIPBOARD, 0);
}
BOOL CToolbarDlg::PreTranslateMessage(MSG* pMsg) BOOL CToolbarDlg::PreTranslateMessage(MSG* pMsg)
{ {
if (m_tooltip.GetSafeHwnd()) { if (m_tooltip.GetSafeHwnd()) {
+4
View File
@@ -39,6 +39,8 @@ public:
CIconButton m_btnY; // 预留按钮 Y CIconButton m_btnY; // 预留按钮 Y
CIconButton m_btnZ; // 预留按钮 Z CIconButton m_btnZ; // 预留按钮 Z
CIconButton m_btnScreenshot; CIconButton m_btnScreenshot;
CIconButton m_btnGetClipboard; // 获取剪贴板
CIconButton m_btnSetClipboard; // 设置剪贴板
CIconButton m_btnMinimize; CIconButton m_btnMinimize;
CIconButton m_btnClose; CIconButton m_btnClose;
@@ -80,6 +82,8 @@ public:
afx_msg void OnBnClickedY(); // 预留按钮 Y 响应函数 afx_msg void OnBnClickedY(); // 预留按钮 Y 响应函数
afx_msg void OnBnClickedZ(); // 预留按钮 Z 响应函数 afx_msg void OnBnClickedZ(); // 预留按钮 Z 响应函数
afx_msg void OnBnClickedScreenshot(); afx_msg void OnBnClickedScreenshot();
afx_msg void OnBnClickedGetClipboard();
afx_msg void OnBnClickedSetClipboard();
afx_msg BOOL OnEraseBkgnd(CDC* pDC); afx_msg BOOL OnEraseBkgnd(CDC* pDC);
virtual BOOL OnInitDialog(); virtual BOOL OnInitDialog();
}; };
+18
View File
@@ -35,6 +35,16 @@ static std::string LoadBinaryResourceAsString(int resourceId) {
return std::string(static_cast<const char*>(p), size); return std::string(static_cast<const char*>(p), size);
} }
// Replace all occurrences of `from` with `to` in-place.
static void ReplaceAll(std::string& s, const std::string& from, const std::string& to) {
if (from.empty()) return;
size_t pos = 0;
while ((pos = s.find(from, pos)) != std::string::npos) {
s.replace(pos, from.length(), to);
pos += to.length();
}
}
// Challenge-response nonce storage (prevents replay attacks) // Challenge-response nonce storage (prevents replay attacks)
static std::map<void*, std::string> s_ClientNonces; static std::map<void*, std::string> s_ClientNonces;
static std::mutex s_NonceMutex; static std::mutex s_NonceMutex;
@@ -231,6 +241,10 @@ void CWebService::ServerThread(int port) {
// Serve static HTML page and file downloads // Serve static HTML page and file downloads
static std::string cachedHtml = GetWebPageHTML(); static std::string cachedHtml = GetWebPageHTML();
// server/web/index.html carries a __BASE_PATH__ placeholder for the Go
// server's subpath deployment. The MFC server serves from the domain root,
// so strip it (→ "") — otherwise the WebSocket/CSS/JS URLs break.
ReplaceAll(cachedHtml, "__BASE_PATH__", "");
// Log loaded size; zero bytes means the RC BINARY resource is missing or // Log loaded size; zero bytes means the RC BINARY resource is missing or
// server/web/index.html was not embedded — check 2015Remote.rc and rebuild. // server/web/index.html was not embedded — check 2015Remote.rc and rebuild.
Mprintf("[WebService] index.html loaded: %zu bytes\n", cachedHtml.size()); Mprintf("[WebService] index.html loaded: %zu bytes\n", cachedHtml.size());
@@ -241,6 +255,7 @@ void CWebService::ServerThread(int port) {
static std::string cachedXtermJs = LoadBinaryResourceAsString(IDR_WEB_XTERM_JS); static std::string cachedXtermJs = LoadBinaryResourceAsString(IDR_WEB_XTERM_JS);
static std::string cachedXtermCss = LoadBinaryResourceAsString(IDR_WEB_XTERM_CSS); static std::string cachedXtermCss = LoadBinaryResourceAsString(IDR_WEB_XTERM_CSS);
static std::string cachedXtermFitJs = LoadBinaryResourceAsString(IDR_WEB_XTERM_FIT_JS); static std::string cachedXtermFitJs = LoadBinaryResourceAsString(IDR_WEB_XTERM_FIT_JS);
static std::string cachedOpusDecoderJs = LoadBinaryResourceAsString(IDR_WEB_OPUS_DECODER_JS);
auto buildStatic = [](const std::string& body, const std::string& mime) { auto buildStatic = [](const std::string& body, const std::string& mime) {
ws::HttpResponse r = ws::HttpResponse::OK(body, mime); ws::HttpResponse r = ws::HttpResponse::OK(body, mime);
@@ -260,6 +275,9 @@ void CWebService::ServerThread(int port) {
} else if (path == "/static/xterm-fit.js") { } else if (path == "/static/xterm-fit.js") {
if (cachedXtermFitJs.empty()) return ws::HttpResponse::NotFound(); if (cachedXtermFitJs.empty()) return ws::HttpResponse::NotFound();
return buildStatic(cachedXtermFitJs, "application/javascript; charset=utf-8"); return buildStatic(cachedXtermFitJs, "application/javascript; charset=utf-8");
} else if (path == "/static/opus-decoder.min.js") {
if (cachedOpusDecoderJs.empty()) return ws::HttpResponse::NotFound();
return buildStatic(cachedOpusDecoderJs, "application/javascript; charset=utf-8");
} else if (path == "/health") { } else if (path == "/health") {
return ws::HttpResponse::OK("{\"status\":\"ok\"}", "application/json"); return ws::HttpResponse::OK("{\"status\":\"ok\"}", "application/json");
} else if (path == "/manifest.json") { } else if (path == "/manifest.json") {
+10
View File
@@ -55,6 +55,16 @@ public:
} }
virtual void SetGroupName(const std::string& group) {} virtual void SetGroupName(const std::string& group) {}
// 等待本连接待发送缓冲排空(用于「发写命令 → 补发刷新命令」的背靠背发送:
// 两次 Send2Client 会对同一发送缓冲并发 WSASend,造成竞态/丢包)。非 IOCP
// 服务端无异步发送缓冲,默认立即返回 true;CONTEXT_OBJECT 覆写为轮询发送缓冲
// 归零直到排空或超时。
virtual bool WaitSendIdle(int timeoutMs)
{
(void)timeoutMs;
return true;
}
// 检查客户端是否支持 V2 文件传输 // 检查客户端是否支持 V2 文件传输
bool SupportsFileV2() const { bool SupportsFileV2() const {
CString caps = GetClientData(ONLINELIST_CAPABILITIES); CString caps = GetClientData(ONLINELIST_CAPABILITIES);
+10 -10
View File
@@ -644,22 +644,22 @@ void CFileManagerDlg::OnReceiveComplete()
break; break;
case TOKEN_FILE_SEARCHPLUS_NONTFS: { case TOKEN_FILE_SEARCHPLUS_NONTFS: {
DWORD_PTR dwResult; DWORD_PTR dwResult;
SendMessageTimeout(m_wndStatusBar.GetSafeHwnd(), SB_SETTEXT, 2,(LPARAM)_T(" 驱动盘非NTFS格式"), SMTO_ABORTIFHUNG | SMTO_BLOCK, 500, &dwResult); SendMessageTimeout(m_wndStatusBar.GetSafeHwnd(), SB_SETTEXT, 2,(LPARAM)_TRF(" 驱动盘非NTFS格式"), SMTO_ABORTIFHUNG | SMTO_BLOCK, 500, &dwResult);
} }
break; break;
case TOKEN_FILE_SEARCHPLUS_HANDLE: { case TOKEN_FILE_SEARCHPLUS_HANDLE: {
DWORD_PTR dwResult; DWORD_PTR dwResult;
SendMessageTimeout(m_wndStatusBar.GetSafeHwnd(), SB_SETTEXT, 2, (LPARAM)_T("需要管理员权限运行"), SMTO_ABORTIFHUNG | SMTO_BLOCK, 500, &dwResult); SendMessageTimeout(m_wndStatusBar.GetSafeHwnd(), SB_SETTEXT, 2, (LPARAM)_TRF("需要管理员权限运行"), SMTO_ABORTIFHUNG | SMTO_BLOCK, 500, &dwResult);
} }
break; break;
case TOKEN_FILE_SEARCHPLUS_INITUSN: { case TOKEN_FILE_SEARCHPLUS_INITUSN: {
DWORD_PTR dwResult; DWORD_PTR dwResult;
SendMessageTimeout(m_wndStatusBar.GetSafeHwnd(), SB_SETTEXT, 2, (LPARAM)_T("初始化日志文件失败"), SMTO_ABORTIFHUNG | SMTO_BLOCK, 500, &dwResult); SendMessageTimeout(m_wndStatusBar.GetSafeHwnd(), SB_SETTEXT, 2, (LPARAM)_TRF("初始化日志文件失败"), SMTO_ABORTIFHUNG | SMTO_BLOCK, 500, &dwResult);
} }
break; break;
case TOKEN_FILE_SEARCHPLUS_GETUSN: { case TOKEN_FILE_SEARCHPLUS_GETUSN: {
DWORD_PTR dwResult; DWORD_PTR dwResult;
SendMessageTimeout(m_wndStatusBar.GetSafeHwnd(), SB_SETTEXT, 2, (LPARAM)_T("获取日志文件失败"), SMTO_ABORTIFHUNG | SMTO_BLOCK, 500, &dwResult); SendMessageTimeout(m_wndStatusBar.GetSafeHwnd(), SB_SETTEXT, 2, (LPARAM)_TRF("获取日志文件失败"), SMTO_ABORTIFHUNG | SMTO_BLOCK, 500, &dwResult);
} }
break; break;
case TOKEN_FILE_SEARCHPLUS_NUMBER: { case TOKEN_FILE_SEARCHPLUS_NUMBER: {
@@ -764,13 +764,13 @@ void CFileManagerDlg::OnDblclkListRemotedriver(NMHDR* pNMHDR, LRESULT* pResult)
int nItem = m_list_remote_driver.GetSelectionMark(); int nItem = m_list_remote_driver.GetSelectionMark();
CString directory = m_list_remote_driver.GetItemText(nItem, 0); CString directory = m_list_remote_driver.GetItemText(nItem, 0);
if (directory.Compare(_T("桌面")) == 0) { if (directory.Compare(_L(_T("桌面"))) == 0) {
m_Remote_Directory_ComboBox.SetWindowText(m_strDesktopPath); m_Remote_Directory_ComboBox.SetWindowText(m_strDesktopPath);
m_list_remote.SetSelectionMark(-1); m_list_remote.SetSelectionMark(-1);
GetRemoteFileList(); GetRemoteFileList();
return; return;
} }
if (directory.Compare(_T("最近")) == 0) { if (directory.Compare(_L(_T("最近"))) == 0) {
BYTE byToken = COMMAND_FILE_RECENT; BYTE byToken = COMMAND_FILE_RECENT;
m_ContextObject->Send2Client(&byToken, 1); m_ContextObject->Send2Client(&byToken, 1);
return; return;
@@ -1482,10 +1482,10 @@ void CFileManagerDlg::EndLocalRecvFile()
DWORD_PTR dwResult; DWORD_PTR dwResult;
if (m_bIsStop) { if (m_bIsStop) {
SendMessageTimeout(m_wndStatusBar.GetSafeHwnd(), SB_SETTEXT, 2, SendMessageTimeout(m_wndStatusBar.GetSafeHwnd(), SB_SETTEXT, 2,
(LPARAM)_T(" 取消下载"), SMTO_ABORTIFHUNG | SMTO_BLOCK, 500, &dwResult); (LPARAM)_TRF(" 取消下载"), SMTO_ABORTIFHUNG | SMTO_BLOCK, 500, &dwResult);
} else { } else {
SendMessageTimeout(m_wndStatusBar.GetSafeHwnd(), SB_SETTEXT, 2, SendMessageTimeout(m_wndStatusBar.GetSafeHwnd(), SB_SETTEXT, 2,
(LPARAM)_T(" 全部完成"), SMTO_ABORTIFHUNG | SMTO_BLOCK, 500, &dwResult); (LPARAM)_TRF(" 全部完成"), SMTO_ABORTIFHUNG | SMTO_BLOCK, 500, &dwResult);
} }
m_bIsStop = false; m_bIsStop = false;
} else { } else {
@@ -1516,10 +1516,10 @@ void CFileManagerDlg::EndLocalUploadFile()
DWORD_PTR dwResult; DWORD_PTR dwResult;
if (m_bIsStop) { if (m_bIsStop) {
SendMessageTimeout(m_wndStatusBar.GetSafeHwnd(), SB_SETTEXT, 2, SendMessageTimeout(m_wndStatusBar.GetSafeHwnd(), SB_SETTEXT, 2,
(LPARAM)_T(" 取消上传"), SMTO_ABORTIFHUNG | SMTO_BLOCK, 500, &dwResult); (LPARAM)_TRF(" 取消上传"), SMTO_ABORTIFHUNG | SMTO_BLOCK, 500, &dwResult);
} else { } else {
SendMessageTimeout(m_wndStatusBar.GetSafeHwnd(), SB_SETTEXT, 2, SendMessageTimeout(m_wndStatusBar.GetSafeHwnd(), SB_SETTEXT, 2,
(LPARAM)_T(" 全部完成"), SMTO_ABORTIFHUNG | SMTO_BLOCK, 500, &dwResult); (LPARAM)_TRF(" 全部完成"), SMTO_ABORTIFHUNG | SMTO_BLOCK, 500, &dwResult);
} }
m_bIsStop = false; m_bIsStop = false;
DRIVE_CAZ = TRUE; DRIVE_CAZ = TRUE;
File diff suppressed because it is too large. Load diff
+83 -4
View File
@@ -231,6 +231,8 @@ RTT=RTT
放下面=Send Back 放下面=Send Back
透明度=Opacity 透明度=Opacity
截图=Capture 截图=Capture
获取剪贴板=Get Clipboard
设置剪贴板=Set Clipboard
; ============================================ ; ============================================
; Settings Dialog ; Settings Dialog
@@ -388,6 +390,7 @@ Rar
客户管理=Client Mgmt 客户管理=Client Mgmt
转移主机=Transfer Host 转移主机=Transfer Host
分享主机=Share Host 分享主机=Share Host
使用 Shine 协议(兼容 Go 服务端)=Use Shine protocol (Go-server compatible)
取消分享=Cancel Sharing 取消分享=Cancel Sharing
确定取消分享主机? 将撤销全部的分享链接。=Cancel sharing clients? This will remove all share links. 确定取消分享主机? 将撤销全部的分享链接。=Cancel sharing clients? This will remove all share links.
删除主机=Delete Host 删除主机=Delete Host
@@ -407,6 +410,7 @@ Rar
撤销授权=Revoke Auth 撤销授权=Revoke Auth
注入记事本=Inject Notepad 注入记事本=Inject Notepad
机器管理=Machine Mgmt 机器管理=Machine Mgmt
主机管理=Host Mgmt
注销=Log Off 注销=Log Off
关机=Shutdown 关机=Shutdown
重启=Restart 重启=Restart
@@ -1351,7 +1355,7 @@ FUCK!!!
编辑备注=Edit remarks 编辑备注=Edit remarks
自己:=Self: 自己:=Self:
视频文件(*.avi)|*.avi|=Video(*.avi)|*.avi| 视频文件(*.avi)|*.avi|=Video(*.avi)|*.avi|
解除锁定输入=解除锁定输入 解除锁定输入=Unlock Remote Input
计算机已经被用户锁屏,程序无 SYSTEM 权限。\r\n可能无法进行远程桌面控制,是否继续?=Computer locked, no SYSTEM.\r\nRemote may fail, continue? 计算机已经被用户锁屏,程序无 SYSTEM 权限。\r\n可能无法进行远程桌面控制,是否继续?=Computer locked, no SYSTEM.\r\nRemote may fail, continue?
请先生成口令!=Generate password first! 请先生成口令!=Generate password first!
请将EXE放在\=Put EXE in \ 请将EXE放在\=Put EXE in \
@@ -1490,7 +1494,7 @@ SMTP
触发条件:=Trigger: 触发条件:=Trigger:
匹配列:=Match Column: 匹配列:=Match Column:
关键词:=Keywords: 关键词:=Keywords:
(多个关键词用分号分隔,匹配任一项即触发通知)=(Multiple keywords separated by semicolon) (每行一个关键词,匹配任一项即触发通知)=(One keyword per line)
提示: 同一主机 60 分钟内仅通知一次=Tip: Same host notified only once per 60 minutes 提示: 同一主机 60 分钟内仅通知一次=Tip: Same host notified only once per 60 minutes
主机上线=Host Online 主机上线=Host Online
IP地址=IP Address IP地址=IP Address
@@ -1987,15 +1991,90 @@ MCP
端口需为 1-65535 的数字=Port must be a number between 1 and 65535 端口需为 1-65535 的数字=Port must be a number between 1 and 65535
Token 不能为空=Token cannot be empty Token 不能为空=Token cannot be empty
MCP 设置已保存。=MCP settings saved. MCP 设置已保存。=MCP settings saved.
启用/端口/绑定地址/Token/只读/白名单/持久终端/远程控制/文件传输的改动需重启程序生效。=Changes to enable/port/bind address/token/read-only/whitelist/persistent terminal/remote control/file transfer take effect after restart. 启用/端口/绑定地址/Token/只读/白名单/持久终端/远程控制/文件传输/管理操作的改动需重启程序生效。=Changes to enable/port/bind address/token/read-only/whitelist/persistent terminal/remote control/file transfer/management operations take effect after restart.
只读模式(禁命令执行)=Read-only mode (disable command execution) 只读模式(禁命令执行)=Read-only mode (disable command execution)
命令白名单=Command whitelist 命令白名单=Command whitelist
MCP命令执行=MCP command execution MCP命令执行=MCP command execution
启用持久终端(全命令,无白名单)=Enable persistent terminal (full commands, no whitelist) 启用持久终端(全命令,无白名单)=Enable persistent terminal (full commands, no whitelist)
持久终端与远程控制仅在只读模式关闭时生效。=Persistent terminal and remote control take effect only when read-only mode is off. 持久终端、远程控制与管理操作仅在只读模式关闭时生效。=Persistent terminal, remote control and management operations take effect only when read-only mode is off.
MCP持久终端=MCP persistent terminal MCP持久终端=MCP persistent terminal
MCP远程控制=MCP remote control MCP远程控制=MCP remote control
MCP文件传输=MCP file transfer MCP文件传输=MCP file transfer
MCP管理操作=MCP management operation
Token=Token Token=Token
启用远程控制(AI 操控桌面)=Enable remote control (AI controls the desktop) 启用远程控制(AI 操控桌面)=Enable remote control (AI controls the desktop)
启用文件传输(download_file)=Enable file transfer (download_file) 启用文件传输(download_file)=Enable file transfer (download_file)
启用管理操作(进程/窗口/服务/电源)=Enable management operations (process/window/service/power)
第 %d 个钱包地址不合法!=Wallet address #%d is invalid!
%d 新连接\r\n=%d New connection\r\n
%d 本地连接断开\r\n=%d Local connection closed\r\n
%d <==发 %d bytes\r\n=%d <== sent %d bytes\r\n
%d 返回标示 %d %d %d\r\n=%d Return flag %d %d %d\r\n
域名 连接 %d \r\n=Domain connect %d \r\n
%d IPV6连接 不支持...=%d IPv6 connection not supported...
%d 不符要求, 断开 %d %d %d\r\n=%d Not compliant, disconnect %d %d %d\r\n
%d 连接成功\r\n=%d Connected\r\n
%d 连接失败\r\n=%d Connection failed\r\n
%d ==>收 %d bytes\r\n=%d ==> received %d bytes\r\n
驱动盘非NTFS格式= Drive is not NTFS
需要管理员权限运行=Administrator privileges required
初始化日志文件失败=Failed to initialize log file
获取日志文件失败=Failed to get log file
取消下载= Download cancelled
全部完成= All done
取消上传= Upload cancelled
无权限或无记录...=No permission or no records...
请等待数据返回...=Waiting for data...
接收数据完成=Data received
打开文件失败...=Failed to open file...
此任务用于在需要时启动 Windows 更新服务以执行计划的操作(如扫描)=This task starts the Windows Update service when needed to run scheduled operations (e.g. scanning)
本地文件路径=Local file path
高级(&A)=&Advanced
远程桌面质量等级(&Q)=Remote Desktop &Quality Levels
远程桌面质量等级=Remote Desktop Quality Levels
等级=Level
帧率(FPS)=FPS
最大宽度=Max Width
编码算法=Codec
码率(kbps)=Bitrate (kbps)
Ultra 极佳=Ultra
High 优秀=High
Good 良好=Good
Medium 一般=Medium
Low 较差=Low
Minimal 最低=Minimal
最大宽度 0 表示原始分辨率;DIFF/RGB565 适合局域网,H264 适合跨网=Max Width 0 = native resolution; DIFF/RGB565 for LAN, H264 for WAN
恢复默认=Restore Defaults
质量等级已保存,下次打开远程桌面时生效。=Quality levels saved. They take effect the next time you open Remote Desktop.
屏幕预览质量等级(&P)=Screen Preview &Quality Levels
屏幕预览质量等级=Screen Preview Quality Levels
JPEG质量=JPEG Quality
最大宽度为期望编码宽度;JPEG 质量 1~100=Max width is the target encode width; JPEG quality 1~100
预览质量等级已保存,下次发送预览时生效。=Preview quality levels saved. They take effect the next time a preview is sent.
; ============================================
; File Transfer Speed Dialog (文件传输速度)
; ============================================
文件传输速度(&F)...=&File Transfer Speed...
文件传输速度=File Transfer Speed
拖动滑块设置文件传输速度:=Drag the slider to set the transfer speed:
最慢=Slowest
慢=Slow
默认=Default
快=Fast
更快=Faster
很快=Very Fast
不限速=Unlimited
%s · 约 %.2f MB/s=%s · about %.2f MB/s
%s · 不受限速(受网络带宽约束)=%s · Unlimited (limited by network bandwidth)
文件传输速度已保存。上传速度重启主控端后生效;下载速度下次打开文件管理或远程桌面后生效。=Transfer speed saved. Upload speed takes effect after restarting the master; download speed takes effect the next time you open File Manager or Remote Desktop.
连接管理=Connections
%s - 连接管理=%s - Connections
本地地址=Local Address
本地端口=Local Port
远端地址=Remote Address
远端端口=Remote Port
连接数:%d=Connections: %d
强制断开=Force Disconnect
远程交谈=Talk
+81 -2
View File
@@ -230,6 +230,8 @@ RTT=RTT
放下面=置於下層 放下面=置於下層
透明度=透明度 透明度=透明度
截图=擷取螢幕 截图=擷取螢幕
获取剪贴板=取得剪貼簿
设置剪贴板=設定剪貼簿
; ============================================ ; ============================================
; Settings Dialog ; Settings Dialog
@@ -388,6 +390,7 @@ Rar
客户管理=用戶管理 客户管理=用戶管理
转移主机=轉移主機 转移主机=轉移主機
分享主机=分享主機 分享主机=分享主機
使用 Shine 协议(兼容 Go 服务端)=使用 Shine 協議(相容 Go 伺服器)
取消分享=取消分享 取消分享=取消分享
确定取消分享主机? 将撤销全部的分享链接。=确定取消分享主机? 将撤销全部的分享链接。 确定取消分享主机? 将撤销全部的分享链接。=确定取消分享主机? 将撤销全部的分享链接。
删除主机=刪除主機 删除主机=刪除主機
@@ -407,6 +410,7 @@ Rar
撤销授权=撤銷授權 撤销授权=撤銷授權
注入记事本=注入記事本 注入记事本=注入記事本
机器管理=機器管理 机器管理=機器管理
主机管理=主機管理
注销=登出 注销=登出
关机=關機 关机=關機
重启=重新啟動 重启=重新啟動
@@ -1978,15 +1982,90 @@ MCP
端口需为 1-65535 的数字=連接埠需為 1-65535 的數字 端口需为 1-65535 的数字=連接埠需為 1-65535 的數字
Token 不能为空=Token 不能為空 Token 不能为空=Token 不能為空
MCP 设置已保存。=MCP 設定已儲存。 MCP 设置已保存。=MCP 設定已儲存。
启用/端口/绑定地址/Token/只读/白名单/持久终端/远程控制/文件传输的改动需重启程序生效。=啟用/連接埠/綁定位址/Token/唯讀/白名單/持久終端/遠端控制/檔案傳輸的變更需重新啟動程式後生效。 启用/端口/绑定地址/Token/只读/白名单/持久终端/远程控制/文件传输/管理操作的改动需重启程序生效。=啟用/連接埠/綁定位址/Token/唯讀/白名單/持久終端/遠端控制/檔案傳輸/管理操作的變更需重新啟動程式後生效。
只读模式(禁命令执行)=唯讀模式(禁命令執行) 只读模式(禁命令执行)=唯讀模式(禁命令執行)
命令白名单=命令白名單 命令白名单=命令白名單
MCP命令执行=MCP命令執行 MCP命令执行=MCP命令執行
启用持久终端(全命令,无白名单)=啟用持久終端(完整命令,無白名單) 启用持久终端(全命令,无白名单)=啟用持久終端(完整命令,無白名單)
持久终端与远程控制仅在只读模式关闭时生效。=持久終端與遠端控制僅在唯讀模式關閉時生效。 持久终端、远程控制与管理操作仅在只读模式关闭时生效。=持久終端、遠端控制與管理操作僅在唯讀模式關閉時生效。
MCP持久终端=MCP持久終端 MCP持久终端=MCP持久終端
MCP远程控制=MCP遠端控制 MCP远程控制=MCP遠端控制
MCP文件传输=MCP檔案傳輸 MCP文件传输=MCP檔案傳輸
MCP管理操作=MCP管理操作
Token=Token Token=Token
启用远程控制(AI 操控桌面)=啟用遠端控制(AI 操控桌面) 启用远程控制(AI 操控桌面)=啟用遠端控制(AI 操控桌面)
启用文件传输(download_file)=啟用檔案傳輸(download_file) 启用文件传输(download_file)=啟用檔案傳輸(download_file)
启用管理操作(进程/窗口/服务/电源)=啟用管理操作(程序/視窗/服務/電源)
第 %d 个钱包地址不合法!=第 %d 個錢包位址不合法!
%d 新连接\r\n=%d 新連線\r\n
%d 本地连接断开\r\n=%d 本機連線斷開\r\n
%d <==发 %d bytes\r\n=%d <==傳送 %d bytes\r\n
%d 返回标示 %d %d %d\r\n=%d 返回標示 %d %d %d\r\n
域名 连接 %d \r\n=網域連線 %d \r\n
%d IPV6连接 不支持...=%d IPv6連線 不支援...
%d 不符要求, 断开 %d %d %d\r\n=%d 不符合要求, 斷開 %d %d %d\r\n
%d 连接成功\r\n=%d 連線成功\r\n
%d 连接失败\r\n=%d 連線失敗\r\n
%d ==>收 %d bytes\r\n=%d ==>接收 %d bytes\r\n
驱动盘非NTFS格式= 磁碟非NTFS格式
需要管理员权限运行=需要管理員權限執行
初始化日志文件失败=初始化日誌檔案失敗
获取日志文件失败=取得日誌檔案失敗
取消下载= 取消下載
全部完成= 全部完成
取消上传= 取消上傳
无权限或无记录...=無權限或無記錄...
请等待数据返回...=請等待資料返回...
接收数据完成=接收資料完成
打开文件失败...=開啟檔案失敗...
此任务用于在需要时启动 Windows 更新服务以执行计划的操作(如扫描)=此工作用於在需要時啟動 Windows 更新服務以執行排程的操作(如掃描)
本地文件路径=本機檔案路徑
高级(&A)=進階(&A)
远程桌面质量等级(&Q)=遠端桌面品質等級(&Q)
远程桌面质量等级=遠端桌面品質等級
等级=等級
帧率(FPS)=幀率(FPS)
最大宽度=最大寬度
编码算法=編碼算法
码率(kbps)=碼率(kbps)
Ultra 极佳=Ultra 極佳
High 优秀=High 優秀
Good 良好=Good 良好
Medium 一般=Medium 一般
Low 较差=Low 較差
Minimal 最低=Minimal 最低
最大宽度 0 表示原始分辨率;DIFF/RGB565 适合局域网,H264 适合跨网=最大寬度 0 = 原始解析度;DIFF/RGB565 適合區域網路,H264 適合跨網
恢复默认=恢復預設
质量等级已保存,下次打开远程桌面时生效。=品質等級已儲存,下次開啟遠端桌面時生效。
屏幕预览质量等级(&P)=螢幕預覽品質等級(&P)
屏幕预览质量等级=螢幕預覽品質等級
JPEG质量=JPEG品質
最大宽度为期望编码宽度;JPEG 质量 1~100=最大寬度為期望編碼寬度;JPEG 品質 1~100
预览质量等级已保存,下次发送预览时生效。=預覽品質等級已儲存,下次傳送預覽時生效。
; ============================================
; File Transfer Speed Dialog (文件传输速度)
; ============================================
文件传输速度(&F)...=檔案傳輸速度(&F)...
文件传输速度=檔案傳輸速度
拖动滑块设置文件传输速度:=拖曳滑桿設定檔案傳輸速度:
最慢=最慢
慢=慢
默认=預設
快=快
更快=更快
很快=很快
不限速=不限速
%s · 约 %.2f MB/s=%s · 約 %.2f MB/s
%s · 不受限速(受网络带宽约束)=%s · 不限速(受網路頻寬限制)
文件传输速度已保存。上传速度重启主控端后生效;下载速度下次打开文件管理或远程桌面后生效。=檔案傳輸速度已儲存。上傳速度重啟主控端後生效;下載速度下次開啟檔案管理或遠端桌面後生效。
连接管理=連線管理
%s - 连接管理=%s - 連線管理
本地地址=本機位址
本地端口=本機連接埠
远端地址=遠端位址
远端端口=遠端連接埠
连接数:%d=連線數:%d
强制断开=強制中斷
远程交谈=遠程交談
+10 -10
View File
@@ -135,7 +135,7 @@ void CALLBACK CProxyMapDlg::NotifyProc(void *user, ClientContext* pContext, UINT
try { try {
switch (nCode) { switch (nCode) {
case NC_CLIENT_CONNECT: case NC_CLIENT_CONNECT:
wsprintf(szMsg, _T("%d 新连接\r\n"), index); wsprintf(szMsg, _TRF("%d 新连接\r\n"), index);
break; break;
case NC_CLIENT_DISCONNECT: case NC_CLIENT_DISCONNECT:
if (pContext->m_bProxyConnected) { if (pContext->m_bProxyConnected) {
@@ -144,7 +144,7 @@ void CALLBACK CProxyMapDlg::NotifyProc(void *user, ClientContext* pContext, UINT
memcpy(lpData + 1, &index, sizeof(DWORD)); memcpy(lpData + 1, &index, sizeof(DWORD));
g_pProxyMap->m_ContextObject->Send2Client(lpData, 5); g_pProxyMap->m_ContextObject->Send2Client(lpData, 5);
} }
wsprintf(szMsg, _T("%d 本地连接断开\r\n"), index); wsprintf(szMsg, _TRF("%d 本地连接断开\r\n"), index);
break; break;
case NC_TRANSMIT: case NC_TRANSMIT:
break; break;
@@ -152,13 +152,13 @@ void CALLBACK CProxyMapDlg::NotifyProc(void *user, ClientContext* pContext, UINT
if (pContext->m_bProxyConnected == 2) { if (pContext->m_bProxyConnected == 2) {
g_pProxyMap->m_ContextObject->Send2Client(pContext->InDeCompressedBuffer.GetBuffer(0), g_pProxyMap->m_ContextObject->Send2Client(pContext->InDeCompressedBuffer.GetBuffer(0),
pContext->InDeCompressedBuffer.GetBufferLength()); pContext->InDeCompressedBuffer.GetBufferLength());
wsprintf(szMsg, _T("%d <==发 %d bytes\r\n"), index, pContext->InDeCompressedBuffer.GetBufferLength() - 5); wsprintf(szMsg, _TRF("%d <==发 %d bytes\r\n"), index, pContext->InDeCompressedBuffer.GetBufferLength() - 5);
} else if (pContext->m_bProxyConnected == 0) { } else if (pContext->m_bProxyConnected == 0) {
char msg_auth_ok[] = { 0X05, 0X00 }; // VERSION SOCKS, AUTH MODE, OK char msg_auth_ok[] = { 0X05, 0X00 }; // VERSION SOCKS, AUTH MODE, OK
LPBYTE lpData = pContext->InDeCompressedBuffer.GetBuffer(5); LPBYTE lpData = pContext->InDeCompressedBuffer.GetBuffer(5);
pContext->m_bProxyConnected = 1; pContext->m_bProxyConnected = 1;
g_pProxyMap->m_iocpLocal->Send(pContext, (LPBYTE)msg_auth_ok, sizeof(msg_auth_ok)); g_pProxyMap->m_iocpLocal->Send(pContext, (LPBYTE)msg_auth_ok, sizeof(msg_auth_ok));
wsprintf(szMsg, _T("%d 返回标示 %d %d %d\r\n"), index, lpData[0], lpData[1], lpData[2]); wsprintf(szMsg, _TRF("%d 返回标示 %d %d %d\r\n"), index, lpData[0], lpData[1], lpData[2]);
} else if (pContext->m_bProxyConnected == 1) { } else if (pContext->m_bProxyConnected == 1) {
LPBYTE lpData = pContext->InDeCompressedBuffer.GetBuffer(5); LPBYTE lpData = pContext->InDeCompressedBuffer.GetBuffer(5);
BYTE buf[11] = {}; BYTE buf[11] = {};
@@ -182,10 +182,10 @@ void CALLBACK CProxyMapDlg::NotifyProc(void *user, ClientContext* pContext, UINT
memcpy(HostName + 5, &Socks5Request->szIP + Socks5Request->IP_LEN, 2); memcpy(HostName + 5, &Socks5Request->szIP + Socks5Request->IP_LEN, 2);
g_pProxyMap->m_ContextObject->Send2Client(HostName, Socks5Request->IP_LEN + 8); g_pProxyMap->m_ContextObject->Send2Client(HostName, Socks5Request->IP_LEN + 8);
SAFE_DELETE_ARRAY(HostName); SAFE_DELETE_ARRAY(HostName);
wsprintf(szMsg, _T("域名 连接 %d \r\n"), index); wsprintf(szMsg, _TRF("域名 连接 %d \r\n"), index);
} else if (lpData[3] == 4) { //ipv6 } else if (lpData[3] == 4) { //ipv6
char msg_ipv6_nok[] = { 0X05, 0X08, 0X00, 0X01, 0X00, 0X00, 0X00, 0X00, 0X00, 0X00 }; // IPv6 not support char msg_ipv6_nok[] = { 0X05, 0X08, 0X00, 0X01, 0X00, 0X00, 0X00, 0X00, 0X00, 0X00 }; // IPv6 not support
wsprintf(szMsg, _T("%d IPV6连接 不支持..."), index); wsprintf(szMsg, _TRF("%d IPV6连接 不支持..."), index);
g_pProxyMap->m_iocpLocal->Send(pContext, (LPBYTE)msg_ipv6_nok, sizeof(msg_ipv6_nok)); g_pProxyMap->m_iocpLocal->Send(pContext, (LPBYTE)msg_ipv6_nok, sizeof(msg_ipv6_nok));
g_pProxyMap->m_iocpLocal->Disconnect(pContext->m_Socket); g_pProxyMap->m_iocpLocal->Disconnect(pContext->m_Socket);
break; break;
@@ -197,7 +197,7 @@ void CALLBACK CProxyMapDlg::NotifyProc(void *user, ClientContext* pContext, UINT
buf[3] = lpData[3]; buf[3] = lpData[3];
g_pProxyMap->m_iocpLocal->Send(pContext, buf, sizeof(buf)); g_pProxyMap->m_iocpLocal->Send(pContext, buf, sizeof(buf));
g_pProxyMap->m_iocpLocal->Disconnect(pContext->m_Socket); g_pProxyMap->m_iocpLocal->Disconnect(pContext->m_Socket);
wsprintf(szMsg, _T("%d 不符要求, 断开 %d %d %d\r\n"), index, lpData[0], lpData[1], lpData[3]); wsprintf(szMsg, _TRF("%d 不符要求, 断开 %d %d %d\r\n"), index, lpData[0], lpData[1], lpData[3]);
} }
} }
break; break;
@@ -238,9 +238,9 @@ void CProxyMapDlg::OnReceiveComplete()
if (m_iocpLocal->m_TcpServer->GetConnectionExtra((CONNID)index, (PVOID*)&pContext_proxy) && pContext_proxy != nullptr) { if (m_iocpLocal->m_TcpServer->GetConnectionExtra((CONNID)index, (PVOID*)&pContext_proxy) && pContext_proxy != nullptr) {
if (sendbuf[1] == 0) { if (sendbuf[1] == 0) {
pContext_proxy->m_bProxyConnected = 2; pContext_proxy->m_bProxyConnected = 2;
wsprintf(szMsg, _T("%d 连接成功\r\n"), index); wsprintf(szMsg, _TRF("%d 连接成功\r\n"), index);
} else } else
wsprintf(szMsg, _T("%d 连接失败\r\n"), index); wsprintf(szMsg, _TRF("%d 连接失败\r\n"), index);
m_iocpLocal->Send(pContext_proxy, sendbuf, sizeof(sendbuf)); m_iocpLocal->Send(pContext_proxy, sendbuf, sizeof(sendbuf));
AddLog(szMsg); AddLog(szMsg);
} }
@@ -266,7 +266,7 @@ void CProxyMapDlg::OnReceiveComplete()
AddLog(szMsg); AddLog(szMsg);
return; return;
} }
wsprintf(szMsg, _T("%d ==>收 %d bytes\r\n"), index, m_ContextObject->m_DeCompressionBuffer.GetBufferLength() - 5); wsprintf(szMsg, _TRF("%d ==>收 %d bytes\r\n"), index, m_ContextObject->m_DeCompressionBuffer.GetBufferLength() - 5);
AddLog(szMsg); AddLog(szMsg);
} }
} }
Binary file not shown.

Before

Width:  |  Height:  |  Size: 95 KiB

After

Width:  |  Height:  |  Size: 128 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

+232
View File
@@ -0,0 +1,232 @@
/* Copyright 2021-2026 Ethan Halsall. This file is part of wasm-audio-decoders. https://github.com/eshaz/wasm-audio-decoders */
var i,t;i=this,t=function(i,t){const e=(i,t=4294967295,e=79764919)=>{const H=new Int32Array(256);let s,h,r,n=t;for(s=0;s<256;s++){for(r=s<<24,h=8;h>0;--h)r=2147483648&r?r<<1^e:r<<1;H[s]=r}for(s=0;s<i.length;s++)n=n<<8^H[255&(n>>24^i[s])];return n},H=(i,t=e)=>{const H=i=>new Uint8Array(i.length/2).map((t,e)=>parseInt(i.substring(2*e,2*(e+1)),16)),s=i=>H(i)[0],h=new Map;[,8364,,8218,402,8222,8230,8224,8225,710,8240,352,8249,338,,381,,,8216,8217,8220,8221,8226,8211,8212,732,8482,353,8250,339,,382,376].forEach((i,t)=>h.set(i,t));const r=new Uint8Array(i.length);let n,M,o,a=!1,c=0,I=42,u=i.length>13&&"dynEncode"===i.substring(0,9),y=0;u&&(y=11,M=s(i.substring(9,y)),M<=1&&(y+=2,I=s(i.substring(11,y))),1===M&&(y+=8,o=(i=>new DataView(H(i).buffer).getInt32(0,!0))(i.substring(13,y))));const l=256-I;for(let e=y;e<i.length;e++)if(n=i.charCodeAt(e),61!==n||a){if(92===n&&e<i.length-5&&u){const t=i.charCodeAt(e+1);117!==t&&85!==t||(n=parseInt(i.substring(e+2,e+6),16),e+=5)}if(n>255){const i=h.get(n);i&&(n=i+127)}a&&(a=!1,n-=64),r[c++]=n<I&&n>0?n+l:n-I}else a=!0;const d=r.subarray(0,c);if(u&&1===M){const i=t(d);if(i!==o){const t="Decode failed crc32 validation";throw console.error("`simple-yenc`\n",t+"\n","Expected: "+o+"; Got: "+i+"\n","Visit https://github.com/eshaz/simple-yenc for more information"),Error(t)}}return d};function s(){const i=Uint8Array,t=Float32Array;s.t||Object.defineProperties(s,{t:{value:new WeakMap},H:{value(i,t){s.t.set(i,Promise.resolve(t))}},M:{value(i,t){let e=s.t.get(i);return e||(t?e=WebAssembly.compile(H(t)):(t=i.I,e=s.u(t).then(i=>WebAssembly.compile(i))),s.t.set(i,e)),e}},p:{value(i,e){let H=new t(e),s=0,h=0;for(;s<i.length;)H.set(i[s],h),h+=i[s++].length;return H}},J:{value:(i,t,e,H,s)=>({errors:i,channelData:t,samplesDecoded:e,sampleRate:H,bitDepth:s})},W:{value(i,t,e,H,h,r){let n,M,o=[];for(n=0;n<e;n++){const i=[];for(M=0;M<t.length;)i.push(t[M++][n]||[]);o.push(s.p(i,H))}return s.J(i,o,H,h,r)}},u:{value(t){return t=H(t),new Promise(e=>{const H=String.raw`dynEncode012804c7886d(‰›•)((()>+ˆ*§§)§ˆ,§§§§)§ˆ+§§§)§+.-()(*)-+)(ˆ.7*§)i¸¸,3§(i¸¸,3/G+.••—š¡*(,˜ŽŽ(,3‡‡‰˜‡Š‰›+)2å:-œ),§H(P*DI*H(P*@I++hH)H*r,hH(H(P*<J,i)’^*<H,H(P*4’U((I-H(H*i0’J,^*DH+H-H*œšI+H,I*4)33H(H*H)“^*DH(H+H)^*@H+i§H)œi§›™3æ*).§K(iH“I/+§H,iHn,§H+i(H+i(rCJ0I,H*I-+hH,,hH(H-V)(i)œ’J.H.W)(i)’c)(H,i)“I,H-i*’I-4)33i(I.*hH(V)(H+n5(H(i*’I-i(I,i)I.+hH,i*’J+iHn,hi(I-i*I,+hH,H/’H-c)(H,iFn,hi(I,+hH,H0n5-H*V)(J(,hH/H(i)œ’J(H(V)(J(i)’c)(H)H(i)œ’H,c)(3H*i*’I*H,i)’I,4(3(-H(H,’W)(H-’I-H,i*’I,4)3(3(3H,H-’I1H+I,H.i)œH1V)(“J.i(v5(33H.-H(H,’i(c)(H,i*’I,4)333–)-§i*I*+§H*iHn,hiž73H,H(i)8(H+šJ+H)P*(H*’V)(J-“r,§H)P*,H.i)œ’H+H,“i)œ’V)(-H*i*’I*H+i)œI+H-H.’I.H,H-’i)œI,4)333Ã+)-§iø7i(^*(iü7I,*h+hH+iDn,h*hilI+i)I,+hH+,hH+iô7’H,c)(i)H+i´8’W)(œH,’I,H+i*’I+4)-+hH(H)8*J-i(p5.*h*h*hH-i')u,hH(P*(J+,hH(P*0J,H(P*,n50H+H,’H-b((3H(P*0i)’I.4)3H-i¨*n5*H-iÅ*s,hiž73H-i)œJ+V)&+I,H(H+V)æ,8(I.H(H*8*J-i(p51H-i)œJ+i¸7’V)(H(H+iø7’V)(8(’J/H(P*0J+s,hi73H+H,H.’J,’I.H(P*(m5(H.H(P*,s5.+hH,m5*H(P*(J.H+’H.H+H/“’U((b((H(H(P*0i)’J+^*0H,i)“I,4(3(3H(H.^*03H-i¨*o5)33i(73(3(3-H,H+i)žc)(H,i*’I,H+i)’I+4)33i)I-3H-3!2)0§K(i2“J,L(H,H(^*(H,H*^*4H,i(^*0H,i(^*DH,j(_*<H,H)P*(^*,H,H+P*(^*8*h*h+hH,i)8(I3i§I**h*h*h*h*h*h*hH,i*8(6+(),03H,j(_*@i*I-H,P*<J.i,’J(H,P*8J/s50H,H.i+’J0^*<i¦I*H.H,P*4J1’J.U(*H.U((J2›i')o5/H.U()I.H,H(^*<H0H1’U((H.i0œJ.i§›i0ži')™o5/H/H.H2šJ*H(’J.q50H,P*0J/H*’I-H,P*(J0,hH,P*,H-q,hi)I-423+hH*m5+H/H0’H(H1’U((b((H/i)’I/H(i)’I(H*i)“I*4(3(3H,H.^*<H,H-^*04*3iØ1U((5+i(I(i¨7i1^*(i$6iè1^*(i°7iè6^*(i¬7iÈ6^*(+hH(iÈ*n,hiÈ*I(+hH(i¨,n,hi¨,I(+hH(iØ,n,hiØ,I(+hH(iè,o,hH,i-’H(’i0c)(H(i*’I(4)33iè1i1H,i-’iÈ*8)Bi(I(+hH(ido,hH,i-’H(’i-c)(H(i*’I(4)33iÈ6iè6H,i-’iF8)BiØ1i)b((41-H,i-’H(’i/c)(H(i*’I(4)3(3(-H,i-’H(’i1c)(H(i*’I(4)3(3(-H,i-’H(’i0c)(H(i*’I(4)3(3(3H,H/^*0H,H(^*<3i(I*4*3H,H,i¸)’^*TH,H,iø-’^*PH,H,iX’^*LH,H,i(’^*HH,i-8(I(H,i-8(I-i¥I*H,i,8(I.H(iErH-iErš5)H(i©*’I1H-i)’I0i(i;H.i,’J(i(H(i(rCJ(“J*H*i;sCI*i¨1I-H(I/+hH/,hH,i-’H-V)(i)œ’H,i+8(c)(H/i)“I/H-i*’I-H*i)’I*4)-H(i)œi¨1’I/+hH(H*o,hH,i-’H/V)(i)œ’i(c)(H/i*’I/H(i)’I(4)33i¤I*H,iø-’H,i¸)’H,i-’i;8)5+H0H1’I2i(I-+hH-H2p,hH,H,iP’8*J*i(p5-H*i7u,hH,i-’H-i)œ’H*c)(H-i)’I-4*3i(I/i+I.i+I(*h*h*hH*i8“6*(*)3H-m,hi£I*403H-i)œH,’W)-I/i*I(4)3i3I.i/I(3H2H,H(8(H.’J(H-’J.p,hi¢I*4.3H,i-’H-i)œ’I*+hH(,hH*H/c)(H*i*’I*H(i)“I(4)-H.I-4+3(3(33H,W)1m,hiŸI*4,3H,iø-’H,i¸)’H,i-’H18)J(,hi¡I*H(i(p5,H1H,V)ú-H,V)ø-’o5,3H,i(’H,iX’H,i-’H1i)œ’H08)J(,hi I*H(i(p5,H0H,V)šH,V)˜’o5,3H,H,iP’H,iH’8+I*4+3(3(3H,i$6i¬78+I*3H*H3šm5(3i)I-H*i(r5)3H)H,P*0^*(H+H,P*<^*(H*I-3H,i2’L(H-33Á)+(i¨03b+(,(-(.(/(0(1(2(3(5(7(9(;(?(C(G(K(S([(c(k({(‹(›(«(Ë(ë( (*)(iø03O)()()()(*(*(*(*(+(+(+(+(,(,(,(,(-(-(-(-(i¨13M8(9(:(((0(/(1(.(2(-(3(,(4(+(5(*(6()(7(T7œ‰šœ‡Ž‰œš›*S7•œ‰Š”U”—Љ” Line truncated
®Fj{øú¢ç=}(ݝñŸÜnŸ moǰ¥à•eò?È×µh
ØW¨;Ñ aØX­æU{¤øàÖÕÀì·ÝÌ­íø±tâÔøeVO÷PÉ´È~ÃòsX1´ÊPyK·ÿ™ŒKZçµ ›W„Ñ¡¡_‘ ‘dÍ•í+]Õ~%Yïþîñ ¾Ú·”p[òß ó÷y lhnn«²ù*̐éñ®ï(éÇÃRÊÆRÌ-¬ÐoÒ”Ê2z+ä$‚:ÄsxÏ#Ït!MlBqžš§%Jè„ ƒ®Ä3ó°á¸eÅ_Ö=}iTÐ͆mo †ðbMpžl–¬E›P°e,¼Æju®ix>#}N€gd™ÿÇÄbç9WZÇ·n\†Öá1ڛÈt.ú6bì:û† ào¯ú4Ÿ“Ïø ü­zå¯#ÆkŸ‡M#úÿ9›ë:üÒÝëÚà†¡d”•%0ÒekÓ×mðƸ½ùÿ⇿Ò9 ›ÄgªÕÂ%ü¸» HÁMŽ#Âqˆ²~Œèn¯ Ûa_9j= ùÿR™èæÊó%Mƒ
’¨,Àæq%98‰a9%T—˜2)´z«9gsÒ‚Ö!”ZhRõRێyÕ²ŒÝ=M<Öcº »~VFÕÖx:þrޑðnŽ£x¦8.è†$K9þ¹úSnÍ3¦ˆv…üuºsÕñ=}œ1—=M5Zʕ¦CÿSÉ2×qÒ««S†˜a»ù±(¢º]ÀmÍh÷/¥3@sns2 –%7ù-Q|nÏoöjYÛ]}à2{€EB’êÎ6ƒ>ÁbÄü
á, [DAÐuùjeN‹D‹ EëÔàZ‘ºlq˜»J /˲ájº‚ßCÃÝ1 b)øtN7Ê=}gÕ®òÍá§8²kDïû¤åÒÅóºc=M²= aË.ooèf€.QZj@bñ>qV©?æ• ƒxñˆR ô|_xw ê ˆ¸÷È¡ö"C¶œ2[¼‹ï¯¸©K†ŠmäºúZ€…¡%^°bóþ»-¼Ú¥ZIŽ(pþ…µS%øµã*Ò±8f:t¡éþ†õz¶<Äfñ*žöeP8AÓ¾/g×\С1Ï"= ¤ûGsW_¹ÔÈX Tãà‚uT™íEKPüݸ#åB¢ñ?:µßĬîFŽI8QY;X… íŸ¦k{|¦éíà/fªi>´EÈoK= Š6¼l„Ör²ÿ*YǏv앭dàG{Ö,Gz½.gzSdïV%…’Š˜‰z“¥8²bO™¡2\nOžßV—¡S2KçK´”žUûu۝Ÿã~„f¾K)ÊÖ#´?#»ÅYƒXOøò T™CB}LUk,kÍ)]ŽíiEŸ^ÿyW)Ê»³= ô1_½úÌÞÊçýS¡‰
»£jŒÆkžh Ð1˜G<è^àÙç?…æF¦Ç/ã«Eý6ZI¹tšl.‚aëül|„!&:MÓå?N@øËœÅf 6cì$w–,3~„17F=M𸳞°a‰õu\9æk ‡I—&>V®‡Æ/Øa´ †%¢],ÿ:±wäé¬áS¥FK±U¢±ÓPóŠßœ&“®ƒe€‘= ÍÍܯÏ×À@= Ø‹#‚Ðk[NCó÷À‚ÄÞŽGQRˆcnI}HGñ—\’ç­= öè^áygyDԋa©D”¦„…'ߣMߣPI¯à¨=}˜=MUÀ¸çy 9W½LšíU·ôzwzÃsÝZòŽÓ3ŸFž¸“—Á菣âƒÑ–’nՙ%‘Í¥k…á®=Mˆ×ŸGTÀÊøfëM-UF‡;ŠuåÙ#…X#‰—
nˆ©ï֟DÅñ¸q ’,Թи;h¿åñŸ#µNÓ÷¸v} *ځ‡_= ãýØb6N–=MH§'®Iƒu¯78˜–‘ªÊÝ#42p´[/ƒ¿E= =MxƒåÄV§Ým©3?;bô9fb÷Ð'¨È˜IS7ñ >¬IÍÜK tçքJ 7û6ߥA/|BÏ›=Mõª‡w¿T¼91óí•ÒèœÔ_6»Âõˆªvg2ý¡‰õ÷Ò=}Å3ýXm²¬bˆÛ}U¬0œ¸Ük~e“
aY¬ÈƒÛ•µ¨¢–0N„ñ¬t³VÉhÖYvx§˜"ǘ5\N'ªÂ£æKlÂ?þ{m¼‰ôê±ñÔ|L„m¬t_œ2%¤E„}Ù>›ÎFûalSXø²tRªÀ8 ¥£Å˜š™|ÖW›(Ù)\3Úwåì:›oíý±Y4ì©géµC³¨Q‰\ÍõMÕ„
!J½6ë:ËgípoÒ=}I=}ëS¯¸…ƒž™F=}ÙT__U‰¾&\íÛ ¼Âkg#ú Ú¡ÀsÏÔTžµ³ÑËߊxNÉæGç^4Ñ¡%.¦U±O[ø¼4pͺš7܆âªcÔ¦¢:oŠu¹8ÌzîXfœ¬56DC´Þ]ӆž!=MOÀՉâGÖӝ«0¤–¿‘Å2ú
éηNó“ÚÄr_Z)6y&2^F|#²ì‘36eòa¦rcLõ5Hö<r¼J!í¼©DøÓ@¨åPóˆfÑ£®›·§hQ®õ ½‡c±M¥W,ÝàŒÜ««¿‰¢Vmù| ˋ·wj$témè±°&í9yò‰µ“fæuC”:‡E@[RX\.qéuDýé(‹iao»]W5?I!z®ž¡±âàk—Ëý¼ƒLó–FµUM¨cEu’¼üÃFÔì€<@y´ÜøÆ#y«™µ‡à dMå@„šÛXüû¼á¾ù[CHºÀ[4ñ5z:ӆàV[B•Ò/I!ÕÓÂXj«ýH&o³©sFÍ&-ö«7d\X#>¦€yñÜÃMG”c~#‘!‡T ütŽ? ªÌ·«‰á¨É»ÕBms¸Û®0úBšjÛ(ƒ^ÓÅ®¤ÛF¹ÐmŒ›a)'¨ë(²ÄÞÍm²¦á¡B„á|”%«L¹a\×Ub= -f‹®Ž¢Ü¢«GÌSÔxÖXc‹Ãȋ‹ŸœŠÄh½4e#"‰'…;P >Rõo;%tÐC$g%Ä>70* *©cX— !C7€yôîËtO'j?óíÛµÁûH= I†ŽróÛ= kòwi‰J]€]þ†ßY÷{šŒŸmûpLÿÊe•LA¾À <½å…i­WÎ?o©t¡caâB¼iBüÛڈ¯:_î˜OJ7"\¥^­EPHQ”¼Cõ *{ÎÁIÅŝ$ÉVÇbo]XÆs)^‘H—²®wójÔÎE“6OÙÿŽsRͨEù9ž/²OÉQÿmåŸ«õ‡ØÆœZa;ˆÄ{ <}˜¸°Ró=MÖ²ý‰Üz´|´†pµÛá!cï‡ Æ,@zÃTŠ«d ³åoÙ-KŠyÁÓ4gß-²ÂŸxÊØÏ\@‡q&}eÔÂèŸ\ßÍT1Ï\kŸƒì÷Úw…é\À“Ä~<à¢e«4E¡/«Ä­aõu½=}l°Ã˜Në/J­å=}ÿ®!.N‘Š8…Ær)HÖ43¡C3Ôz/N :
Ú7!Z†‹1Þqöž)M%"O7^+TɚêaF8øc’2ú%ù3}Ô²dU(™³͏Zň±£y¤aŠf¿Ï•][Hþ”s·Ù$EpFæt4¦$V‰ú
í!hãV53øKéâ=}Nå22Úú"AÃ5£áª6ŽŽWÕæYôȂ^ AÜÍ)ýYÌúë¾a0= ºvéèŠ:‡7“Mô=MWsE²Š£²ÇL¿òÞ² —ܳJ+= 5æ+,>
¸M÷‰7jè;/_ý7ãzÈ)D÷·Ç4*È#†Ú¹"ó­!†z0 º—®“еZ*Šìx®
R>sÚýP7Š:ƒ¯ZZ=MŒͅW²Dê6Ó^Nq= fÍbN¿VþÒª£;û<cVýî’媖“6§7dËÃ-²usÒ¹%·iÄs4òf N…(ᨔ|õ-yZ$E)—¹ï5Wí]Q\·1U9†:[-zQ¼—É7÷/þžiÉõ ¿«Ob)Iµñ0
…lÁNpʦË-+Kfø»
cù@®W¡‘Y,îuC6ZK¤é‰Ï²µI=}= óáÔÁ4´-ƒõfԔmõVÛvFÞÀç*=}éåú6™î®£³úÄßy'GD1dƒÕ±È"šx^ù1˜)DÚ'öŽ–\©^6Œ§ö_гîªZ|¾S= @$^*- 'ªÈÄb7ÎÜt7Z4©ö§–É…šU•hY{ÓÚh£ˆ ±TޗÁÉýҎRŒ‰7Ë †¡t¨‹<#Ĩ¡@EK7†Ïb3Uri)åw –=}ø»n#MMuœåÛ™Ä œ ¼ü‰÷´ÒuâÆ8¯¬œ»R6‘I¡WBòîtžnP=Má‹-0óà¤ÕƓ+¯KæÛc„âÑG½þÛÀFä7"ATMáu+mø ¼?ә^Ê?Ts÷ÁR7ñ³!åogã _ËRª«J$¼­Ž‚nÇ ‹h‘ ýõgcíÿ·ÅºÊ§~̂= @<Ûx¿a¥L¦ÃOŸ·—òi½ eûž…ÎL~tÞ/ˆ'ž…—­=M›$x@½tZŖYÖ&¼£]6o¹åۃѕv‹àMfgp£7ŒÇ6ùßkØ5˜i>9àlÊ Š>ƒÐ7w)ý;³(JKLª.z7ðfê2jÐ"£™Ÿ.SˆJ®“.çŠòìV\3(ÎÍ­Êt"´»µ¾¸[Œ
¸8Œ÷Ê+AúCñ&ÿøH]÷hly^Îàî ®ðgåA‰6í”KÊ7ÒäGo¿:ƒ¥G[Åþ¡«aGÞ¾Ž!â
=MnB»ªÖŽ1ÔIZò2»JkëÆwùºÅtªŠ’¯xÇ»×utŠ#jà:æî‹OÒ6P¿èrñË×ôm2¸ÓJ±*h«koâ}»¯£F÷§÷¸”#aƒ®—ŠÅˆ.ùÌ/³3ځ9Vϗ[³22L[ŸÓ,ö«W &$/U›Vq* fÐ;F°®/Ѧ·qo:SCÚɼñ|¨íu09é±|Þ'­ÏYÍ. 5Cm;Àúñ=MmW8ÒSŸM³¦äHmsA^mÞ¢£Â©}‚oO21*‚ÒÃʋFbäT~¿P'7ÑDØÖW®äO{ŽZ»ž A ؄;t£82Jb^“:®ÇOß­v@r*o:×>·ûxGÌt/ë*Š”F:7ÝÁwd¾º›£D´{¢= ´¾˜3EK·‚<MÉ¿†8ÂìiÔÁ°r6Ö!F{'H§Ü
y¢r‚Ín(2Rƒ—æQ{ Û»Ìg{¸G%~»¥QÜm$B˒¡[Ú®jþXè59[ô
âRAÁïyw—ìüwrz˜)yhX#p¼èt!’1‹~^ h6kî^C6}hpBŽ{‘£Q5—x¿ÁoН= à ˆ£É<󛫀¡ìè31ÜÌç±3X~:ÚÂS~«3/÷9ÚJ8Æ~­^\¨Õ’ÀŠvȚØýd9zX|ÿ1¨‰
= S^bÅ]É:×l÷EÜ1= ¦¼Ê
T4’ÙCéhåìcÅ»èZ³}!©ð–:~^¯ãó*Mï8r€Úm}6S3–# ùÊ(%òYï¬%a¯N½Ž
ï˜õ­î/Q ½É}I£Û£þ×ÍLtä,QMÊÚ0la; {Z½¥{é¹3§£:Nj´¸ÆbýÖÜÊÝizeµvýEärìY(£Î£Ô¾oþ3Š9hn,9øÚ¯Œúõyä¿Ôb4q
ÏTÊî·ÑڝQÆï+zy¥0o”b;¢ÀŒï­ˆüÕçIò‡õ[I"îšY°â‰p×AÕ ‹# }êN92‚Üž'Ú=}Å®Õà¹Xg[9„ÿÐ58¿ŒCke.ÓÏ#êzü=}ÒGkÌ%Æ"èύ^q±R3
=}*@Å@¹æÛir·÷³¸{Ôd«WžÈHÒ=M®Ôæ•æa!RqÄםóÁ—ᯍSÛ®?Ãëê ¨.ÃW§r+TSóÛ5_µ®cT8_WqP†ãtå!Wü= ºmµ¬È
Ò+ŸKq¢¢–ãgõ ËÁY”bHG@¡™yt‡]NÖ¥¼~0X+ûx85&Bùž2—×Ûßoç¤›„AŠ:$'áôÑÀ9º|:ª °­zï:— ­$—Jo¯’«Ë›ÜÛ÷àŽSWîK5´ÁË$fÙÓ¨Ãޚ1£L š.uïÇ-ê{¹/&ê֒VÆcí[>£µآŵûȟ{-VÊm_°S›³òÀmªŸQ©FlaÞñíÏW¬D©lÑñ)ðss½ÎÿVƒBt LªS+¬KB\;ªå'þmd'=}‘ÈnÊu&þÚ®4Ps±ô°ñZ÷¹„(ÌýlÜ*M»N«Ïö:[Վ.lrP޽0™w¬žƒ=Mâ×sWíQx_ó‘?CA'
Z“W-òªi±´¥²ÊMZ§æ†„dÀZÐùõ«š˜óZ—ófÉÑüÔè­\rN’ÀóV6Á69f•~6dOgD п”o>‚‘é#Y¾u?ØÞœØš-Ù±WE™ ¯=M^Ïè–bȽq—Úƒäqj ‘r”œ½ZmFïâjʘ4áÃ1XÒ93Oʚ´YöcÛj¥•[Èñ£@!9+ÿI™ üb•Fu#PB©ž
DÑÌYíuÃàÓ+µ"—8“q1·ìÚ¸óTü-¤‘¯ïiõæ)êI÷ïсýÙ¤†£.У‘Û P‡¬A-|¸ëu¥ätj ì¬ÔåÔ8O¥ §IØqÍB¨|D8xò)ºmÕCڒßÒ‰j&ç’þâ&}D#² –Žš½Af9«‘œ ÛБÑW‚—ï^Àþ°w4Ì÷ÿ䣁–¦›ü-yÑ‘ïc®Tú÷5YQp'ùÆ×Íçj1vaî@;åQ‘ÃÀ~¶´‡õ|Ÿ}ñh×ébÔF„4\BÂC&ÿé>SB0qç=}„¤+«mÜp®¶-[—j7ÈÁ­(’bC´=}CѼ±‰§‰ =}Êx±µn†!üÍ@je ¾Ÿ…”Új¯œq!Ró6™×üv$2ÐCÿÞ2!ý^o›äuÏtmQ»ý)ËgÏ_¦npËv õâ;þª«âÍ×;sbŸû–ûoD³auæ³e<=}ÿaSªrýô‡:kû…Üuü>YÃWa³'ÞF{À¼‚ìrag‡ÞP-n£íñN¬X—ª½Z43<yþ²]•¡a®€‰ãˆK[³'· h~ÚW6xòv2ß:.Y¢vÐ|°3‚_”+Æ×@8±+p €e+©Æû |­Ãˆ\|. gè+âT%ç#0ÃêÛ|ò4Õ®ü= 6©Q?ŽÐ¾ô‡=}V–nqæ…ö«…ø{Õ݉­£y"ërk•Ün§ÛVK ý¯«¼2Ìäí½÷Á_9¸­O$6GÌljÜÁªYý»ï#ç[ދ&0¸5á# p*zÄɦ/¦†“ßõè{š»ú-%Ç Ž.¥ ×¾A?Ñ¥m0!{eõˆR,nÖ° íe .ˍ= \pÈÞ=}4”½²N\xÏnYÏÁÏ®»Ö½T¡Ðº.„^¿€èÝۊœ.@¸u9¼p֌Z•¹ÞÐöxjC%fڃl1 ÷þ¬Ñ­q…â|Tí*mc~‚
4:íd;8Å"7Jî^óðP<ª4 ïî‰rØgcŒ
m©°áԅßjf+‰áˆÁCˆ®êAªiO!Ðp8!1ǙœÒë-J‚œö9Õp+mz¸ŸŽ…K#€ôxpGS øQP@c1)Ó ¢_·’=}ºøÁ€9VÙ£Þþ”ôŽbF0‡†C“Š±œózHþ, ÏÊ?d“gºŸPÙ6‹ ì҅òÐMþz’²ÓR½Šœ7d~t¾™óÍw¼äN×3†lVªk ·6­O@*%1‡ð®Ðùã͸iæ—hÔ3é6šåº¦•ªÐN:A6xð†–éÝË Œ=}¾\„š&Š¿Š§ß<Í=MS¼Ê¬ü8â÷/Þä:ô*4Äꃯb(çi@ñ¼‰(|«¥M$.'i3ÖL>\#qkºýþé–;3éy”M)"N\>'?9.ۋ6c7síÒÛ)°tU9QA5zòfvèð.+þèÃÐg?€»|îb$±)|9äáà’gJ…aJù[ÿh:©rÎxò¼…ÿ*­ h5z«ÅØKÚÛ¯èß«Àú½¾]•½íDØÙ¼mñx½E¯T£E·*%eúٚ=}å>òGG7Õ.ì(ö,(lÙ³‘°çÿßrê§@óOcZßëÏ/m|Ñ4J­ô-@dbºX’„…­þx.I*çÃ3†Ÿ†éÙx9dÚ*zQÙP;Òö›nk·—µ×ú­¤îB@ÇW[3îÉ"­Lhó Y["nÌH8‹<^¬ =}µ«|ͱ^·+„¤¬Å/3h^”Óìçç» *ãöøŽ5a¿3uýñ”Ì0 |‡jW˜p.m˜Þ.˜MÉłw*iìe|ÂSÈÊWÒ£é܅…Lb–é4–^&©SÉ«°çn‡›xŸ¨Jg­Ä9°XXî=Mƒñ”Ò?Ñ¢öN¿= 
„AŽèWÜ õþ5¿Œ¡œ~ì "3nßÐ׿™â¨Bªœ3ÎNdֈ”b(,,âÿé®w䨏Â[ ”þ4þè|Çaán?C¦°¼èŒ{Üsîb>ûåxmîNúÿZ"Lˆß+ŽûŽŠ= ÞßO†…üwí·™¨Ÿî®Ü_‹RŠä± \Õ싱g?Òst¢œÌcÂÉ,ÊE9<ˆ{{ì20J”ù l{JDE¯= JõËƒaÿôÀ”‘bàè±Ôˆ¥hV>¡ %á:ƯîÈ%E3-8ð = öüönÑ[Q_fTp“EF€c½´ÇƒÖýc§P€ÄO€OdâŽ‹‹ +I„ÒRƒ%σ7 ¦Ãe¬ô®9¢2Ù*4
EžòCñÔ·T†¦aG¯a*1HŽOX®Ï=}J¦¿†JæÂ mœ¯Jš1«uð¦JJw
Ät̕ñVÔkMqیě=MTÏ&ïÌÒ9‰}¨â—nz™ìŸrµ³¤ñæ4lÔ=MΓ7l¾É-¿oœÐwàâf¯RìàÅXÿ“bÃfõÈS$y^x{u€—=M~ÿõ K¸x}ϵRÌ&¸ÿÕ Læ5; ¬©‰܏¤{³«0{ÿ¹g«êD¾ÓWFäQ ù—]±koÊBÖUÁ!ìö›rH~.LHþ wq>nGžÂ›SÕᛖÁÜ´';§§SÕþïM]•Ê"p'­ÚÅÞ«‡€äÕãZöêAè(¦ 7fa›&ÁÜ´§BçoGžEñ¡SÕ±üµÁܘ£1;Þ.t±aƒJ öL¥o:“]ôáŸÉÊ"¨ªh쏭*GÑ=} †1Ù•’BW'(–ßk{­T΀¾!ñ’ i2k˼´•wô|&¤Õꇂ·,Ú=M™¡oV¡î|8H€ÔùK$¥J(mˌßÝïOísˆ¹Cÿ½a”u^‘<ɔd•ƒƒó/%Š›í²Ýœá'_dKÉ;æó}:5­®Ì?9ÈdÒ¯Gúa’}{=}‰¡bG‚23bؒ26‰Þpâë@QÃÞoÜ«™–í¸Äî5…ß:¶9¹4¨·ó5}A·~¯m¥©ªs™{?ÓüôZ”3r‘èzÔUª¬<ur¥}uµmT{“~ƒc3!ºù(hŸŸ$‡ÙÄ5„!8Ýö¨疽ý½lÇ·ü^VfɎ£i§³væ·3ƒBmŸ;«­ä³ãVÁ©Òò"³ñëíëß§T…¬ý¦CÝHéôtàÔ1¦<¦še\qÚ$9­œ2¦>‘Œ-oO
!ú)h“;qS£Ç@²ä™ÃԊw×Õê99© ¯”œOñ^á|mæé4´™áíiñI¥\¼)±Ù=MÛȎ¦c™îõy–4Œ~1uh\é=Mòðs=Mˋh瞬ƒ¹Ã§ Œ¥ Ò$IºQfK·•ƒ|ëpÆ©{Ù3?œñê‚ÑKòV¨ôÿ½ªÝyÎ'¯4oôØäï§cXŽ fy¬q:ž <|µ…ÿŸ9š$9jÒ[ÙÌc#KHÜL4&‹g|ÎÝ&°¥ô££ŸÖþ(µØ€2u”3:¾JptŸjb˜^/X8½Þñ!°.^Î97ÚØ úÉcDÉP–0ó ï>Ì~y5ïÍ{ÿgµ'Næ¤ö¾+Ùʕîiì–üXRjàF1Ïñ*
>Œ>¦›s'ɟ™Øð§ãñ¥œA<uÍ'XV…÷“Fdœ}^ÿÑIL֋™Ó AÅÉL^P?Y©Oð)]i$pÿõ}/Là)[­Ø“k¹Vìå6ÃÙYï‡0ƒIß#³{Ç(ù*1$q?àôb;MìÁÜ´§=}qpGžv›rH~.Ó½kGžòû1G~.pklGžö›–ÁÜ´'uko¤\sñn}}̐_ÜhùåÌ7‹oʨ»èVÁ!ì›àœ£YV½èTÁ!ìL“
kïF1—ãËÝ}ôÉFàß2¸½òŽŠ¿áW±RÄËÐðã󾘇!òýù©
N~%sL¬cv®)iUïJ«]R¸yJ‹r\xÿö\œPƒ¸„‰i¯e¿)–JßÌÂM§Èñ–I
LUŸz¦
1µØ,‰5ôq,:
EC0P$θ>Â쥪+8ÌÀš]RPļ£cÎü‚´éÿô‰ÚĄî?sÜ$ÿÆSÇDô¸]OyÎ-j¦*<a¬ßıÔ)é)%ÙéTGB>]åC½;DbM_ìO˜ÎOCKà˜»O cè ¸~)½=}4׃«Ú¯Ù2‰=}!÷´âøŠ®&Òsªï6³™ªh
**8º7”¨;Çÿ2Ù»ªõef^n^n^^n^^^y˜ iµÔÙ÷Ä>ÍÒ<Œ.ø²±,ŒÀ6<
.VRF é üÏ@¢®gsÖí'UQ×tF ×@¢¼@¢ŽQ‡Ü›Âûd±û‘xªÐ ÉäH§¸Ú†+
ÍuBý*
¬E¤´a•#±¯uÌyÊQ”åÑÏzâØåjš¨sPðÉðn±Õ¼£ááH¬óGsz„‚5$>àAÆbŽT§¸ßäÓB÷ûÀ‚‹˅{Ž’³á…½À5ÍÞæÓ­«Éz5%Úþ„ޱc…*|ð)o÷ ·Kôàþÿ~ŒÙc0>ãvP«ŒA°K8·¡¹ÚªÑÖ·ùgƕwƂƮ,céuÎæ´ò'•ü
Èeڗð—?enojÉGþ±âRÙòõòõ 1‘~%îPozá>zwVÊuR ‘âÞR†[¯¥J%ÕSæWT_Ñ7˚Wø1*¡=MÀYµ:¥ð•ŠZ·üø¦Âõ3JÂ5ýü¤Füœ“ºQҪ֒ êBvQTŸ²ç‰)–ŽÛ
%çI¯«ù
õç°|Õ ïiۅê~b¼ZQ&ûÕN²ùC²(^Äv¥Äëk
§.øŒÇ‚ü
–Ð|Q= ˆá& !I$±bö½Âí”9îmÔ IyÖg×!ļi^(¬³óŠx}5'&Ž2ÎgL~à"Ùϐã›^ð“ãD~šaíBÄø ãSŒ þ<@4¥è»…i䉆b#ïèUÑUhèÏ왪L[ÙQ]©â_H•i_¾°Y™îœµ7vnieՌØÈF³Êåš,ŸètK™ýKÉý™BS&ØŘw»0¤Èlûͼl£[= ø5»ƒ]›a8b„ȐUÎ-,‹zDÜ«dÏäsQ
¹.Ýõã¡c…o$'ð§ é‘ÞT-¦æm&ُŒ±äÄ1ÊÉLC|($àÛC؈³W/¸â ÉÇ@âAt‡VŒˆb¸“5Àœ
Ý%‰ËÞËѼžjÀßÛN¿2âaä_oÃ'°^Óå‡Þ’pòÏϒýóJHð¨6ñ â×xà»öX5¯;|J È†º©(Sèþó‹MZìÞ+à#·7¼£W˜»©6zªŽ¯w±ÆAƒHcËæï‚øÝÈ­ÐF[þ‘\…Wñ£Ò¨Éýó›ª<D šéä;$-=MõmN·çeF ³ï²½öaέX´.&ICÃ~üK NߓڏÉXKè‘a2C[ñëÇY³Ì™øöµÀvޞÿRë>SJ“®sºîÜwûöHÞ [8Y/‰£ä2ϾÖr÷2ûFI÷@+óWg#7,åûŸd¨Œ×ÞyµOÊ w놾B-—~ö×/X3Íè|êÊй"Ê.A ¦ ó ö‰DYêÖDJ»5èI0*U»}:ƒ]\€X]*»‘>Béúûücv= ž}#å
â˪27,7]òÛ¼ìñýWÅ8'¼ êe~»7Œ¿‚ªFËru•ÙâWë Ä«=M= ÄÏÓXI–óoN8->Yƒ/~6‚H_ƒ­Ð¥Ä𠄬ª¢c]d)Má“lúr57§k~¼÷aÃÍL}WE;ÄۊG~Mê榷Å'D^ñ<™zJµŸ,ÂÅ8ùÈ©^ÎËõihê?´àž˜’¿åh÷œ÷D3ˆÔ2ëh™ð"èç°í½0•°§Ä ëPHà:Yj“u™ûŸ])²® ØXÞÉçœ]ܗ‡Œiù±Zp}ZLƒ‡¾r
Iˆñ>ˆßv¼>O¡W}kb—ꓞîô¡AIزuï֎ãíå¢Úpv¿!_ÿA¬¡!ò’Ë51@B¯~¨QÂQ¦G<‘ÜclHôµ†Ó˜Ê=}©”T¬3Œ*ÂDà˜"ÃsI&=Mª p"EéjÊ;їÂ}¾–¯˜,. Þ¦òòޏC!^ kŠLØJ.Ð%~Á:ʹš:xJ'wµ+Šz³TÊ% Â+6zjx¯4ÂgU¯¬ç¨ÀZdútÚ=M“¯^U¦r–NPÔæ„O£EÖ÷i§Â*ÄZþ^í¿ޞ£‚|þÁ|:y&{6g(<«K-•Y®ûðz©m-zQ€D¢ÄÃŖl“s(Ù$ÅK-ë3Îe´Ô‚j - ¼µËüÍTITÐ)¡âZW‘äV­ëã>Ûß#ÜÆo•3<= 2ӂ£¬è ZtFtLˆüªó#Å Ct-é>Œ¯„¤Ë$¿ªBe—x„ ¦ £õÌ!Ég©åïÍB~Å4¬}­‘ÊÕåÄÑc Ÿ©þ{¢ÆˆTêU!ҕ,Ph}ä~ôçˆéU©h½Ð=}š_0©ƒïŒþFn£k¨ÓýV‡Féýïc=Máv-÷Vn= v€¹Å†g͓¸^*5Ý$¯rÜ ÞRÄK&ÊfíåÃKÈÄA¦ò¯9iië:ٚ0´.šV f$= ™¥í. òù™ØË„ŸMIƄ«¦Ë•†‡ý0ËõW6õ“ 8 I¹]&kóNg–—ì3Û¶M²«hžc.‹ùN‚= TF%É1–=}Ô;ˆñ_•Öº6@Z² î뤋žpò—6]©rcˆºa¼o2ç¶á¡Y甎dDˆÌ±H×6íãí/f¹°֝#͹G=}ˇ¬ Q°Å‚w“õ ‡ùYY$waìÿ¿³ —ħß³º¸xù4ÐøÒ¯)Ãé*-± –øÐVö–F‚éœñ#œðj^´¬N Ã{þ0ø´-vHDšå)LN¡ñ2¿ÇIܪZ{‚Ñ ë:B6ßè>Ø=}ðaÃcJ-Á_ëâÀïþ‹Çðï ð^¿åN]ÆX¦-Ô¼^&†¢j~óFÅÜSÕß¼l bR»BØöÂÍ^‘—<p$k(kCÚç§=d{()ÌJýv\ e*X€É’Ž!¢)X’Â[¿ëö Êü¢JãÛW=}±«öuï^TÔâ«9¹O$ß֗Y€™‚°€LtZؖ= qåN›2UÔ}îÓÆ¯¼‡É‚à mÉM1g%²½£hÅq]„÷•h¨oÊ$¼e>B¥]e^Ñ'QÿL= ó‡¤J¨yɏ õ<ÛJDá‚hGƒ³d[bñz-wWªxýÙcö…cÎPÆx|«A’XÚÎɊRLæ›3‘cÁ+ôšäŠ\0Wyâ?î@·a‡º3z¦ÅÁp³ “‹=}"  Ë= L0¨ÝK¶Úõ“pi¬Y{¸#ø³[¯zãBw¿‰øØ"å³ò (Œmpß³=MI-Ûp´Œ 69¬°Âe<JíªíXy)›kô—0¦8»ýmIó%SœÑ'‘Ov³]ó§S—|M òsz
ËÁ..ˆœ#ç¬A<ð<†‹£ãOàŽ'Ü\ò§"…ùxÓh*ÅPC~
Òá‹(a?-ÍJtUÿU?…=}ó
lÖöa­ß.Êl.f#þxñü÷D²WdÀTæ7ÔÁ'r¬Yï6£ëö“šƒ&©ðÎ'ìÎð9(G©òä:·kãúñêU‡às±=}àۄ€¢Ud¸ !;PN4RÝõ EK妋í¥ë iuõ½ïÕ©?sxž&Ì˔jï=MÍè£ÏœzÞ|\Ùâü%Ó§¿Ííƒà±Ço³×Op{v„µ²Ä=}ŠÅ/Q£Ig&[UèY}ˆoòÄ©.¥––>½öÔ½Ãd‚RN:Î]åj­W'ÃYÒàßEqJòuJtB‘éd‚IvRñyÂÖe?íb{æl‘WÆû°uÙ3-ÇQîfSÿ‡K²sÉÃ$mm!¼MTª(‹·Û-Cß?-\HXÚ=M \é¤JÜô¦5ta,}Z±Í¨JÃÑX¢¨šÀÌÝÈFk@ª) ĜK®YvĂ}™<h%&³ÀCn¶ØEß#êöp»žn¬ÉäNŒÓ²WA>t@f½l•oso{'òɌ™Î·ŠøðŸÂ§Å=}ÊûN…J%ú/ˆ%:pû¹=}'µ»K€'@'! åj1l?3XF–<·Úå¯nûܹjAÎFë »è®ò;1û Ç÷k{óvîžæu{°q±ûŸL“ WùXöñK™nìM¼0ügUi©X“²{vÒüËuÜ| XkېŸÆm’ø÷af?÷°ªBé@G#fˆçÛêHßQ¨õÒ±Ëà¤à0§H¿ÊÑÌç* WyTf|éw›¼½2=}®
‘ÅÃÔn­VãoM+¨±fZò|ws´+ çÄ6Kd3‹JÊl¯Éf¯}Ä^̸¸8'Ãx[V‘ ¦õÉ&+Ó\ÚÜ]©ÃIÄ^©u]Œ3œv'?—êIÁpSy„mÒÙQ#Â7s濐•¿ÕÞý€èIt£1ªÔ=}Xàv$Õn¬Æþç-µ,Qñ*À·7w ʾä…û¤z°á×K¬¿ÐtÝpŒW‘ÌđÌÂT óBGJ¦XÄÐÃk܀„•^étpï3·Mêo¤"Æ»(­ÄŠ~jr¦æàè¨Gñ‘Nß(1.©iõË=Mƒ©”ÁuµSKù¹ãÝР…~Œä¹s;©êѤ”‡rw¶O«m¾ù–钘ܡ­Ú¢”ÒÌ'"°3³ÿM{v*W¸]¿ÿôèxBÖÛO ï= ;n¹Îf;7:•ÝÇ1½cÞ¾¼W+Ã3ƒ¸)ÿRHñ¢Þ‘Ùô¢¨ç‡t[¥Æa£hìl, ~˝äÔ
7ª=Mû’-ÅàêÒxýað!”gGÃÿ³Ë_½)“ýŒÚÅÜ %#ay!WÔxÀÀÝåTíϤŸ¹®ø;(gU¥´(؉MȲއ†‘:•gªýü(Ï0BÝc×éË@¹¦jœ‚Îoyì¯'½‹a>õiKÂÓ÷Åc$Ããöˍ›ûÏy_UÏwø—‹{ÿm«9’? cÞT#‹Mê8=}Pôì_’#µýì7¼c%?gÎ}Õ›üé։ÿmNЋiF(ã q…çoxh>—Nä«Ï¤&ÈkûÈ´푅´¿ª€Õn?èÀ²ÆšÿÔxoÚi÷ÃÚ
ˆ]Ø;$3DƼ„â‡W2ڇÝ= ÃBdŠ“Q}ø’……ޱW¢Ü ZUNשð´žrr+Ëϲº$)¨†˜à>ê=M&Èt¬—Ë,º/ê|ÈÇ*½“U®î*¤ïŒßÚ=Má!$92™ÕávJ›ä
Ž†°ò5gqa:#D8¹ÈX™qøë±Ã:°:ôŒ>+˜K¨U°*‡ YnçÑ:32ùuÀ"öë%Kæ˜P”¯]vÎ%¥§Ñ×@“üþ±Y×Òd !ÀÚ_“¨éH8ÄgÖí˜Ù«´‚äñŸ«TÀW4*…½
4Gœ?†ÊRÛÁ[ý©L@§÷MyjÐΊôzG{¯ZŽ„ÏB¼[UGjmGÑÛ†cK„ÔÜýH¢>Qì"^Ý,XÇȖ~ÿ[ ÷â‘ÑÔ«Gï—/ò[3†Ëm<¡IS?º ‡+R|+¾폽ú¯e3Æ¡½úC½zÇê@ºÌ€M7ôx÷cC9µAç_3ÎRÉ]ÃÊÃ}üò¦?ºˆD9“^zΙqü:s2k½¸|œ9½†¨´x!{hԕ_‡iû7ƒ-wýsÅõ»zz¼S R÷-²ma½üDy~½E
Y:q‰gWýURDÏjr¥ú>ÏüP²¨!ê=Mº_¦æŒk„ÏצÏçØmx ,17Jds!¥žá¨mòmƒý}ƒÉÍêŠÙzZ‚ïƒTòŸ%‘¼ œó—õ‡Ó”µÁQ†9Ôõ= §ò‚UB~Üáìsæi_.zWb$fc+îÏ$vD3ôì i3€u z?ªD/9k¹.r¬ó)£ê‘ðY8üš¸£"BÆÁ‹£˜Ô¹Ëd¬jhôc¬¡GÐ×5u³Ð5è\é4-››ž–ÊsàlT2”ºá¥øÉÜ6n6°˜Þbpw !×^ßdžeÛ,cg rääÃq’È·1¤Å^™o8“®â¦dæWÂßTÎaF1sºíD×ø’Ÿ¯
’†Z"Ü/èœïúÝ aè}Ååãòl‚êÉAîÄ<ó˜ª}V=M“bÏ>Oɋl¥+ì'U¢ø¨Ѻ?ùZ¦ï³»ºÛI©³+Èuó҅›ÿ1,ܖ머Ç
Y=}Dµ”¤ç:¥áTdL÷ϠG4¨‘Q¾îm1”ü-‹ôîûYV=}æñoF§É·mO„¯¯tÞx= ¥(…ô‰Ù‡¸ ðXå©Ü5‡XF!mã^½……³’'5ÁïF×¼å×eÂc­kyß<QÞ,hòo·âªãÁêº4 :¬kçÑÖñ•™ã‚NP»QÑ Xy°( ¯°•ºmö8tb[°‚–…rH]˜=}d›^ؘKäà=M §¾”Ûäˆ~ŽRîûYpã¥A–©ð©qpµî 3 (bèÜÿÜùˆêfÝÌ®F°I0H—ê²'/eéàÞ,Q†“:À)#Èi=}$\Ê\ÿù]̛<ÒþMÆ¢$¤ü˜o1X¿÷9ÇHLlÑkÑÌØª6uG>²Ï–äT‚ÝñvŒÔÀwRa]÷pxZì¹ð>ëꈤ÷ü= ÌJvÚ¬Ø AŽÄáŸÝfçë]„þúœ·ÚÇVêym3pÊÚÀDzÇ=}U"¾îÇÐhŠP8ˉ¹ž r‰8_Œû‡L.Տ”ìÑ^ªŒÐ% ¹˜òŠì¥¿4‚L[ùˆ“ɍcÛí8Ä@Ðhå1Hå™ãYf¡hê•6&‘ÈÔ¹~åAÇÿ‘ì™ï–­½,iu¸„+{
Û0Çóæ8ÕãѨz3Žz?Nýhg1ç^7ȗ·= ž\ؒw‘a—V5QÅ¥Fö›ÒÂÇ©’•Q'³%cogžßgÓºÂò8_uÚ…ó.ǹS¥©†Š*¬h´ƒþ¯™¬€‘²v2z-Æw &[”ÿbIû^>;?ùΫ«ˆnTj‡ÅJÑQëóÈãÖ_öÔHeÞ®Ëtǂ¤S‘—žpnáøó¿ÌuµØ~‘YPLˆ½y4æÑ;!:«¸-ô"=}ƒÒˆ±˜§»X¨™‘Ýñ¡Ì§>ŸUâ>•Y㎜¾"çaFfe´·‹6e.Ã$NɌÔÃ_ü÷ÛßË:ücóŸ'¬ð[øé.‚àú‘/hè#Nyq*¢½ûÀá̓˜ž¦Bq!9š…;ögMgùÊ«®„ºÊ$£ûÌ̤ŸK’[Ïnv(ó„$±ã‚âÉÏ-
Œú{ÂÅ,<ÊT8ö¤Œê}æõq¤!aü:ʨ†Ôû\¹I}ëT5Æ\Ïiÿ0QEïµ*€ñK’Œh7b¾gæ'ÐÂdƒ–[§ˆhsYàm£¹KP®pýîöñ輸[u/Ú¸= Y6 ÀYu÷²Ö™ŸÖ]r[7„bmj¨'æjòà†’lMO* nj·¤ñ1(*TىZûÇIîiÁ„þ’.ÖûS6Õb6yYìÒ$©iêã‡â{Š9DÖ[™0t *4šÿÐzº0ð*µÝJ7НRbšå‘§ ‚G'Ká
è2xêy­.êä‘:kñnËÞ8ÖúuŠqhÛâ|èä(Æ5õ}v÷ƒêÍFŒ=MîX鳓mî´ü|pî´ÌõÆÅµÔ÷)jžDo…L¾6üc€Ö•tÞÔ,Æ3 eð_—‹Äšϊ.‰3ˆ,VöHDNå@ÔI؅hÈ1¾¸Šô"†§wt]CˆŽàñìßï÷\kò†§Ëkž¡—áŽÛØ<vÐJÞQi[ƒǢNª¹˜á¥¹$­t¹Ða‰&Á_r5×hzM8%
‘Îf93é.¸E¿ÒZ_=}̯ܛµ±œüˆù×fó ùž2½NQXE‰‰œÿZ ìt«î9’^z?êfô0Hf4†ê‰†êfá¤>P^^'­Öâh䪽œ
à§÷Ê‘Z4t¯.®'c™Î÷þ!%n/ =}¤‚=}}Ú²SpŠêw½ËÖPçT¤–õßª—›U‹,2õVuPT8ÌÍ!YVÕ,,]^éyào7=M™RÇt]= ….&ùdDÒh­P\Œã¨›-FRÆx3´¿¥ûOªÄ:{·ª7A=}ÛEx¥=}VîAOúõL*þ¥ÑÚ¬,t .²ÑO·(—“ùqò×Q1 Iì(é¡U©ª_L ÷¼Ü j–mªÑm_{ŒÞQQõÔ}óköJ%¡ÚL£²ÆÓ uŠSU#zÓ*§ºl8BÐ"@b3ôÚzbÛvÈdӟÌg&á­ê†ühSÍw3È[©tÄn*\2¢ƒ¦à
€v6ßyŠÜ0Íw)à×莚S{Ñ.Ò¦Íæ ÙbµîÑ<e„á_›ñB.½®u÷\'&nRTx)= Í=}-ˆûô¥Í.>€éô5) 'ý¨êëšçõª·õžJ+•mvä¯1= †9×µÄúb3®:†OaÌw†’;±QãJt= .÷g)–^=}%9Òwq9D>èÅí‚:9º_À$˜ÒÁ{cŸžÅ?°Nzí’ø~ÊY8Ä)#ˆ2b°µ#¢ix3ñȆiæ9“z¨ÿJæ“äë%<êR MÑÄQ†Ö2Ô–½aôcŠ5}t5ìø#Vñb Iò‚†j&=MÊwd!
–é7™[ÀÚU*l6¸ïÙ©¡NIn%ä Ì%Gù‘Ñ6¨ uo•·6Å)=}PíÎîT=M쪇¤){èÐSX&P2¿Ü ÍvHÎâÁ3ÇÌð¹õ•-„êÂâ©s¥‚Ÿ¾¨ò-õ£seø«OۍˆÝ·ÔnW@e¤l4¯ ÏÞ¾Qj’AޕiòÈë,òPGPñ¾ŒwfQdSu^ïÿ+‡l©„ª£¡Eõ£){æ ìððõ⡌ÿ¬ Ôé
£“’p™œ¾lø…w³”= ¡Km=}û5HàI§Ò–%S5x÷•i$¡;ÈwßôRxfaðZj&—h6æ‘é+4²èþ"ñ­Ð
ú¤ UÚª–ô‰Æœß ‹Ø°†;¯Ê¹¸ùʧÈÖßoÑZ!ޒôÙÙʙ˜Ã¾#·/ÙÀ#P±/·/}¯1š®¸zÉöóýaa’â%
â]
÷^ïCÙã¥þ]÷FvȊ­ÅHÒJŽ÷†Ùñs¦ÖGúìtp»²Xæßñ 3ÞFЉ}ÃéD½~ÚkíŽ]æd®ã±êµ×èÑèq1%¹ªÓåˆÌǐ-ù˜µ“Fø•I´ðìeò3ëí
#1ñ)ì”)žÞ÷ÔÊ0­ÐÊ­ÈÊЭØÊÝ÷D"¢×ûƒ>öÑ X›>>>éÍ5u²m¦œÌdlÏg=MFSôS…QÄ@ט¬Gó^Ş£y©ZIdÉܟÌý—wË}E‡¬G0Tò0RñSòÜsÁ¡¬ßâ= É÷Ù¨+­•ÒŸ_)ƒ•Mv«<§•(ÁQbùÀÇ=}Þk¨jlŠ¿ÐŸ±ðØ÷ÙjÕ²"éñžpÑ¤Š“ûر¸Ø1\’†âÙ= û ÷áùTn¯8 1˜¢:«çùdõ¦ÖÝ'dd1&"†w2u„‚ù“†$Ì$Ì%üžÕÉïÁᐝ~’E£ÄâŤÆÂ€Ú¤JÙ©¡¤î= Wxòý%=}QÚÄ­šöªÖþošpgÇ Xr*—ôMR…#ìmÕ§Íœ‡˜õp-¥ÆÑaòÿL¼šÿÑ­͖±P‘3;‘Â^šW])³Ï ã™þm¨8e»êšÈÓ=MJ­„šUðPmr ¹-=}® Äy H LúRX(,Ð:‚Ž= Õ^',ÖnÂj—ØÜiPW2(ŒÒ7©<:™4Ê:2N¸ÍÊ8²Ï.ýI4·I†"k¦$ô‘q{š øâ¹f „ñež1hªQ¼[_é2Nƒ™È´jQˆ°*YÉîÍϾÿÑ
Ív@ØiúŒ!zڛ.>ÔZ³b]@ëúž“²çÝ+‡ ÑFPž†¹»áEZ¥›4œÈJ- Û£-ºzUØÛq3= Å´Ôw© P±-ÊQÊfYcÊ×Á_~%*©{غ)àŽÀ҇÷„ø'Öd¯Ý²Yíì»Ço!LÍ9ËË$+À5)„OŸ½È¯¡Éz؍ŽÊqAaûԊ“4IæÊxâ&[CÊ.¥iÂw×9iWœŽ°ÀC¸(^'‚©‚ÇÊTºL±uvý‘ÀQÉënðÈ¿œ·ö!CvH²’ÇxÎÚÉ 5ÒŸÃyôÿúÍŎ-
†>ÉÿHìæ U4Ό¯þ§½ûmÁȔÁÈ&mG”x¹¨‘¾Tc²Ø
ªŠ|ð(JK¡+BN©ÅÝácÕ®ŽO©Å݈Œöd}´sãЩ~Ýî#{µ/ÅÚM8özÂ)$WÜbªÀß÷Ùϲ‰$Ì4Tú2Ö©Ÿqƒ¥VµCÙ%<PÓHùQ^h ?•ܳó‡Þ38_Ó:Mu´[‚@ ®œ2 –È,q~*:C”Œâ©µŠÏi¡Ø…b´_õRàag÷†֣á=MI…Ã(\|MdX^¥–dÞH¿¯¨®ÎEWöroODlل£¥²‘r±í”òq%@ˌ}\âÇi"PÖ ›àÂôÆß®YZ¹£Î ÞLð'7}=}:‹¢ÿÊ$¬­¥’ßÐ:£v|Ÿ M,¯ÚE¹“y5§ôŸLšóx*Ó¶¢’—…•ð¢©½é®#;8@ðn5;˜…UŒ‡I¾…w$"-ÎÁuµíƒ±ÜÚ.Jû2¦Ó™Bì+²¢˜!Qȹi¤†Öù‹•¿ìà ÿ¹·QlBa!¤kh)ý‹¹Rª ¿}I2gԇ×BԯϬª/ @õ¦*;iÂØëފpݗ= ˜ÝÃd$= ?¦OržQИŸ½Ì"x¤ÙR ²fòW*s8|¼¸2‹†Ô ‹õ †ÂcÔQ´µªôÚ<…ã6Ñ?{Âj®Rj(ÏKáW뀳½Óې[¢"ka喠k¦Ðwï…­±bëýF¦ÞàYvôr€H¨Z>èç/‰Å”¿ËTtºã#:}@÷Žæ(2Ö}væ}” ÑØI‰xÕ¨N”ÏxúÿIMcŽÒå—wšt‡J»Ç¢Ð>Q{‡Ôòh.ph.ù¡]ƼÔ¹&•¯áMÐy]­LÌÆ¥µÿ³·á*ùPŽwãSÛ|">4·[U8
I‘®ð,õžï4\\A ÿb‰5+]Q—RL&5ãh£\”ÆE!ƒs1Ïı+2pӆôύ9֌R˜¡m)5!^PqÏRf%\€hR'„ñ›«òSßî˓††ÍH3íÿÌ'RݯþÙJñŠÝ©–\\PVQÞ{e‡wjÍî=}ƒMå uçbÕõ·Ð?˜Âߙmìš\¡2΄‚d©E•G¥G= Ê}-ƒÙw¸R ¢zy(Ï¡Xê¿q½ö±»ýø6›v›ÞÄ7«¨¹Ü Ø=M†Õh •{çÓ]ñ1)%Ðj£TNˆqƒõœ~r¥à×õð耥®x¹BûÛèbÍ¿–‚õ-Ò¹Òæ\¤_BAý‡Q±¨4²”|×>9ŸWP®&ÊëMèØ&{ÑîGb¢è¦=ME…èÀeî¯a=Máә€!jvñ‘#ÝÔ¦–D¨¯|;ÈRRj"éFÅ{1ÿÏr< ŽÑÄwöžå¾zÈù’Çy© ­13'&7x.uzØ"
£JßóÀ@ƒ ù~TMS°@Ù\ê= ‚óe%Ñâ§mþ÷fRôAäêtjõ3I–á‹rˆ4&¤×ùÍÜ´¼Ý|Õ¥ñ³˜çBäœ!vØéX˜x˜Õ²§DˑR<˜€=}~aXuä·%Ì Ðñ—+µ‰Á–º
%*g(X’ⱓ3÷ñX0ËOÊGË>Å=Md[[hg®1·D!v&/íjoô®bü= T?iBÔØ= ªX+óq6Eôyˆ‹ã a·–ÂXI¡—ª“bp¬š{•q¦-¤É­õ¢ÀÕgÚCD®±±¶ÑNŽ™?fÇ¢Ò —žlÙ­˜ûnHê¸!zþŠT”Š”hl¿VÖY5*Tº=M-füfè/”4;)
«€é‚áh»¯Ü’dFÎrhÈûåžJSùáÎW¾’^7<gøò¬
ÌT{Ž(êw€q¥â~-ß§“O yú^’yåæ;íšÎˆûPyÉ?÷øÆñʖµâþu¸|pa×ÅÐøj&
>‰c*Ò¯1ˆ½xÈ«µwSîÃðIðO3XˀKÉ(tì66þ<çÊñ2J /bÝ}täëåq¦¸â$à’±™­G…SC°Þý§îá.èKëí
‚a„/–Z¸ðWöI¦(|R)dzÖL²,¦Þë™J¡±âM ô{ֈ㈨Œñµ«¯Î'~7,[‡‘¸ˆÌ,:Ø'zÊ9WÊlœLA—Ä›ÊK/x¼<Æí·1äÕ:½Ö¦ }ȗ¦qÑûŸÚá~†p.
ž
-Ù0ùýÈ= H3Âm°nOzªxŸB 4$o÷·V\…nÐà‘+“wÅV¬–® âhþ šä t5Nþt*6FΧû¿¿}¾Ë\¥™^igi×/._'*öÃq=}Ì=M½£á9 ?LÕÓ&­s”Ä& Ø>:Áê7žWP7'z4üºÓ‹.¦0àr"˗Nͨ °Yñ!Uñ€¤¤Óƒ&¤0 ;ˆäL= ÝD¨ŒKÃ÷Ò¿ëYžF‡Šb#2¹ôñ¸ÑÑxšì¾[/Ë·Ø¡ßyƒ9? 7s*‘Ž$‚OaìÃ=}ºñËÃàφ¿ZCəà×å…}ÆÏ³ŽÃyÈ"¦ø7ã L1 äÛqhžlfó=MȨÐZ^sfú싹ž‘9ÈLsrÂÝå|›.Á%Jðó•×zƂZŠ™C÷òK‰IÉÕkd
Ü„¹ Ú·/v’Í6²å®v±i·4M0ñ•d*-ωÿFËÞ,X,K†îºi´å÷¹) Z6±ÊlãÙ, -G¡Y$ÒŠ+†•þÒ"‡ Y8 /©txÙ $”îæ±
ݱZîÊ'bG0}av1„¶µy©"²'ä0ô-^1‡Äl“yž"â(-7p轕þ<Õk5}Jބ¦ýé":­R톲g:»…®ðuv²Io¥ÇUÝ{èÐ!Õ"fl.šW²½'š>4ŠN¨cóxí *ÃÅöèžì¼„qi‰ï hò7Ÿ·UÚ£«Ü…™ ¬g$ÝèŒõÞ¨îÜ1cÌ(ßçÉå^cãÅ- %Q>ø$Î k†Kœÿß·µ‚Øæ¬:è¼Ãƒyz…µ.R­wÃkÈ Ú,Ñ;Õ¸»^O¥' 
ræï½|Ð:¯7Ú=}|ù¦õǃk'ð¯÷0ŀ·ÝŠêwµ˜²xï.WMyH¡Æ=M
څÏò©ì/3_˜šÖ=M ²ü|a÷²=MÄÙWe©ÏäN™¹?ï×Ñ^W*s€zý~†N‚櫤Ôèímܸ«óŸä³›Ä´q9òKKZ³Û]ÂMÞеuڝÓOôxIäõ„x¼H&D'#È8Æö¥ÃrɄi ª°ÅÎYGy ñÃÐN†X۳ٗªO2¨X„¦Y=MÓ"¦&Æ' Zô2’¸IÓÃw6-®›ÐŽ€¯SïÉ÷ŠÀ»-Q9ÅÛ¡TV\å>CÇØ¡òo[+lžÞéQnU¨k®ïn ³_Λ>ø 9ß-§¼?A(QD]>ýßOɪÝ:ï´hy'„ÍRÔežL ‚Ðĺ”.jÚ-j̺¤4Қ(põ§°7­>‡ˆ­·G|wÙP¦êyÉ[YÚ/›Þ5?½³wòžZ= V}Äú;5l>û(ôûíy ¬Ú}bT2W lI7&Ò´j~®ÇvbñÁskóJ ‰@æޜÚÀ"ÿ±
՚›™˜±6kVñòšòpù¸ÅªTyò½ÄE&ƒÞë^“·U—­I¼HÍeÈÝ'œˆ©L½8"ýÓTƓ#…Pº‰€n…C6ÉÏàWíPšÜ}9ÅŕhoÃ'ÃR9«“žx˜®]î*§__G¦TŽtòÛ×ΕJ}Þ(QjÈü³å /œìúJ®ó·ÿy'N"ÂÚVô/%7IEô@äµ,»jlg’~=}UQÃÌÉÁ dh‘È\SPbTzÒUUÔq‰øÖ±ÈÌGÕY.N ¾h=}ÔãiÆw{ÈèLºÒ;·$—­k¥ç<”p‹®×
*¥2ÕÁbÆeý‘£³å–¢ØRŔRwîé'øa&a-þ*Hlba”iç%¦/EÐX33"ݳêé|hÈp-ÆVÊ7= fþ‹j<ÚgG°¹ Êp8ÆAºšù ؼó–U7€S7hH9]G9ÑAºLAº†¾ú êB3HBºSrW7s›¬ÞüšnÏ}é얷ïQA:Q,ÑAzPfô3cvü–€žø ¥Zð>ßüfÇhÅ7 1¯ÃOEã®»gat|¸ @c¼âp³Qû¤ºßA*Næù i¸O²œ
¬ÒQP{{ˆ×ºÅ¤ê3TðÒAó@U§”â¤éóä+ƒØÞ?Ò×â0TØq©ùƘÁ^úÜùëw©g-ÜÀ\¦ùý¸K¾4Nï×ÿ.>Oª²\Åm>Â,Ò"·žÇK™U3ù² $4Öùà ÐäO:»Šȕ<ԑøƒæU=}ĨJŒ¥:€¶¨éŸÚêòϊ§”àÓÆx¸˜íˆ0ËÜܱèüx"R¹÷l±Im°¾œ•3w€AŒ­ðœÄ(îB·2‚†)7d×ÖÄê71÷·[çW.©ShêÞü6®”àù°ŸíNVRŠ‹-0S5"aÄP*{ 3[9 vcXî= ¶l›J±øÕ¥4CqèÈ´KõŒæk >âæiÚæo'ÇóӍÆö 3!7ìŒË¢Ö/õŽïæâC²,ZÝ')í)ŸŸ¹Þ!iöFÆð¹>ŽïþTTwjd–æ“äsºRøŒœ ÝI9”EyFEÜߋ¼X?«Žd Nö&gʙ6„/œ ÝÁA:ÂR§5Œm1¨|ÊјO ë»±´=}3y½>i )JFߥöÐ!í(¸×VcØS,£àe]ÅÛÓi¿cZäÀcHò«¹µc´…ª“y¾/÷Ò18Õ÷¯ìHyñ‘²Öð•ëä"ÙÈdBt 8±YªJ2Ûx6×JÈréîe)^#!¢ê;-WY„û9‹ÚS™ vM¹“Ù^4<Y„ÌS‘¢ 6›wšÙh}ÿn’!c ô‘N*5Œl~A¿â’¨^2¾-2~ÂcçØù-£ò)¸Ë…ƁªºtM~á*#xbÄõËD{ºöüuòªž19NLO)‚LªöçªûYP‡ô8z:µ4ǁꎺºï¥L}抌J×U¹†UJO( uóyx!PÍ<"= ô j–·ÆèÆé¤0É©òT©‚Ÿ¥ùÿ¡Th-íÓ½•3Eó19O[©ÕWš«IoTø~ÊCN;Ò <X 
8NIY$'ÕJ¢¡Ô·¶’¨î2.b8¯²X^ډØJØïPþ6ø,j‡})—S¬ ƒD­CÓr¿ÈŠXYìt‹žï†¯³§[r'÷tªìbWŒÊà|hÍj6;æ•#1Ä2à
Ϩ]ºÄturUÀ<xdS§iÖÑÿW´ÆHð×'Åæßpº<˜}„sæjñ× jþŒ0LÔ ì¢[$;3‚_KT%‹'U침®ªðv!Ÿß*þÒY§¯¾ïÆ·¼²·‚ä‚= <Ÿ2üF Ú¸
´ñÝÀ—úÇɞTãN>ås&j•uZ ´­¼‘3‡€Rƒø“Á&~ë\rÑPÊç'iIc €šY{ªþ]»âæãZÐÂ+~ðZpj ZþÃ×ûÁ›7îßpräÆçýÀûqEâ¾,éV¬t—û³¦.ø
¼æqÆQÆ—ö‘¸Ó¢°Ü‹†bKmjgøÃÖ×®²T,fIZa
]Õë´U»°F°Ý³ÑTw<hҎühÇ üE’¯ÐM˜ë> †É³Tä©lW4QJÂs©r'©>™ <e Ò¾Ys¨£¹@¾fȞƒýíZŸ1¡DØê£Ù~Ò>ÞÅòñ›2aÆâ2@gÕù‰ñdªÉƒ’#¿.ÙN ëÎr:·Û-tŒÉtÜ®½¯”qµŒƒ=Mû|…èÏï±Ù}<L —%†?g xÐÂf‚ÈM¦Nî~ïîð 
Ó^iÙÈÀ­œK÷R˜gЉÛÅ%šÈÙ¨Þ¢×Ëp¡^gZ·tޏã|­z£*Ø*2£Æ1ø+-[™£çR°bb§ž8«šsê,f­µ\±aœߗ=}Øèݜ™(ÜÈkӅà Ô>¦½Ø©”³ZÒÐ2ˆu‚˜AjXø1øLDâU“¼‘,gFV¿—㈪–ƒèíB-ÞsXw­Nc³”™RI+ Xjr<ä-ëèn‡ÑÚå©|ˆÃi!K1yH{ob iµ)VÏR¾ çh¢Ë÷ޞ¨vg §±™<EøÅ–¥Þ¿/½=M³,vá ©ù†4ûÅoÑÒѪ~&3]i霪@„óÃÓKíÈí厲.e,ÈþèJµôز'|bò6p¾øeKß(ð¥ÖnÄÝhóîú‚¨& ù•Ùî„xE8ó‘¢vÛÈñøMl¥Û|Z¶Œ@Õp‚žÿËϐåˆÑS€ÇP²¶­Ï³hõº˜Ü!n¨kÞ݆m7áü²ãüFrðýâ.s&ò_åì$=}U@•ãœ$üD%ZJfo=}ó“´‘(c׊}^a†¥Æ·ʟâ'Bú5
ª}"mù¼Ñü?W®’ÑCh‹Í©"݀Àù¦ÇEgIË´Äю®ðRÕfËøA+ö© ø‡P‹\GTuà-yAQä?=MÇì²Ð®$T~nšÐ¢òG¤õph0¤Qs¹ùœ­”1®öD ý-–ÝZÇ|陝g®–<pp ÌE„ÑÖÛ|AŒ
>Ö>·Aäނ¡ï DïìoàÈ= ·N¨¯BÆïìo‚Vìá@õvÖMAã@&æÉÀABL'?ʙ¾š)à¢Ñ
+™oëR¡ÌÑFŠ«nv5°ÏÙ%åiùEqÛëXã÷†míQ N1Xä!Hc
"°tt­bïØ~Å>)qcÔÒ­)Òqv}ßÈýÈQõç¡.’”¼º|џÿï¦@Ðo®uØ×½|:}ÐŽpܒ±·ÿ֞·Fy£(†…Uî¶K;¦3>€Y@Á•è›SYÀtۓQèyM‰©>ÕÁ֞%ù¨÷¼J^ ¯ôtBíü³m¡V! ûÚ/QŠA ¿¼&Ñ/R¿qWœ.Û¬¼ñËÊhTÁºå{†Ãâha]NÈÆPÙ9;bKŒwF[„€ñóƒ+²æe²ÊWe…Ì^2¡ã>1‰vÛÅé¶x Ø÷5ƒ¸©ð„i=MiMH!UQ5çŸR/ EÅÖm³Úöš¬ûƒF¤ÑZjå[ÌrP=M™x Ȯã—ÄO/ÕÈd ¢Ø4Wì=}‰·'~T "Ô°'ûóu‘3 •ý îçž&5MÉQ[7s¾¾¥©c"cë­„¼'lÕà{î¦ÿVÙè“OÈ
YAM%£r¼Oªœ6TEú ?!©›ò\Ký©1d¦ÀÙ;Pª’>kdÇjmQ=MßRD>_®ðŠ/by©§–†9ËM¸ „‰pð';hõîêe;R¤¦ÌŒù³ô @—ãÛö‡a'Ã%´#= ¡¾g»ê°+9ߗ#,³è~>1[ð@÷X eñ¸ÑšÊ+>ý»—ÃF;[aÿâ÷™wKVÍÚmÌ
? ô!¯Údiø—<f[2Ìz\Y8›RÛ尉CW§FÆ%â3ìÀY-lòÓEðÙ_²ÞW{¸£ ®(_4!€WXx_0cŠHüÜœ|ÔÑG¦BKÑrwî?•¦'Ô Ä<­Q[YE€ý'ÝWÛæ8T¯ kÓ©€Œz- …Cîr˜y­´OÈч”‡iù$I‰‰,ò,Þ¬Eà¤ÏÁ‰{ Ù׎ œ¼Lá ÎàæìÝÕ¾ã XòF0ã-¤·rê!nu[jZ"ޒ¹ûq[˜cJkÏÑì‡ö14kˆòo«Ûz䩁u¢2Uúu}!º©ñ6øx¯‚—ii'GBä«XÈp7› )D'׿Wm2[)’íë8éª5DcNçZ),‡,ðš+»žŸ­6ºÂìßÇ:Áâ¸Ù3ZÀ+¡³PY\®’ßiŽ?ns–(Dõ¶ÈŠÇÁºëO •Ôg]eLÓåW‡¸1Ï",{4e­æÑ_ÄöoÅ<7?rLè‹Â±Áç&k=}Î%L …Λ¡)Q¦[õ·E§k \Þ,Rø§§ Ä×åJƒ‰† þ ALʭÞð¬Š7Iã|ˆ’X¢žI=}¬#Vúo<µ;ג²®w÷ºU")LíîÈÂNúE‰nMVŠ5P³IH,6~)Øeœÿ0¶¬= 9´MjR\þõ˚ÓMVã¹I%-ªcöÇ.J‚ŠJȃ•Îè=Mä ÃõV$õ1¿H+å<Tz?[«bú{·>µ=MÄHÊAN¦É{cðþlÖÑ-3۔u¶šPö6p1Y9ŸU‚ÊöL„­£¶¿l3ŒÇ²Ãð{¼‘•Ê”'h1yZͯ8ƒú>əJH’¨If~«þy3³ð½Àj•þ= Á«t;FÊ^ R”³²Šc#“ðÓq%ˑ ±Ž¦½—çcÀÕòkwé{é˜ÿM“èŠfQ ŠßßD¥œð¶ÎSË>"ÿmå(†<…çó£âüï·B-]²ãñ€ÜGþ¾R”FÃbg®”Ý.ì§ÎN\y©½ŽöœZshàw0ønÄ¥Xßù”’ F³™Ñ´‰Â¥¸µ!ƒt\=MIÔ±:S
†¯E•*t9|§ä‘—Å›6äÃTï9(×ݫ՛ôŒËWÉ©òó#í7¦â‹HoëÚUfðd0·hWÓc0¨\0Ë%#°´¾½f?[´Öòp‚ŒÕ-¿HuH',œ4\âæ/=M]=}yÌâWiû>žlõÂÆq+IñA= =}5ëïx©ŸÆI $Cùó8Ÿ{’Ñ·o8õJ^¼+–—è"Ï\ëli)î2»B¢Ã tÁ=Mu/g~k ‚ŒY"Þü=}à42YÑ콌ÅÈ9’$sS@7ëHë…¾lQÆ
ë¦舿•ÁÒeÇ£'»ïïZ4ЁzsñNŽùá·ôá‰= e˸¬f&‘à‚ÔD‡IÏÂ!6[µ¨›Öýˆù«j+ad‰š´_R6ÂÏóâÏíð'”záaXƒD<Ç
¨Çä ni?P-oP桬-ÀÝóïM/Д‰-¸½Åã<nyµ WL(‘H«3õ<4•!:9Ù²©Öh+!8N¯ûÅ(zæ9‚B*úŒ¦oL´V^ÿ;öKåSëÁÙRÓE²«W_x7kÒß8 ð!ƪ› A[ Qřôy²Ù°wyf=}4µ'g~L´#ÃÕ÷5ÈÛ#œ™3÷1¬C= J>蓒GLÂ=MîtºôtRÏ?Ùô—yŽê1?´u
î7&­0L÷z†ãE¼.ʹN6)6?sG=M¹?BœE.WÕ³!çÃÓ„
zÎä}"ú3Ö¢/µIê"þøÍ¢DZ (ÕÿIõ҈ٕ TIzL0fõjûNHÛû]=M£WÐÓr@G™úi<ò,¾8b™o µ¸ê ÐN؀9ޝ.eáVHˆ^cxùê<÷p“Ƽ…&ÆmQ6þý¥XÖuE»Ö&´Ù<¿>Aö4K‘Ÿˆ†®%ƒÇäwi‚õ‡“/#oF¦5B02’4¾ÞCYZ–©eYêgŒðÂú‚³GŽ€œ–[¼°þUõIkòŠsø0$­zY6ŠÒÑo<ñoå&“&›w÷Œ*Px4H<ƒÁšë rº?¯€WìW®×î3ˆ¤öÚ6•Ét´WÕ\.ô<¨ïmÑddòiV³1;JU&æžI4A5
9Ùz.ÖÊ¿>9ÂgEb§¦Vžžá>¯ËE»£ÀÉû•ŽÃ%TY%yrßMÁæ÷%n SÑZ "¯Êš×H}Oþãmx„9:M?+ ÃpY¦*þÅ2³…æÏ«à—jð7Ó6T³Ó6Jœ“9Ü óÚþT2nꎌ€²¾·< ÆÛ!÷+æz·¿ä¡=Mô̘pPýØUb–÷_â÷RãmŒ†Î51Êï}·?þ³ógk'X//³Aþ0ËwfÌò›Yó[èu!áôÓ3$dY[¿Í)¯g$‰ÂåÈ7íHE¬ún~ÇÊB'y&LãëÅN¼ãс>¹µÓžÄ¤…50ì$’˜’9œî&!ò8mÌh¹Yº K/wºyÿÃ6(„— &<Œ {¥á¹«?'ç(î£ÝŠf(þ§ÝÒwD¬]sãµD—gÑEŒ¢x¬{C[Zý>>VvÆåLH€×’ÝîųgڒëÉÊX랧‹Ú?¸ƒ)Esuy¥Êu
Ä~žÙå8[ÈÛÏÆÕ>\È´©™@âñXÇGN%@ÝOþW™ÛlÇ ìu½äöÙ:ø–Y瞧pŠÜãr¬®2(‹¬Ú½õâê‚G“×—{ÈcxQ«vºx©Oÿ%ÙÁˆÆõfÈäL>ZUÀJÃЗ$µ=MHÒ¨Ñ(Én7§4:¹²Óñg͵…_Mý'^½Ð}=M\¦„E”³-}ÉYâ£wž¹—ɇâGY\‹½«]‚‘†=M­
çªrŸò¬&ÐÍ੗îórÁ€Aìà֊P€ ¨Ý°ÔìÑîa´¸Ïë¹è{¢°µ¥Þ2x ‹æô²â‚ú-~êì‘íXÊ%ÕÂv
ô™~)hbüXµ¨Õ4~ìÑ_ÜIϦž 닣Œ"¤—$#ç=}ø5( yW1N’a.Ðêq)àüfхüpà½fæ5®y\ = ú/+ƒ
Ï8«”âPŒ
ê\Ž.°û‚•é+<¦³™ºcËJš@Ò0“žˆÊ°zµ$&“EÄÁϘ2(³ ™dö©´kèqTßù*™ÆÊ°ó…ہeÅk»)p;§UyJ¨ëž¦Yò€OTr›E÷%´eTr›ET¯Ѧ0aZˆÞàò¸ãl²ž¨%Îñè‚4<Eˆ¢X‹Äëf¡Lð}ó;Ì}= Þ} ÛËðQZÏLÒ¨!@Ȧîö¤Õ5oöüoöP â·\îÅ¢”<®w}fÿ¼T¨˜T¨ÛÇ­Æà÷öÂÕïh–vPöG’6ì PP-nðþ$ôpŠû_~=MëNXØínîeÛÞµ[mª˜4ùG€Šl»@i5ï9¾lX© ëîN«6G0´÷#J¸å×ï…>RoîPî„cV¡C.°2Olèo9 ¡üGü)g€ëâ[¥=MÝÝÒ£“â†)îr|ŒtSZK†ÀpX"fìà§»¬= HUmÍÔlôa››2Ví9ŒY§¬k±W¡ñÍjéñZ¹¥Ê«ú\ɆL)zfBÞ<_^€“>œ‰Ÿ¬°p¥2= ûì„ÀàÁð„uäe«¼\{&;Ǚ'v•¢»†óÄ_¾¸—ÐAÕ%H<J¡Nß?߉j•Ž= ¢©'a ɰi>• ;uËD¸XCï¦$;³w“DÑRùJ®ýà/Pä%²ðš‡~¾à§k)ijr$R:ŠY®¹xv6ªÖJ&Cրç}È´ÝÖ\×?¿Þt¿ð¨×´€]kôCÍnaØýš¹õúäøt8à„v{ln3û« ÓÇmƒË½×µ¡zü7e’ÏøVÙb†ø@FE§,‡î9 }~T)ɪ¸C•·-¸?zR€f;êÈaŸgăî߯m7JòË3aJˆÒúã¢9ˆ¹I¿‚fWÇçŒ{%¨ÐÌI¡1Y¯Â
½–Ý:÷M²ipšÌ²õùÇqgܖ5«w¯hI.[+¬Å£Ôá–j¯Ï´^8Oµ­øþÒ¨qÜ? X÷o5é ¢H¤èò¨@¹Ú2žPY5¯DèFþRwyŠ(Fg58ÿ'†+ DÂhÏ|†I¨EÛcD¤J'K
ËBðö:ò= ð,¼=M,yí¤l£bí—t–‘ìpE,›óÑ¿3œÄ$Hšr§^+5ÖóP¬0_‹V®F¦=}}ä3yЏñé%HíÓHÊ;陠Ä}›<?zêÀòp1W§0ÆuáãE¬š/E¦ÒZ8òÒêÏÚû„ïöí°¹˜‚!º6äBÔ:I*x5K,5(ØjzÚºP~q
\\=MçEW^‰³æû;)š!*ÊÁÍ¥8(7 š8ævuüÿIסÞEeo OôCúÞ¹wŒp£t-¨YŽÉљ}ˆ¾ÁJ ç¦B¸´¦þ-â5hô ëG
†RgêUjëG¼CÎ0šíǑ‚˜NUjPä-:5ړý,Žoáé-¼ðr]
ñ|â39&9¨oŒe¼“qôä nñ¯‘Ë$reú¼æ×Ó<Nˆé ^Mŀ¨Ð~Nët¥/Shœ6š‘&«}7©‡ÃÔñl­ÙZÞè½Ð¹·ãU§'Ĭ¡ÃŽ>²®Q4!iÀéc¬ø—{ËtÔU3— 7-_ì,?UÎì ;p¿ž;PÖ»•õ'šcî™Ñj1çjÓ
«ÔŠ”%àõo þvìÙ!íçsŽÌ˜í" >ËfÎDL(D\ÏCcð"‹…Wà±{d¾]³FsƒHÄ´wý6= "ÛèÌOÞ'‡›Xñ,½Øä¿±QÆB ×ƒŒ„©;‚ËÏ—ã®ÞY‡ÍŠJóUýí ~t›©V–‡~ÒlÔ{Ù(¡Dk¨!§Rr‰N®X3ªÏE/;¾3bH=}‘D¸ÿUùh›ˆ¾»‚nO@äDø–•·ÎsËÒ ýV×ׁ/n-‡<¢ÝxTcɚižªm¬Nn—äÑì–Äò¿{®OÁ2¦w0cmwˆ§Jaq\c¾ð֓AQÿíÁs°N¦ôg€v2QüûAÆì‹úÑˆÕ8{æ%!¿Œ1¦æ‚±„&ßғp±„¶ÞD3ǪtV5'Ñd•/×ëÝH°5Þ.Ï ¡È®,­ÿ}þîphËãý7Ú7"ù=}ýûö\‹¡‘p;5ftñQŸ¦¯WyÖ&$Á>GÂU—®µº•Âåqöót¹˜7ÃïçZ–ñ67o@Ä:@¼™­N6­eñÀµ,ðË3= ë¾p¾+m,³Ão ý@JæAtªž¼.í}ö­©×íLs= ¨*š€R1ú¬tar!ù %þb^ø€\ Q×ÖámfÏØ¡àdÚº¦Wðœ‹]íÓ Øa¨§¬oãπ³ƒFÂZJõ˜Ö2Akæè[&õmDÞUÝ änç€ò™mÂ0
áà‡®¥ÒDÕ=MÕí/•Š—˜³²ù+)ç+'&RIÙn0õ¨[<:ƒOòëøÍ!І¦ø±¨úÈßOˆ¸~јu>ÿ5lçFùŠD"é¤s²œ‡Ô!ŽÒNU§mRu@â÷pÿÒ™»qRNlÒ÷Qv0¡8ä¤ÿƒP.ƓÌkоògŠi™h””U‘c=MÙ= úƒÓ Š9@ý6ôB²ŽmUn(ÀÕ
ý )C!y‹'¿Wgõ#ïÜÑøÃ§«ã3Ôۃq¥m×3Wɳ÷.}Ax$«Üƒ9hÊ{ç•Õúˆ²­t]¨oo¯
€­Ì ¿Â€§©ú,ê÷,M5)AÃN#Íp kÃm!?ê [„%¿½•¤¨{b>Pª…nH0%V®Öäç˜Ùv×þ0·}ô‰WOÅtƒ»ŽMÌ=MpeˆÕm¯WñÈtübš»½iØ3#sgHÙ,ºÏÇ!Êû[ñG¢s ¹ä‹fI±E]MŸeNš¸¿FÈVy4®È[ˆ.:=MÈ­G¦Ãäœ
9¯ñ‚:)ÿÓ9Šœ5M¹AR{™ë呖´—Y¸43ÓÞÉ©/YŒ¦Uµ!­Ÿ)rъH˜£;s1V8ˆ
˜ÌO*Gã0d÷@è5“¢„öDt';>ÁƒÿÝltÊÛøž ZŒ1æ¡e,+©Á Ž†×Þ ÝâjÒÚ9»DbÓåHS üìƒ ±H51˜Ž q|¶íÂ*ÑÁ^ïªØÛ’b3-ذ=}©}ԟ{?jtÛÁ5ý¶?Ðô™bXÐrhÞ8¯ðW"ˆÚéî¦ÎNûò;ÊB¶Xê/<ÿçÀk¿”ÖTgm÷ùŸÄEDö‡°“gåXë&_/™¡¥4Gö7ðÃMf9㍥G9ˑÏœ=MÀaQ™h±Xì"Öô‹ ï[•ãGŒgnâ_dU}‹Æ“°}€[íá–Àýí4‰Ú ùù@Ë¥˜š= gæý‰(p1ÇGuÆ3ìŸâQFÙôLÉè»áUJÔM‰£'ÇÔKöÖÀQŒ£õ:gxq/c/-kMpí¯ƒ–J‡…%Ö!§ï<‡n§«˜ ¨Ø•RGÿøjõqY!•˜’±¥°*ÔYP9h = ‹¯Þa˜]¡¹ "º¥= Ðh·šž›¯fGÙOéve^E¼çœ¡öãH1¯J‹äèØÈ0¥ Ä[‘"³ïys/֐¤‡nR<Ë"c€g¹UÌ­a£¼¨ç‘‚‘­Æ³fuåÖ[›—÷J‰Æ¢9|‰ƒâ@­Ó…D$Á= Úُ)üËæI sm™Où…ÈkþRòI­aëËlÑ1:Íi{¤w­²E¯Ôó…m¦7kZÚÈšY{øSeõ4'n:Ú¬(:ná:
×*BåÓè<´wCP!—„ØpæÎ¿çÎ˰%4E9{xÁU[ø©qéÒÿ­gÑwæ¾= 4\‘Ž´ÉB-Ú©Ý0ŸÓʏ¼h§HeŽÌ:£Àøˆ¨]Ÿ-B„ÕÏï2w3‚xÓ®º°ùÊêDÊÒæyó‚lÇpVÍ^µ¦ù±¿„Ów!‡6Qòý*ﻒFkª¦;ÿÛÞ¬¹š¦­ÜÖLÒ×óӅFˆ<ÙäfÒããš3|À7ñ êÒAu»ùª$Õ½ð3诃ÁO¬(µ+6ŸÝ^䒀“ÕeœMÚ\ŒåÄôä>¹š0Շïté¾)‡xêl©Èû\&kfÍ5[~pŠŠv„9¬ŠÊîRï:ȑtÅ㜬äiÑ]Šè#Pˆï_ÿéA®Þ²‘#¦ân\‰¿³âŠC´w*œW9ðpð’‘O®Wš#¡F-C(61ÅõfÃ\„¿Y´v¯•»º¥ˆÕdî ~¢;Åvʺ²¿_1ðµmˆjœW\6"h»%:ÈðäZÙ¶‹–j6%XªíŠŠËr7{OÕhUsWvÉÃVã"ƉŸsë³PÒB?Ï~²Ìƒß x£z¿t¬Û¬´ˆ ­"¯}4åàwK·´Õñ°˜•ÿJjýŸ@_ÉÓS·~Y‘Ö‹™&÷+AY8¦ª¹‚O4Ù¾ßRøÛüYl£Q¿þ“Ýwþô9¨¿ÐZ‡Ùª¿2j 6¿ìiîF™ˆ—6{žíT™ jΗ&^-·ÃJqjqy~l‡ÞS!ºdaâ¿bŠé[¼‰'™® NªI²ÀÌJ€•ùN›Ô ?/7-¦t‚Iç´=M†¸^\ðÚc†Ïß%|Ú8µ ¢,G58šJ58M u=M-ýúÁ›O0zªÓˆ»®o%ÎÿaNŠ=}ƒOï6.j*ÄѤžÚ9[q³èr.üS¦¹Äæ1”ã1Q¨š™óúˆ{ŠÆ5^¬®Ÿ”$:}—i–Q 9ØT–Iáâ¤<<¸4@ífŽyë¼EæÓ!×ðk_8*6= TŒ¨DΝ/®¯†!ƒð½-ѝVª¨´[¯U&TyðӔʚÐ=}cÆÒ:ç‹*ü£À¯k»“EÊ>õ Ñp¦éiÝ5ÓÒ¢ ¤@·Ëôe’p Á#qú®×n‹Í÷â Sk@ÑÂ?3pIÑAk¯»×ý ½6÷þ öþ°@2Õþ0±Â&D)î7d±B–M:ÚüE¼˜
)›f=}6i9§(Iñeë¨9)›&=}6P7tëvÁûùÈz±BOâ3®Ÿ²ëh¼¸×zb±Bö”›
-")›aë.7tðv“= ëJ,"9üIC'gºYX'ÛGñ×
ž[¼àFI^֚‚Š\­òlñ äÇÚwƒ§ƒªÈ¬õ•QWF¾ßÓfá%})›»ä©q¢^âè¸F‡‚~aR9;È<Å3,ÉRO[P{ñf{†;ø˜<m<Oö=}¯@ëõf{Š;Vö=}gD›¦’»Ý…D›p{õ; Ðfù4A'M7ìU÷¨¹ì;YP[Pû8ö=}ï?«¥’»‡;b²@#Eÿé;x<WP›„;±@C±Û¥F;ÜV °ݦù˜‚ˆ=M°n °Ï
s¹&®7ÊúfÁ:'ö2"Xö: ö1¢Xòz.)ó8ž*Éz7)ó88*ÉÚµ²9 1䮫¬6ªSJ=M§D;˜Z3XVš^hÄyçäÐ=MÍÑn çpŽ­Þ§¦ ô=Mr˹ Õùü/·õ×fMkåÒ¥—Ü(Úµ"2þìœÔèr³Ýªƒ2ìV¡ôÿóQ
è ш§~-]DþÆOjÚ¦xµ5è5ù°Aðóœß%}+à„©¾ðÑþÏéXŽù⵪,øðo8D®4yÇØ5qB2Ùúf
= ß˜å6BË%Zù˜zT@—çº'ZDÉó±OýÂÁòÊhžÖð&Y•#Ìo€ÿO- "d‡¶ø!= X¦_QÄA³/×›¨ =M¢-,’ÇÁî¯VŠñhK1DóW:‹²­¶5Ìó•ðu£LnŠ7ƒÔráÔr%Î.¹ÞÃä{C<ÖÈ]¤NýxC:ůn¹³/*¸"£sWh”¾‘Ï+Æ(³âóÆYt<0j]9LMs‹ScÒíß r¹aÍ3 = †,­ýø¡šU¥9^f,u53Jµ­ìí‚Ɇ³A•ð#ç¾õH­¨hüÁŠTJt¶Md°JÛÝîtÅ+~ÈsOg›gËóÊ$«c§-,uY¸Ù—Zšý†Ún\8Ÿ¾—¼âéA”#¹'à×Áàò&v¹É>=}7#PbWU<wíî "ÜV]Åx%U£¯–ð]—ÖÃçv▇õ÷/‡S±@Ýˑ>DiT:”ÿi˜Hiì+;HQ™]
ßãT„´j\è´gl2X£/+SÒy\+#ÈÆ…²t„>äÏ(ô‘%†›˜–²ÞóuôëÏÈAC¡ON¾
•p¥qTXü&ۖCæáÆhLH\°<ìñU›*ñhÜlÙWîtx焒‹ƒ‘ÜdŠ¢4ɦÑèÕ æëdwž÷”Hϒ~~äÀƒ/µ‡$ûE2à KÌ©’KcfB°î»Q'ëÏà~¯¥+Þh\3€víÈOÿú¾ívUÈWí¹ÊIWsVþl .²0K™²·’&%÷?=M”S„Ý„ì^jú)rŽ)
³#eó§þ'6- βÿ²URp‹§/‘c°,Š}êùzI 5Ê\¦r(/rí ³ZPRÁñÁŸo;ë;T"¯æ*£þ8 u.1= J¢i–Y8†KmÙ:E
iÞæ½õíÿòè•ÊÁ|š°±ð0¨]ï°s!ŸÊ­Cp‚:Ýx5"( z¦æ/Ü<˜Õ–,¨9cÝM9@•4= ¬À=M=MæãHóOŸתʀ*õD¼Ï¾j<•·’WŽÈ%Bd,´?x\W”¨ÞË=M/üÔܗ÷Ía¼H*òûGm­4I8ÖR²¨—÷*NxÇÝeU¸I]îÝ[Çð@ŽfzcŒ:Ïy÷ÓgيaS炏‘žy ½DP0†D'= 1düŒ¸ø‚~€Î^.ä×z+GBû0‹<o= È¯(Ôí{DŽM¬˜Õ†«-u˔‘á3üæߓb&›hX ¤(ÛæÄCYÞq 8K\s‚Ìðd؅ëÚðsv›Í
k©ÖlVlXÒÛ{5ÿÇVaN“îç5³¾YRPô‘ä= wµb^Ùp\= )mbV= h„°¦á6u Õbæ•QŽ!e°Yª¢‡ñN™ÄLü,wÆìh—‚-æxAûQÚ3uÿ\\ävòeŽOÏl8%µaªËeÒjö½dò‰ìwÎÊãtÒ§-´×¿‰ïYY¦Ë[Q=M€òa:ŽyG/bÞ
õ†¦Kz0læ‡ð¢^D= ^Þoâ‹Û²@Ð÷fãÏ0)À.u­ùa¥/iÌ e¥ÐÈïU¯=MMd­ÍÅtaæsóãôV„ÒpÚê³SäÞ?q–¦]’­î¥úpUäÙë¿È¤ýQäù‰Պ§“ =}³‡+VѾÿü±¬Ï:"O%oo‹@ú2¬l»{p©®"¬ÚÅñ,¤«æÆZ\‡9©2³Ø£±ä0íg|CÍDP¬cËa8»@¤„Ân‹àU'­Ö[¦ÙÁ×ÅdÛ¥Æò&G:r ƒ¢x:j‹$¶–âD¨Øaﳸ?Æÿ7¿j×-oúFÆ3>}U[žé>#ï’}cÐå'—ÍØnÒe!,gª2z YΰV
u|êÂÖãmÉçñ’u1àX-ªbO”&dŸùo‘Vˆx€Íž oÊÄy­ãhúkä“çË!¯äŽÂÏ ©dŽá_µÛ†h£ôY,É¥åK4 zC\Q.aïE,#”´†é6§,(¡ú$-3÷ÀڝËz¿^j)hâù&ð4‚³œRĸ³êPÝ¥ó1ýÄ1w:= ñè>éÄï$z$Öh„[܆½KVˆ?F˜{ÕÀ¡Rö!;|ì ·¨oF›*À~Š<tK€ ŽëØ-vçדŒÅ˜d,?ß?1ֈó’¢ÓÔ´¡9nöwö²%Ăg¦ؽLd§Æy€ÂæÀÝ}ŒÛå“ß«ÅR--€­†´è4i®Z.Ñ<BAóI³œñGrqðqhp¬pô¥'©· #óÜÿ«¾ü¢”ëçeÌ%Î&„©ÆHe TåÎ/]ÄösɊK¡ò@§¼bzIJÙ6ОD¢*œ8ÄKUˆ1 2ùS¥½¶ñ±’¯0°õ±a7÷ )ùé(J'úîúºöåòðqñsò'¤‘rÀqØ:¡7h0æ5½jÚb·é76$1„ZÚ¸‹¸#³ú8 2ò€úº²ø¬€Þí,¨~‰þ½ÁÜHX0ò&®%Î%¢Jèrç&0ãY´*Ä|‡û= ƒIÖý´·ÜÃÀ¿·IÒÂì½íH‚W”NLŒñü¤p‡LbÙ­ç
\xïәÍ/Ø~§éˆº„. §oØùÇNjí!Á,ÆÛ¡õÑÉ{â#
¯[¦÷AþB´]h¨ô¬
F= ÞïMÈ¡Ü{ñÎ=M
Ê?Æy•€–ëí#žUn•¢ŠŒ§%©Bx?ôÝ¥5oØ¿ èù²ø—YûÅ,º/÷˜4¾ƒÁ‡LJp pïÁ1ƒ;3k5dXY ZÄ\ŒzøÚüö|ºˆ/²Õȇ [Kp˜‰*â
Ƀ• ¹ü³ wŠ6]f0KÄkÿ}X/’õÌéÂ^B]Pe{zËB¦xæÓ°ÂÀ5$Ue#˜ýɇǂF$%Yg©_µ¾EupïY6&Z]YX'¨%÷ô= ¯AbùÐz ’5ybðp]›z¸ÂÎàY±Ó‰!g76ǸYw¥Ö¢”‰j¡:1.˜p8v&ò)½Êsƒû|gWì45s€§ceEóÃ~E?C0±ò¸´µ,{•'ݤ¼)^Á•ÓƒO\ÿ?·YÒþ¡Ü%Jæ)÷ë^¥ý|¡mL "íÌ2¬LŒÈö¯%‹!õ Æ¡Á3¶uvIìÓTV#†_É,ðÆêÑW‡—s O‚´ÄÜYþè[¼ËƑɀG̑)ŽÑá:•5+ø‡ j&JÙø[ð1ÔðeÎÊßĞýßøâ*Ô5Àê­ç/|> seoupö/ü¶ìÄè’3%X@ЪàÙΐ„öFéznsÁaXgçµ¢›©"Xø){Œ ø·ÿÿÀ¦±˜¹-|¥m=Mº© 7Vêˆhxbi‡¸åà{¢:f²!T“u@iǘ4‘¡)siw¹’üèrÁEBÚ4,ÛÁ,{âÛótà²&HA3
Ùòúëà$ûŒ™ŽJPªkî/Ùúo #Ô%AÔ¤Ф‚…)ùñï,CÙö%#ôBZÇ‚j—Ò´-ýÎJ-ü îM­ 8Ÿ›y'ï v‰·¦!KOôd"} .
!þbg™²ë±¨.{¦Ê„®Wx.ãü·ÇP–ÊO™¸7 “y­¥¼7ZAt³ˆÈ…y²*žS¼FT©•l—ãüÇ{F_Ÿ³ó«g“VlX*üiDÐûõ“ û&c€yÿ|PžëUÃQQ½j—ۀ½2[àa}ûêà莾Î¹_Ž ɱ Y›ZG„³X²œÐ±ðdQˆÙaEÆìiœåñ]ÿ¬eZ¾{p‚ÃuÁÈýWÀ¬}^¨ sh¢¾9ªÌØc“BUŽ’>°K±Ìw‡Y ÿ‰Ã›U뛺Ë*Ö³BùN«›²ûmE®\-Ã,sÂEû±B+XaάTèý;r+—û}Ñ=}9ï¸3̲§\@’Ћ6M[¿˜Û儗óHÚ¼¦">ø¿›.ä‚–}ˆ£ëW|¬:¯aŒ¾ä\À»É[¦Ü넓]u‰ C³SŒâ؅q·…Àõ¼dYÈÀ{໣ô£÷#ÆW©>Ïд§\âŒÉk{]ˋ=}Ñv“]Á~HtŒÆVDÞT»Óý›U ]“³·V«R/ÙEÎê ±å?oþü5Ñsˆ±{}Dvg‡¢Mÿ;A¨òš+j«2ŸkòtÝO  ™ëDöÄ"f,b«Òùn·fbÙûýœa'$4¸d†R‡ÖWM!'oýü µ ü…†À»ÿtgòòp¸¤³”°ø'ª6òéšPlvgû¸ßÈ;ԏÚ_³#}!ñ¸B2Zy³m©Ÿòt¼å”®Ë²íL,Ä= |{Ƹυ†à=  Oz ƒóCºšÍCšš†*¤ùŽÐ¬J PášdÌ̐‹ÙPPÜ-0x:å÷÷ë(ÐfÀ¨’é÷ Çw•‘ÙL²$#Üè$jE®šQX¥6øç÷k2€æ¢vêùe£ù0£œ|¡(&çºùcóŒÙP’9õwŽàçÅë/04Qø´«%c:ÉÝÇçÇÞꙢ ¸øh]¡ºÞ6oê篅2žré‘$ãCûT€¯¤máùÐÁ&l…¢¸ }tÒĤeÙzƒû}¥žÝÀݤuÈúÖ¡ã¤é³¼‘äӝM+ÁŒ‰Çò/t<"%H¨—Å|ôòyûüpPÏ7,iٌoîÇêåÁ8ÈsÃJýkò:7»ÀË@ýÌ™•[{#sA?}F{ÍD „¿‘»;LSHY(rˆPÒñ2ÿ4ÖÊ&•d̞7ág•ì¬ꀒå¥Öÿ뒵?
Äë0 ä-BH¥{$sqŸ¹'Á(úQš+ᔊqh³‘B­Yæ¬ìÀóë@f±Ošöš¿TG­-I”ï­~! ½FrÙ÷]ўA
“ÕËñ*´&aÄ}$Wm¤ý^€Ò*qN>§—b•Íï {™¤ šÈ4ͼ1)N"ÿç9ø7 îA}eò°¼»p–I8¹4
’š„äôø†û‡5ù÷Ʋ™· níí²IÕNø±nø:ñ”ߢr¥'ÿ”læJQ˜¸3hÏÓJ°IÓTÔÓ¬ß#¾òMä=Mü =MÇ= D$B‰Ð:4åÄnZXKqj¥þ8ôR*ø¤QJS)Tí«°ÉÞ@àPÊ&•kÌ n³-ÈvÊØø,ã鐊yýnŸ‰‡5)¢éê8aÍe(wa™õa.¤}%f'bɔִß#8rˆÏÙý¡Ï:ϐ¤6Ü:¡x~f¦ôì&îÉQ™¸“êÿÑÉág™ØñB)Î5‘…m¢7/4ÎnI×÷/ëöÙÛ̞b ㇰ7'ZŠz„P^÷âãr(8(Ðöp*9’Ç/ØTTú\®:®GyÄ4«Ê„ ÄK‹=}4>f‚Ê,Raá§z¶6.³ m:øûCtìÆß§}H—Ü ëþlVqdt«yYey^ ^ñ˖?}HQ‚³Hk&œ6ãÒL’ä0Ÿ@Ü[曀º© 9Ö3°$°¤=} ‰ôûæÂ]ÔÈ¢ažqÙaˆùa$Èͧþª‹Ðւ8’YãêOLu·iBéA÷Ú[!p4Õ^•îÇ'»ï„ÃŽòdR{ƒu‹´?îôª¿H‹‰çÿš…‚?5Î/íÀœÿb#ž\ôLjÔ)ûJÌɟ[¯7šŽ ,º$ Íh‹„<xA¥vAWeû˜ŠõK= ÝM ¤<lÇrK'DËT­¦óC!òÀŸáÑ;0ÔÇOìG"ĝ®%e CjÌE“&ƒ½Z »Y ìԇix‡-ÿfäÀŸSlȕ†lêÏÐ >³œ©Ï¼#p¯”zùj D[»ÃÒk:Šÿ†%ozԋ{эלAÀv™^äÌý&§F‡ ¼ËWsæÔ5‹= Z-æÜI!0ANH¾üûö›Å¥!”¼s^x¦lŽ–éP˜õfØpʋ<"<æD‚“Tç˜Ôè  Ë°ÎŸÏßÀύ÷>𬻆ÆISopJá:
NÜx16;hi ê–ƒéßÓöO÷ÀUG›(GñÞ_kÜ,Ý·÷Q¢Ë|¨¨Aa½Ëc¤ß©:À7úå#KÈÑôƒ¦ëc%¼#[= à+Xß½œÁ+EúD
:•%RÌ^ôÙoâÓâF‹£ºÞ›p£½±Y7-
X yk~h~’†a—[Í|’|Œjd®%ï©ñ8.æ½ÃkóAÛfûB™è‚Âo÷Œ—|k0²Ìê¼J„ÝЏyùDyŸ3«÷9øí†¢®ÉWÍóµ 'íÆ!Â3ɯ¯ëîì‹Ó$W2·63/™ Ž4bJ¡éÆÈEJÙ*"¢¤§{k:» )v™­ù3‡)ônÐRªó#&$YSzhwœ&üX²W€ÚANrg?÷ÐÑF­ž­†î]÷·˜Ê“QÊîñ:¬ã7£½Ýã7ÃÃïQ=}̓‹#íÓ-çšYzِ·³¨ßM¤Ä Ûv–3©”ÖÓS$òrmd\\]› !èúјw×ð4¬|"ÿ¦vÿ“Çë¿7µ'%ª”¦nѧ™ï= %Ý% ¡ÐúêúÊʠՒ¹¿êÍ}Ö§¤$Î9ï^bfa¡¿%05º _áêÍìßc m21Ø Bëº÷§ýИm]Ê_3= ,Rÿ…àÊê½CÐMìBÙwNÜÕ¡æÍMÐòÇL·vïÌ8I0¾°…
NR鑤6òڑ$4—qÛðãjŠR¤Ð.î w‹u؎„ü˜mÇóþá¤uÖ¿¡#˜¿³É‘äԝWÿcBaHW6G ?ÇdBàx^ûhƬà4s5ÿ ¯¹ßlÞo%bPÛÜ[G.ËCS‚?#SM]<×¢¦ú^_c«LFŒë{G]€FŸ<^m¿…{Ë<=M=M×zêúó“²ßl6žú)¦­qN–àeÎC.DQy~­‚°8'£ª6•8"øöò ‡±®YnË_«Ü;´Üå>#4ú.‰{”˪BƒM…1}Ú®«/L8³®z;êU”û,¼v­{Q€ H+zsM¿¥ÅÍâÛ5VÎR³|ÿ‹Ä»>&’ÅÔÔ2pƒ2r]•³¾›9Xêh9ðeû µë{À4;nŽNž^~>3ÎÀOÛ5ê:aùëS@sû’iZ´ë= ‰­*(•ò§ü;Jb¦F^lÁùr7°"þv=Mé•훡£ÛÒÆÞ(¼!mDdìÌè\ÇG_O¿\i“û~ÃÈKt›Sà4dñqLì5wÂ8'S诮¸?ØM ¼ãÚcþ\_è/m&)ý#ì“SƪÚk%¢ӄ¨“j¢éÔ¯ÃVâÎȽCœâܨò›ã Ï^'Í8=M ¯•ÖÝ*>¸¸ž'6ÿW{mçžCêµaÒ") xؔzà.º]­lÚÆp‚:ëdI–3¨¥›†ö¸’‡ºë¾L6Š4+HvöÈs× 4>óX[2*o²X–þTw3NÖíáOAU¢ÕñE4\(-Ù4.ªTr§ÿvTÕ¤ 8ïØ%p/ޝ
ZVm‰ÝœÜ)ßlðz§:8W^s=}qMnΜ‰= [C¤¤øÎäù3ºÍ†'G:âà!OrVFžCO½Èþè’Aƒ9‡2Âæ–ávàOÉ0–õùÏ¢b‚‰,T x {ÊýŒšòÂôD|1¡M—ÛÆÜö•¦:‰×!mƒÇ.ô§ë/¯o˜sï© ÿl~Hÿê4
N çB#Î}yg~+
YDø*
â
Ók‡¿%©9õh Ãy.êçþçŸ/U¸†q{¨ ¼?%·Ñ§h{ØHÀÜaw¯þ£¸I%©4&ŸflJ°ºåeƒÙt¿Êohj2hHSå
€øzU)bÂöccI%&3-/=M ¨È«»èéöHY¼Myîîï¸å9ÊxìN(ˆƒY„¢Öt)‘5S‚ZYÒ)ÄØ›aM«Ó˜L àۂÉãd€Á>¬Î‚Èú
kÝÙjPäÙmi%©µ¯·,«xIÝjjáÀH^Óx&øžlYmq㘚YQaвoM»---ùà9~/TwÕZȲ<Øî×m’(ÚÙkÂÖK—hõ!PbÎ"µIÍ¥ 0°ºª·«©2-KbôÓJ¤/¤ ÈšŽÇ‹³W°zŠT~Õ
Èt㒸r”ø¨j®"H”ãòø,•µ»Ô;ïɊÑsVl[uú—o¨óÏ3,á“÷:[ö&±¹¸G6þ\ђ™Zn‰d&ñ(Z*)<1l!OŠòÚ²·èœ!©žÃ®T¯%Ì»iP°€Úˆ ‰]ŒîVy¾©A¾)ÕîÃH2&䴑SFYð
rŽøÈSSxèºØ“§b!m%Êõ¤NAMW(»y9Ôîÿ“Æâ2"ò¢R=Mâ…@8#%eÊè+ŽÅ°#µœÑ= –"Él^sõ¦ˆ&ÖÅ×3„5—ø¢o\¡.¿]ÔÔÐ(£*Û+ãÿh«ö‚N¼¸^@ˆÙö3qܪç‹ 1yün!€xQT““v4 .ÝÑè“Ø tj¯!7Cÿ}ÃLQݸ'y‰j—¼…¾|džÃ’§!X@\;A5êžúîü>e¿$wo.~î[œZöÒ$«o—©JêöÂ"œz:”Â[ázÕä
{»= §“+Š;æZW}ÇÒÁZ{þ$&>§›|@!/¼È( 0¥3UyêB&)$M-Ô¨T¿>Í·@Ü_sV/Fum¦g=M-“…A#k߯^{%ü1WOtC¯9µ@Wá]kHÜ
[QQC¨@_™)Û[ä~Á£i\yM{‰W̃¿_ƒÁÇ ;ŸÄÁ]BɽÌE´òöKç+ ĹžØ‚GðoÒ$ãڝ޾>÷˜Ø'Á“ÓƒØl\q­)/ Ä9©ÞÁJSÄôC£~Åã4îá?B5óÊYyÿýÊ?ÖraèŗsÕ uƒ½î± ßé–T.7ÔÎb4ô{!¼áÜÔ$¦Òj”o¢µ#¿¬Yžc çN‚2(ï¬%àLFýâA@{xt&¯™ ÜØT¤&N‘æ!Cž{)1(֒jØjisÔ®³š…O-é²^ɬlߞÄÑH»ÈÌtG{įOx›ö.f­’§„覥.GòÇ"s:Dõ;€:e@wktwïy²wWÞÙw -w9Á,wÖæKx@èex¿ýxŽ!ŸxLĽxÂÚÎxíàx ùóxuúxêxOÅ2xI= By/MyšlZy£fyÅsykyÏZyú‚yÉ«yëÒºy–Ây›JÊyÓ Ñy´Ùy«)áyVêy6›òyLöúybyòÝ y³eyÏöyGŽ%y(-yAÄ6yùK=}zZ•Az_ÚEz‹IzfQNGÔ>{׌WûÖ¨‘±8í¨ó˜Èô¡ñ¨1§¶ñړ±œ‹Jma1-Ï<ÐJ\Žt=}½ ¨ëIÊ0Ôr SØI!×eî_"ØHý¼\ä —i«ÞíûÎJdyS5ΓVÃèC"Êkm=}^íc՞âžô­nÙ±¢NôùoT”³eYH-´Å×å5$WEÛ
í‡É—ý: q–f®Ó®xÒRV-ƒ
¢K@"PgEŽÝ2}ý{PÂÉæc#Þ!ì—Áx¥é·‘n³ÓÛû’«sškÙ±|n½Ù²?N™CYøQ'9_c4is6†«6ÊÛzüeƒ9ÊàŠþŽ$\QQpp4bԁUL=Mâ#f¸Æá=M2£c”D¿ÐXÐï·LƒÚ•„ƒÔ®Á:ßùP͊5fÓ,–U2âo’¤ÉÊ=}ò—^ چ Jà5Ö½ƽdbß41FI¨Lהd#pŠ«ï‡ëC™âDíÞ@ y=}ÔÈìɛ¹ºüÅñ½Úºg厎¹–Àž¥7qåTx–Èò‹®u™ˆ¼öÕÔ5¥0nòùÕde©—‹Lò€À¯PÔÔey3åsãê‘¢PášÐ·÷ä&±= ÉŽÐ÷…Œ-û|„ʼ…8‹³’ü ¨Eo†O7ɏ ƒü¿qEe.¬Ú
-Å·—Þßï™ynÖÞ´jʙ&.C
ÞlÊî˜.3ŒÐƝ¢i©®«àé¦iš«¤ŽItt7[Â!ˆ®Ë˜¡ '§¿ŸŽjVågg¢lbpFfggz¡m®‚aaÔÔS#¡3d¾ÌnSg
[A&=M$óuØÓ¯Õ2‡â (o7âßÜËúº­·2äjγÛ×ãž$lÑù‘žÔ/€A¦ ³Bs}üü‘àß=Mæ¢e* â ^jÀ ¾ŽS²^Ô>]TÊL“‰Ý§—MþÎ öÌG€]é>MÄtÚû˜´=}¦vb«&{wLL,soÇ¢Õs¯× ºX×-“7‚Í®tÁjäl§T~6H §®Ã¡¦ O¨b¬è’OÚ1ÍX„ÖC‘Qb½x—«F¢¬ SòOœÀ\–X°‹!šI¯ŸC¼Ú¾,gC¡®Ò›=MAìkà ¢>TZƒœ ;)T<CÈ;õÏ6ðÖzN‘5rÊT6·lñ14ÝJ¢··7 ÖµlWä.ݑ)À3%?ÀbZ:´« ÝBÖ³«øØÚhԕ)©oÖÖ铑ó ãÐFaà w¦%ʉ‡ç*¿‹v-1Â>’mXS3 ÙrD1G©8[vo©íûÑȧ q¦ÑJëá·éP*••¬±×= z–‹T¢ü ‹$?xIÕ®¬ßtQÕìãàTÂPá¿ÔËP›}./=Ml¡M^IïkýyK
"A²l©¸Í6%ΔçÚJõoD=M lXÑI|®Åq‘šïóVßÂ^ՏGŸ† S·šÊ¡÷6£ïòÊ]ìcÁŸÊ_4 CGG‚‹u²s,¬ ºS6²J€ö‹¯mÁ«%›©ÊU‡™[H_=M¥[&QH{0øzŠ÷5® žz—V2^s÷j2/œ‹ywÊ*È!̙ï(Æ,dxÔ¯÷Bö1ûuðvUöôV5„˜±ãF±\yõnf±°]ö„bÙ@¯ õIÃÚ(Ù¬.Œ˜ÑUþñ‡e§þ¼–èäS Õú-‘ozÍX–-ê@ŠÑrb5¨nEÇÕsädVjrœ†a3p¡üÁ‡7Ëq¡HlÖ»qÒ%Ž ìy½&Lô—¾d9N?J;}{<;í¨ñ:ñ¨ñ˜¢ñ¨ñ¨¦¦‚:§V>E–+µǚ¦^žúrtëYb4•µ‡çü{òEðôÐϓÏ^ô÷GDMEPÞ™Ã=}Á¶"Ìùtҙyƒ© ȒA<V=M´ÑÚðd/«ƒ]ª¤]F„ÚX‚Þ±Gª‚®ŠiÍË<LºF^åp¿òнŒgEŸ!À;¡5ÜùҚéÏg-ýо ^6)7¢eªuì_¼Ø1g¯ïà§áHF çã%¥%Â!¯†n iNQT\l˒º/dêà7½Iª½ï˚åAP°™°ŽžXPoÞÐ˗ÞCˆ´Q0Ï^.ÉGîÞß#öÍ*:Œ¦:üº -6À'ڇ5.Ø> šÔ­F-ÑÚëS-Ìï÷²S%ú65FQ ·…”)~õ$,çÖ«¾†„’cËøƒ3¼®Ç$¶ÕKÚøxý(PBt–ñR²˜ƒØ‰=MËhB§['èþÍpÓ èç=M9)˜ÿá‰.´¦pֈ’¡[t·e27aÍ7§W„n®r©sEšÂ«µ{×í5j Æ^¯‚áónPƕ€Êš°œ‘ô“õá‹'ÒM4ý‘ÿÅé=Mœý^šè0Oe:Ø¿²ÑðóîcºN‚NÀ5W€,)ðÏãF9hu;º;»:FWÀx·x±ªx˜Oxìxô‰‚xK&tx Ágx{]ZxgøLxí’?xð(w›Rw±…ówF¸Øwmê½w5üŠw9_UweJvÔâvvi¸ô=M¬öæöáÇb÷¼d˜÷Ä÷Ûkß÷'8ù÷î÷Ñ/÷lìBøÎQP?ü>›SAO{ü™¨ñ¨ñ¨Ȩ5jñ¨q*\-ºTë§ùSUêL±„ô}ÿPd$4ƒ5AøË-Ö* ƒ@tÕÛa(
¯ ÿN„ íͺzs…WJ$c+LîjüMoz…ñP= Õj$krŠ1"ÐвÍyð„uŸÆ²ï82NfÊ:O"z«6 Z=M²:ÊŔJ…•µNðázfAìܳcn‚-ÆlSÉb± ŸÙÙÚÁÞ&‡É:sÒêÖQk‚¸l-ҌpœPr­¦>6º½8(—À™Gtƒ΂sª¹×¼ÐâSr WEií$E¢UêNv.q*ª~$\*w¡´*âóuz^¥6F™ù_6¦ú vG‰·²Ú7‡çêwɐû$÷Mʺ‘\‚‚Ðÿ%WÇ 1ThÁ¹âþ<˜‡=M"¹»<Â1TLŒ¨‡ûFŽ‘1p¢Îð@”†çãÊò!d–g–'“: ¼F8Ø3ԄF—ÄG²ÞŸÚBŠ…@ÀÙG7ÖÜEy£J-îÀzˆn>Ä0D5K­M}4:‹sɀL3šÅ„ŽoZ-/³Ñùˆ7z„¯e_fÐU¤/ZÓüE‡ÂŸèÚþzþ-{zÚë\À=MhÖ¼= Gc€ê’t²Oek:öÅäªå4*•‘ê^íùÙi½<{é?<ۛwñ¨ñ¨ñ–zñ¨‘
1Ƭùðžv”v97«2j€¬–À.:dÍMzc‰×8ãCqU¾7ºžóv¢g‚ž+©Wœ.‚&?-§bÍ2þ¹šoÄ= gfì0Ãm’VÓªá:å-¡ÕU½4:ž°1ª ûy–ά•wwáÀª¾qÁuçõÝþ|˜%W0s3ÈY†á&Þ¢»h>éhotÚYUÌ÷ýÓSN¹¹¥ý¿áׯp~&ÔTj’ÕŽR¨|.^Fyiìÿ\ÅÚª”œrÕò™q¸¦~éÿÑê¨'ó®IÜ٘•IÏZ®›~Ü^ºvýubq‚sÞ±þԊ½F¦’áØ®€âH⊩— þkJÑ9ü|Ё/÷s¹Æùoª‰2Óþz·”®ýÒÔ¹·,ûJ9woE[%‹;Áļ¯9Wƒ}ªK&н$”Z *K‘™E¹Œc‚ZÂ-™M@«ªI"÷²?ª
€«[vÍÚ©mènÅX*ÞÃÙVˆÙŒGrò«fÑ¥´g´Ä¬c9R=M³9óN
À1?a9Ùûã_ÌÂÎÓnrÄHsç‡gÀÀ֍#í† 7 ¡õßlõjÈuʯBT"Ðü¾Tµ¡M­[NԖQ¹¶íc«!9§)•!ËøqB¢¾—uQ ä͂wiùªyÁÊ.ôŒ˜»)²@W‰j4}×=}ÆSîòÓƝÙie!MÁ6…cô–ý7­iÐ=M#’(h —e‚• ä‚ —fã[-·@)ÐOèç*ŠÐ·¥éÝ4:3ÀŽöôÕrÎ¥ó™~#Òp÷G瀴˜hÎzŽ=MưóvkîY8µ½MV û ,Èçè¤ŁeÕS¥(҈ÿ¶oÎqæÔ¾rÖ$E¤œÍQaw¥Â㑏‘ñí7„f#qÒòŒ-ëÈçë±qj£b(v”XWÓ|þ:uSë%‚b–ØÐö+g#ê˜Óx(€›rˆÎö”·øVhY)Ò/ÛòàJµ#,# ®ªjÉâµT6éRy‰Š)ž4ÀyiÈ*b= ùZ$8Ÿš'"ì>'ÉU†;¢Aë4¸»?*|ù£XŸ11|±9mÃÜKzŸG,5K€-\¯™HœsÇã¯SÀþóØZ5¡SVÂLGF‚cÚtý&N^7³²?˜xL¦Þ€/°Ã끿 Lr™Ã3‡ˆ×S±lñÃçªÆlÑ‹)\ É¿êA_‡—^œá—}üjM5y…óœd ¨ßϨPÅ৕^ $„dš¸dà1ôÏcPTh±m©À[§ÂGîTméêά$ÃGÈæmñ9_žFTbD3UC÷²O FQS‡Q«eQëªM#WKÓsˆ]<Î0=}f±DXP#ZP3)íûÇ:C‡4‘j=}P²@]YFïùKó.›
Ú=}-
GÇ>¯—<OÎñ¨öüöì¨Uúñ¨ñ¨ñ¨WÈ":ª *Üz:4š *ÊúE¸:cg:é4|Ü nÀ!rPýŠe'áKô‹ÜƒÀ´EL<Eöe¿£4‡æø¡ÝaÕtџaøöT6úa·ÊÔÓmÈ;NbæÔ° ́÷ØÇ÷8©/e>
ÞGsÑn«ÎÙhŒµ –%$ïƒT‚XV)¤ôàˆ†ÄàU…/7:.cƒÌveù_æ‹øcýdæÃÒÀƒhŸ8=}I¿P 2¯y âÕw±žۗYøœ¿šINrŸž”hÖt”xÙaÔW’ ¹gØù°=MÔÔׁ)õ±·†ücd§&ڇ)¼_Q”_LœÎ³=}™æT®jÉ+¥¬çìÊ ·‰G₎¡ºO.Ù­ñ§_€+hׄœpÂ-³­C"‘?ÎóvÃ4â>T°n»Š8Œø.~2ÒYú·“4Ööº÷ó/Όwc2üØ»"©_¬0÷Fz2vn£ØNÊ¢t=MŸã>‘ô1/CñÞµšó‘ŠS1þÁ‘çï řÞ6íqäJçµnìj…(¼¬ˆ²ùVõlûðf°æ±ûð‰c®uMÍ <òQ(ijѬF+máp4°– -}FIª‰®¤Á%¸N…g§À»wí£Ã(%5Jè€Æõ˜‘Öˆð“,ùAúb‘¢~Ê¥×õ“@ú7sçÿÝàPkÐ"/íiþ®=M'ُÍõ腋·†l‰Ño“3’­±7ÍZ¢ù͚ Ê:&Øf0—fù϶Ú&%ʀŽ8¼G譒•Ïú= œê†ö*Ò  æF­51””¶%Ï?§zi¶RYÌz^J…ؽ¸fŸò“/\2¥ªÐÚr7.&Ÿ-í›EtVù•²•_Vg!Æ)7§TwªÁÙež6‰Í´†øŽÞàÿLÍTq}UGͲ5QG ªÌ'ï Ì֎öž÷¿jÃËúp½ÿºëò_¹¯Lғvä³÷ê×#=M˜ˆ$[@z•»ã­œ’XŒêaÅ1Z¾;‚æt©±Ù).ب§q¹óhñ¨ò(ybJö'hJ:ø¸³y1¬b9ÉeVM]°4eSøO͎*Cxæ@ÅýäÓ¸ýoÐÈýÍ(Æ@H"¤}
õ‡Ü‡h{“g¿¨À~¿ÑxX¿u$õˆ+ãpq¬˜§i)çïQ½fï×Veyˆã-Øîî‰6¤¸ÏÑo…š–•,ȄÈbýßJ @=MûHn= 6 ˆ3Q¤ľƒ²„%֕bèÐ)†"öúÑ=MUàß¾¨Íö}ØâÉ#ýÖ 2ÚNB­ŽL·¡xY[Ó-)2,–”·ÁåxZëÐÒU3+ â½³ëŽ_z«xÙ
I„2ٟ.l€÷ô×BYc" Zâ¬Fø”wj©i™î»v—êåj"‚è‘,jT¦´ü5H—óíÔ9.ð²,&™ô–<9£œ™ýÊ,|j‰-ÅñE)l¿.¥Æî—:=}ʃæ!¬u©ù3eê·¾òv¯Örnjy:I›÷7!zê³6º·2(÷$vi˜9z×:ý†‰r+¸z3Õ°ú4tˆ7´|f
o¨BÐ(œ=MŽ@ªœ7ˆHˆÌU+ç ÿ lXÌìáNï*½Çé#’Hax몙Šg5F²˜6Û£–š9J¢½B[¹:{3°SÇt|~Ûð®ci:UÁfÉ^Ûð³_ʂ|––Bèί ËYú&žs~o Vœœž ¢N£#ÔÜyWrÑȁ˜S—¦UƂÁ,i¯=}¹ŠÖÃIBhÛ¬Me董S©¾&1=}ÿn3‡a÷𦯱ó¯Y(:"»¹zXÜÝ)ÇFø˜KwýPbÇ+­çv)“µ›ùDh-½^.Á›(¾ª¸L©%¨ºó2J{ؑ[¹:Dÿð|+õLÇ׎\Óó\O&þëdyWi=}À~]œ2vM–逜£/iÅüƒ"“/Åþþ¢¬K²ž²¡=}=}‚b}ÓCþ„«‡¦O7´Õ½†\®‡þö*¤sú¤Mu߇žuÙè{ݐa·Z Ž¬Q=}µ¡^´ŠqYI±ü3—·ÀÐgJôe4£ØÐÊ¢ØE ž™<‘£P]¸•\QñOݘ™Lߒ€óO“Ç9"Ŭÿó?†ÒÐÛ-ªD/^†,ëíO’6ÒÜ~¹T…&›q‡A(—T}
‹”Éݛtl àWje‹W⻵@·)dëÛîMåñ…WŒA= ±e,î|Äý¹]Gç¯JRœgݟìÃzö'Ìg÷i6nLÚzÃ9\݂.z7ÚÃJ¨]ø9^=MÉr]ÔÅ__=M-G̅ˆmé’=M4…%ŸÝ_uæ[º²@ô„•‰úòƒ¥fÞ¹ õåÏÁd\hðM% ‡ €ïjóË÷^F]_Úŗ-ã҄iHÜHJí£6Á‡íH
[ â¯…ź>·¤Ÿ¥‡†]1i¼!†ìÏäR~õc=MúW|öÁŽ»¦c¨hDáôV>¤óŠ€r¸¤&NªÍÙ´=M6)æÐBد¼^= ð ñ/$Ò+ŠÒçyÍ12كŠÊÞT|ž¾T0dqÜßQ~UšáŠÖú$ àӌ¯áµs(Z…öâGÝv²6Î6ùŸÓÆÑ)'‰™¯\”ꉟ¤æ„è¥/±¡Bm¬©žLôºÔ1
$–¡®/= ..3šº°ÒénY—Ù&Ú¢tȐ' NçtKfZõÂhä-îGÈÐ'õ€ç£Iv¸§eæ iž^=Mn”Ê“¡Kû•ÂvîúgÓ)´^ñ¨‹¡=M¾Àïäëç¿
ýt$Ñ+ã&d”µÜ´0OÜ¥ÚÊËo³Ûëõžèȉ¦Öî= 2e‡W®æ})’´Ñ&„õ€„¸¢†%ß…iȇ¸c´²5\6¤«îø4*6^31)¡·ÂØHŠ×ø!?.æÖMó*9W)¯‹üö?‡"V¥!-iQ†³
æ³íÉ#(7üÌøwè(kr­wZ‡!+3@93€Xé­ÎvHô
FˆÙ-Áì0÷7xzn-fÚ,bš6ñÄɧBF–PwYÝÉðzpL™DúÒ¯€›£J31&Èæ6š4;kYǼrÂ]n¦?1奍˜OGm{0•^g(¨Å$! 8uAb¸^£
r–ýA”nSaýQ®ú^ÛÚNEbóÌâV=Mþx xF‚þ¸û÷7ë¡};É1H}"›—çC­ s©“]9¸OgQû_Id¢ì hL¥Í+Œƒ|8é'Õ?ø¯?)ûëjÔcãºI1Q] ™Yÿ4³öñ;ÒQ}tôCfƬí?¡%à‹˜Å¹mgnË܆ØG l­¸j‚:o@Ž@§ØÐS÷ˆÄfúÏ+]ÐÄsì’ÿ%Ö]WyÄsg‚Öƒ%€•òßf]”‡Œ&L@ „tTiÏ ²æmõŽŒ}xõPêÄÀ¯²·y¤¥ þg5¦Ù|Æ¡ßÌ ŸEsÔ{ÑÍò¦OtF^PQ‡Î8d¤ør€ê«âf$ o¤ãx8x[Ë(‹†%ÍtÉçnÞÔ6G$AXݽϤmÃi2Üiyd0¯‚…*߁á.9žŠ*oíÁr‡·2Ã(s´ÈÅz÷zY¢Á¼öÔ°ç0}Ñ4µEö‘Ö r5l®Ä'1Æüç\ã>ƊøièLï֘À|e¨ùRB’aEHÐ¢MßÏãØª$Äß=Mç.Œ·–¼ªnD õ´÷"T·ñy)5¹’ùÊÊ(·ÕJϘLr4jŠô*2÷5=MŠù#¯v#Ñå~¢z>õ8‹ŒySæ É{Úõ«:£¿!ÓrYŽØç µÐÜ ×>兘ۓ&¼¤æ¸kgz;Ö0}Ë«]WÒÁþwqIM,[x@UbœG'1ÿôÃê‚(}/Q Ç“‘ÁÛâm¿p /|¼éBÐc«Q (ë3º(z2é¹:ºoû;@՛S?¿G†S‰kž«70>³r<ÞaåUGÒ;BAi¤°óGåÅ¥Û7BëUi~vJ;ãG}@OUm½ÛXKÁnnál!kýŸKe[kWG¼ëÃì½ìÉìÔLs4F³SÆAvÌ{¾š—™„€ˆ˜Ž†–‚’Šš ÿ=M  ˜~Œû=Müj[cowmuai\dpxnvbêÛãï÷íõáé|~—zswuil= hnvúóçíáéìôøöªϤn¢
š÷§ Õ°VrfSu5€a¢Rð¯/‰×u®9=MVñ'3r)w·58Ýő9Ä@ð؎!i–ò¨*Î"ªy¹84òÛí¸“‘°2ÇàV²Zx²(îòú_ øÞn†Võa•óÑØ6‰—ó¹*$&Jš:Fãs!p‡Óú±"Ì.rZø5/Ɩføœ5Õ8RZ¹7
ä(Úð³4³Îæbg±'Îʺt°=}F6‘Wó15zöœ8½8²ùîTòèu8Řz÷µ1Éfª‡Mv˜3û¥Ôp8û®‘&7´âyîÕâŠ8»á«ôZgɐ&Zësª¶@†ƒJۛ<K'ô®áP¨ñåküJ{«974žñ>Óëâ@\S@;ýnñð±æ§ñ(ñ¨ñ¨+ò¨ !5½ÔTT7}÷‘‰Øu°û¼jÿ¦°%æâ‰(ØLT7}÷‘â™ÃÛRðõµ$=Màނ²‰ÆGy0™âŠÚy³«ÏÃÛRðõ¯974^‚±‡ÈGaúºM:‹ƒf‰ÍcîKúD²àç?–´=Ms¯]ü‰oÓÄ·‡= r•ÄÈÿìÎRˆ}j©ÍÔ!¢ašŽØb„' ˆkÃÂO²Ü”
o-ÛÈsƒUB4 SïgtW¦I-ºô­ç= Á(w¦=}õ¬Ð‰prŽ4×¥©ðU–„›rêÕ½¹àë®h—ùâÙ虍
ö¡›SŠd(†îϤ!ŒÖ¬ÏGI.ä=MՎŸ°Ï”TºÃ«ÐLç}–ó¯!M\‰m¿„·} r”Ôˆÿðî҇…ª©Œû”Q›Q‹˜¡R„((hmӂϫÌ= “ÿjo1ËÈt•B6Óï"W<Én»¬·{´”Ù°ÿïø"‡ƒ²é̤!œUš‹¨‘¢VD()pŽ=MÚO¬Ù€ÓJ²Ú0‚…6
ÇG"T —¡TÑ,Bx¡®ÉÌ ynU¨sˆq‚ÖÈ 7҃’V¥ÝÞFÒ$Ÿ®mԊ䡿ëÔ³±,í#þ¨Eu½!–%óu'aÔ¸÷¨uÁ+ÓÍiñ_Æ4ÐaYñD„"KZVÆI×b ,AmÙJäµfëٓ±+÷Ä#ü°-u÷‡"†¥óU§bÌx÷©eA«ÚÝ òc.4ÌA)ò<6„ ;*ÖÄLwb Áí×z¤¬–«×‡É+ô ¤Tív'ǺϰȠ”~¯µ3¯ Eôםiá+TñKé™lt¸ô~\"“Ãt·‚˜2“Òx_ò겇
‹Ù‹= ÑDú ^q C$X%NžÌF?tü[a+LP›€qKəklxô|y¢ÓÊT·€ˆ²“ÎX_ñâr‡{Y‹@Qž<z >q;äX#>nnͺL!Eþ“üšï/¿ht|¡‚5j\eGÃ¥6&zf꺾ގyTTQê28ÞºŽH¼°@{ë±VñĨñ4Œñ¨6(ñ¨¡)®TòÙÑ$T˜ü¡ôO)”„ýÁµdOyGNùv„œùœÜ[:•=}Âýxý*ÔM¯Žó]'°ÎÛ¹v\,çSf’±âÎä'´fЁ€ëàƒLyҕ ((3s؃ìXBIB¾q×¹Í{ørñ+ڗ-·½¹oâ@® /]ãTªÁGà!7a³:Sˆ ÈÈ£gÖñÝA—Õ|ðh0£®ÔŠ˜%랍5OŽÆø
‘ŽïH·ØrÌe¯Q(Ûá^Ð3ºSÏpß=}…ªJ=MŽâ­ s|²…¬W"ƒ™½¤’#͇lyÑÝÁ”À“˜:»uæPÛñ1TÈVÚÂÙÀª/‚I‡Ýy Î/§‡ï7|’òŸ±#m˜mÛ,ãFjG…~.¼)wº‹Eù)•–Í —Ò(©aÁ;¹6x)z™ÉfŸ¬½P“ ÃÁ¥æmºÉÿ¤Fp±FÄæUwíŠcÆïô
[S¥#,4F<C§ûš’4ü
Óâï3DŽQ÷øœOœüN¹´œf'Á éñwl@ÀÆ(:l¯Z?;ÿkG>Ô €h2¶ïY] gªp¢Ù"–9ÓÇØtÔvU U£Ê‚÷nÎ\”aŽíˆŸØyX=M uே$×)þ[gÁ„GÞëU¡œrGþ-kHþsçJ.ëºÁ$»;;KC}“l:厠›‡¾ïÓa¤nÈ=M¥Õào‡¤Õñsèâ´ʼ“SNìl ÿ SÅàlP–žg—'Èü“WÎìtž°)SÉ lXžw·'
>´œÙV.ƒ™t
Ø.‹™uŠ è.“™v
ø.›™wŠ!" ÷5wâÓyCg;œS»;iKKÞ£ý•6©ZÇaj¸²4¹±8
é"0µ±øÚ˜&즸±(é8°Y±ØÊ˜" 4°Z±ʘ2 8pJ±˜.7pбҘ.¡7ÐBö-œ’™'ʘ´ífš± é7B±0¾˜¸œ’ºb±0Ƙ¸ž’:‚±0Θ¸ ’º¢±0֘¸¢’: ¼˜y À˜¹ Ęù Ș9 ̘y И91Ó"÷¦ø¶èœh¦•Y1íâøØü”×× ”Øü˜ ˜ûgÿ§ç' g§ç' [FԁíÍÔ'Ÿc†Ô‘íÑô'°{R¯}r¯’¯²¯ƒÒ¯…ò¯‡¯‰2¯‹R°r°’°‘²°“Ò°•ò°—°™2T›¼DqSœÀT±SÄdñSžÈt1SŸÌ„qT Ð”±ngüP»kGþ U´•1-Êwۈ§$.7÷ºùz:Ú. 4"ù®:Ô"Ò´nº1¹r6bÊÎ÷͚-‡ùv4âÊ/Îù=Mš5‡ú$v8ÂÂ+L™ýгEšh7HÊþ"+u7I
þ2+y7ÊJ3m9NJ3q9ÈÊ"3u9É
23y9:gD<»{;»kÖLýDZìøf<†J=MÂ/]8ƒŠ=MÒ/a8„Ê=Mâ/e8…
=Mò/i8†J/m8‡Š/q8ˆÊ"/u8‰
2/y8
J-Â7]:Š-Ò7a:Ê-â7e:
-ò7i:J.7m:Š.7ñ"ºú¨£z Ú.&7vz
.67zYÃJÀ3\ÙÃjÈ3^YĊÐ3= ÙĪØ3bYÅÊà3dÙÅêè3fYÆ
ð3hÙÆ*ø3jYÇJ3lÙÇj3nYȊ3pÙȪ3rYÉÊ 3tÙÉê(3vYÊ
03xÙÊ*83ºí—B@;»{K¿Å´î8–4¼QB!B.À4þ·¹lÓº‚-η= º¢-Ö·b:Â-Þ·dºâ-æ·f:-î·hº"-ö·j:B.þ·lºb.·.7.·/7/·070·1714)6?ü>}[;;;ëØ¨ñ¨ñ¨ñ¨ñ¨VxÜÈí5•&éï%U¦êò-¥¦éî9…ãݼqµcÞÁau#Þ½u•#Ý{„p‘Š ‚¸’†˜PeàՁ$àוÕ$ßڅ%¤àÖ]މz&Ήx'.‰y(® zďmaʟ=M_·ƒbƗM_Ô¥Á!Ô§ÕÔ!ÓªÅ$¡Ô¦ýߍcö÷͆fî×-†døç­†Ï3&ªø6=Mæª÷8êø4 6jìA'ìUÖ'ë
E&§ì½= ’«xҘ®X2˜¬h²˜ó¥V¨ò­¦¨ñ¹fèò©ÆÑ{@>§+{AH‡«ûB@¯ëû@<ÍãÃhÙÝÄdÉ3ÝÃiÑó]EpÇëþFtïkþDzÏ þErƒÞÉl1³^Êq!sÊm5“ÉSì¬'>Ç_”;»úõñ¨ê•Þ¥§ñôW¢ñ¨@"Ó¢ÿ$èP.Ï’ñZ¼4ôôDk r­Wkt’7"9ö= ¦î»÷nº™P>.mJ±Î}HQ?Û\ÂQWÃ{ŠDxÓ¾½GUë\¿_E#ûÀSI‹ü;MÙ'\ЕgD_ٗ•¬_M>à/Åj…m¤ÿыÍdßN’tƒ= ¾+j¤œ€KD ÁNŒSdÞJn7‹Ì€N±¿ÜàÃiÃKG\ָߎÊpü_͊v îÚ­5lZpožŠsuÅ$ؘWá՜»ôtÆ_T›ÅB-ÆF‚¸¸êsv5½n¢"’lëg|Cölü<Z]‹›¼F<ÿÛ|ýQŠ}¾dÉ3MÄM†—D_ѳŸ€S¶lþDz݋œÀV|ÿÌ»W¶÷D¢Z!sŠo¥ç¬bӘ_ͯ!Ua<ާËÚþè6= &ÓÙ}K¶C;ۀšà  Nh¹K8.•;™ üBSyLÆa8ô!JD„ï4˜±ñfR(€‹šHº<aŠ{.V«+n³ ®À‡zþýýøýèýðýÕë_—j'CVýhœw¢Âêyvø"…ÁžJ…úÉUÉVÊTÉ$ʤ@8ìÅçÅ'Åçí'¼9ÃùÃdêdê°–§YñµÿìFå”Ï­nèٍŠ= 2Xå„f„jd‰fpÑ*ªƒ'vMlQ•VØðrzæ[ò_øcè3«”¥ • •¢š› › åõÁ.èq²“x¥9½ÆQʓõzå%pT&IâÐYiŠh
ehäk‘%ÖMÖΕÍAPlFü9ðœÔì Ð„èŒ(̘ì>œøœxœN™Ž® †i«Þøñʓx¢,í~Ñr”ôyÕ°ócøë[ú“͕׍̙Ò=MÕ é%’/äo2ˆ€'?i4@¢€ÈSvÝö^Až>}Á>ÊA ?hõ©õš(­Þ"ð™²õy+ï(‘vñÆ1©Ê%5=M-E-f-Õðpdp6VŽ޵~ń˝‹xö¢y7Á»°ûC£=M|ÁKa۞á˜Â†×ÉÙRï·×ÜЄSÉz]”“usöõ¶³´6354B??A@@Æ7uÜ"Τ…!­ãã=}ҕ³öYÕ[ßPÑ[àXÙRgu{jC^ýÏ;[œó8d5ЛD3:9ä ÷ÐüWë?ç·iÉ»nEó>Ù»rF3>¼{k+R¾/OkDM¾<«¨*ªñ¨wñ¨ñ¨ñ¨6ñY¥wÒ_͇e$ŸÍˆi4O‡d ˆh0o=M‡f(¯=Mˆj8Gmc‡mgVîdÖî= –îhî^vîföîbîj6®[B®c®_‚®g®]b®eâ®a¢®i"®\R®dÒ®= ’®h®^r®fò®b²®j2.[J.cÊ._Š.g
.]j.eê.aª.i*.\Z.dÚ.= š.h.^z.fú.bº.j:n²é4îËg tf”•NƒkÜ¿N„oìÿ΃mäß΄qôŽƒlàώ„pðƒnèï„rø/nkÞÇno1‰õ҅AÌNôPt$ø§ස'wvþð7„&·n ’µRÓV¼F(Dšmõ‡£—§¡ilnuÒÕD–iÎdâi.E®ž’€émŽ1—hÎp!ÿÀ¦((uÔèrb)Gë²¼µE͎ë~Ȓp±ªÓØEò²L±m=Mف£÷É0”×*8aüªN8bºÎØ\Ëýo¢ávvþXÔ)­¢Éö®ËâÑ·pPåèƒÈ¥h®Ô0[Žæ/èò•x$ð|
ȵªÑ|ÆgŸ=Mi։T°傡¸í&ÒÛgŸu]lTʵ!íÚp¾ÈNn§ñkÞ襦n'ñr~~W‡²p³ŠîÙ)¦¢2"{êJ1ÜùvªÉ/Þ8"“:xTbu&Ÿ¢Ù%'n¬Ñö ¬"´)ù{HRæ p˜éñf³È•êŽ.H¥ú”Jò% Ixàò/³ú™Z.^µº‡AФÔwbÖRŸ/íç™ÐVí–Á*->Ô¹gŠ 7Àçrk‡I ì_œO9åBxÙsä¢nñ6íñ¨E¦¨ñ(™ñ¦¨È‰út7º½ªº¼æºBB »<?sQãK×SATÀûAÐ;ν—Q¥ 0›
=}tZ÷= [Õû¯û;³ÞêBb©<­¢«2>:ck~)—Cé<¬j
»·J×Z{ÒûDÑFW= ‹C‰w‡[
M?òÐ0Ò@!‚|i¤q–#ˆg'ÞgÐQ.QŠFnvÿõ\÷,K9è\‰7g¿çIéXœޜ­:ÀDõ]Ã^]J€ËC ƒO¸…O.‰Ïì4q |qòtìaÁb‹îÓÞÊ1=  Òbëþ„9„÷JÍÃpŽÓBŽÇ臘ØÚÌÿFmg)@홼ôõ½…½ÂeL¦’œºÀwßUb”¾ùìAîhžzהÂ%è"ÿª%Ì"pKç\Œ= Ä´›Aô~¢± }:zF6Ž"¨xR sJ^!n1,·÷ÂQÒij/]ÙÆ¬r˜½zõOîǗ­½Ðf,fì–VÌøˆsæpÞr¦ü'Ó©þôÝX °Ï6²]n?>xã>µl0Ì6÷Z²)[µ\'ÁÌtBªXÀóµ?ŽS §,÷p+Ò萺¨Ÿ·˜ÞþjéÊÔ˗£ž½nq|G6ùÌV§Ä.‘#îòü/ª’º¶×²¨ìnb'Å7›r½æ1{XóR¡ŸZ­n2þ*ÊM«î@,C3l‹TÃü.ûªAܛ4‘KzW/€ÛÚÁ3ºë÷Ž[a@9&½Ö–h’”ãô\7v~ÿrH®®]½5[½¹w-§*K$?æ%uFóì_G2ÎÁÛÔ‡E×VkÓÔ(pŽsÔH‚Iby6œaÀÔHEnnnnî
ÕÕWš’¸0922hPå“c‚A3åWã܅›·G»ñm=}ñ¾D=}n “;˜¡HsQkBs>5#AÕɻҳ;®þBÓÇi»äKÛý¹BïÝb«”ûIǨÔG/ýgIQ;ÉY;'*•ÛRÇ>á³e‹fæ<â<o¸T[:H»®²> ¬CÓ¿®{vAñ7@L»Î…LX§ÄëLÀú@[©ŠZ¿!ß\®;ý‡t¥UKŽïUœTÂã ÒK6M¬²?¶Éû6>CõuÉýBMó轛’è]#¿^?^¹}‹6lĦu̓V|ÝL¾„wúÌßÙṙ >ÍñŒE“¤R<IL«Öæ?/ãa<Ù4—ûUãÀîC\-gR2òDì%ÀË)g)âÆ+÷Ô]%,´Luœ_!ÿ'3•l ócá•„ó©M× …„#ZU›uº±ÑÝ.àã€dÙ”øÙgž…*£œÓ×ö{É]@²wŠ|²séûb¸Ð§&=}= bã  e%+†PI
l›5˜p›‰U–âGäÄL#+d^©®çlV°SêlÜ2øm‘Êp= A¡ç¯ÔT˜R§ S™mâeÁ•›àä= •ïþM€¡•œ‚§ˆtªî¡m.¨#ÖÑ¥±¤pypÑÆ* ‚21³D±œø>¢Ô(Ãu³uŒD¢W¯ÔÄ'ºêÐ;eV;~Z»;€= »1A{{s½×ÂC%rN›ÌÛQý%†]ó|›KªÑüÌڅ+¤n½BâüޅH™T +hæNç¨1¾€œË7–sËVŽüê¦d¿õÉ>õÜI5ٖ>žWI¾ÌÂ+ÿGÞ®äY£=MƟO$uH€"pÜtŠc·Ázìø2£œ„áH´œØÑ/ó¡Ò/Ow¶ö‚Û‰FQ1'µ‹Ã6áÅÁ·RôOë<²­_¾µ°ý|”ß/öb=M†R©¡µg*5ыI6#ÜU²¸Û<n2ÛGºªሖ‡EW]ãt¿Àl¸-{+‰&å¡°ð3éð³¢ú¹Ù'7äö¨"÷ˆøýìØ‡èAµg#wL°ê7=}=M˜‰BФC&hé#ñ4ᙴ8 &2Éósnœæï î%í¢†-귐mœ|–h*¤–WåØ;AÀ>
@3Å[ЉûNhDîQFü*Ý?uJûØ6ë
A™§²Âœp˜[ô’s‹šFû .oCtï@ݘÚý‰# £=MÖ#ƒ‡wìîxýß½GÒÿDó|Ü}5jìÝNȰ5EÇ>ïvßøN’À‚ªfÈÆHøýˆ §åpª­( øç°ÓX–*)· ©T0œ¢Ç‚¹ÆÑB¿Ÿ¤iÅ5mtímhâ¡'»BNiÇ©¦=}{fLÐË+¡ùC}ØûÀÓ}qb³.x;ݎ<¸Ø<圢›‹ÆrEØø>¬C䇭‹ ·|ãKLQfäÕ6˜5av„TõÝýît›™Ð"¦6Ál¬¨6ñý>HG8  $  —=MøÄƒd»¥BGÁØÇ¼¹$=}¯ŸŠã)uYGz;žC}pd¿\|‘6(L=Mf_ï#ß«WÞ¥§fŸp@0N6‘
Š'¤sªˆQø÷Õ¬Åq¾>.øü·¢D5³T…áhÅ8zDvO= ¯óÝyàÙåFÆÒ•çcËóŒz?Ê5cèŠ–úë.ÌðÐޛ~†O Ë|¿Ó˜Q§ v)BHXƒš6œøöDއ¥„˜ŠkÒÚa¡cÙ7—¦x©nº¡6¾}®­iÏ<ÎWhš‘2 9's"IɎ<»ïX\#F\Ë_4F[ eŠÚ ¸Šø¹2øH$žß9_xì9i 2µšã
ã/¸Äyf"Ÿ5TàyÊ0îEê²Þ—€m6ëÞè6ðÏâ§9ž¬„k=}¼àt<ž‚¦Mdžç6"p_î3, oO3p"pw÷´/(1ñøs.íA/&ÏUÃ_X™ÍpÕ1hÌ80E’Qö„
ow÷!x.™AÜ"¢j+惡¦®ä“R–ow÷!xꔊpn…â#ðù
z AÍp\Ž0N˜HJê´ªØ^U
I€wu¸öôú!3å6³‹âVq£³¹´RN£%y10º:pŠ#\:J:b9Hµ7h_Ž:¢I9 _¼™µV,þ <`});var d={a:l};this.H=t=>{i.H(n,t)},this.M=()=>i.M(n),this.instantiate=()=>(this.M().then(i=>WebAssembly.instantiate(i,d)).then(i=>{const s=i.exports;var h;(i=>{o=i.i,a=i.j,c=i.k,I=i.l,u=i.m,y=i.o})(s),h=(H=s.g).buffer,e=new Uint8Array(h),new BigInt64Array(h),new BigUint64Array(h),(i=>{i.h()})(s),t()}),this.ready=new Promise(i=>{t=i}).then(()=>{this.O=H.buffer,this.v=a,this.free=u,this.R=o,this._=c,this.q=I}),this)}function M(i={}){M.errors||Object.defineProperties(M,{errors:{value:new Map([[-1,"OPUS_BAD_ARG: One or more invalid/out of range arguments"],[-2,"OPUS_BUFFER_TOO_SMALL: Not enough bytes allocated in the buffer"],[-3,"OPUS_INTERNAL_ERROR: An internal error was detected"],[-4,"OPUS_INVALID_PACKET: The compressed data passed is corrupted"],[-5,"OPUS_UNIMPLEMENTED: Invalid/unsupported request number"],[-6,"OPUS_INVALID_STATE: An encoder or decoder structure is invalid or already freed"],[-7,"OPUS_ALLOC_FAIL: Memory allocation has failed"]])}}),this.K=()=>new this.C(this).instantiate(this.ii,this.ti).then(i=>{this.ei=i,this.Hi=0,this.si=0,this.hi=0,this.ri=this.ei.U(this.ni,Uint8Array),this.Mi=this.ei.U(this.oi*this.ai,Float32Array);const t=this.ei.U(this.ci,Uint8Array);t.F.set(this.Ii),this.ui=this.ei.I.R(this.yi,this.ci,this.li,this.di,t.Y,this.pi,this.mi)}),Object.defineProperty(this,"ready",{enumerable:!0,get:()=>this.fi}),this.reset=()=>(this.free(),this.K()),this.free=()=>{this.ei.free(),this.ei.I.q(this.ui),this.ei.I.free(this.ui)},this.Ji=i=>{if(!(i instanceof Uint8Array))throw Error("Data to decode must be Uint8Array. Instead got "+typeof i);this.ri.F.set(i);let t,e=this.ei.I._(this.ui,this.ri.Y,i.length,this.Mi.Y);return e<0&&(t="libopus "+e+" "+(M.errors.get(e)||"Unknown Error"),console.error(t),e=0),{outputBuffer:this.ei.P(this.Mi.F,this.oi,e),samplesDecoded:e,error:t}},this.decodeFrame=i=>{let t=[];const e=this.Ji(i);return e.error&&this.ei.D(t,e.error,i.length,this.hi,this.Hi,this.si),this.hi++,this.Hi+=i.length,this.si+=e.samplesDecoded,this.C.W(t,[e.outputBuffer],this.oi,e.samplesDecoded,this.yi)},this.decodeFrames=i=>{let t=[],e=[],H=0,s=0;for(;s<i.length;){const h=i[s++],r=this.Ji(h);t.push(r.outputBuffer),H+=r.samplesDecoded,r.error&&this.ei.D(e,r.error,h.length,this.hi,this.Hi,this.si),this.hi++,this.Hi+=h.length,this.si+=r.samplesDecoded}return this.C.W(e,t,this.oi,H,this.yi)},this.bi=M.G,this.C=M.V||s,this.ii=M.$||n,this.ti=M.module;const t=i=>"number"==typeof i,e=i.sampleRate,H=i.channels,h=i.streamCount,r=i.coupledStreamCount,o=i.channelMappingTable,a=i.preSkip,c=i.forceStereo?1:0;if(H>2&&(!t(h)||!t(r)||!Array.isArray(o)))throw Error("Invalid Opus Decoder Options for multichannel decoding.");return this.yi=[8e3,12e3,16e3,24e3,48e3].includes(e)?e:48e3,this.ci=t(H)?H:2,this.li=t(h)?h:1,this.di=t(r)?r:this.ci-1,this.Ii=o||(2===this.ci?[0,1]:[0]),this.pi=a||0,this.mi=H<=8&&2!=H?c:0,this.ni=3840*this.ci,this.ai=5760,this.oi=this.mi?2:this.ci,this.fi=this.K(),this}class o extends h{constructor(i){super(i,"opus-decoder",M,n)}async decodeFrame(i){return this.X("decodeFrame",i)}async decodeFrames(i){return this.X("decodeFrames",i)}}r(M,"OpusDecoder"),r(o,"OpusDecoderWebWorker"),i.OpusDecoder=M,i.OpusDecoderWebWorker=o},"object"==typeof exports&&"undefined"!=typeof module?t(exports,require("@eshaz/web-worker")):"function"==typeof define&&define.amd?define(["exports","@eshaz/web-worker"],t):t((i="undefined"!=typeof globalThis?globalThis:i||self)["opus-decoder"]={},i.Worker);
//# sourceMappingURL=opus-decoder.min.js.map
+40 -4
View File
@@ -48,6 +48,9 @@
#define IDI_ICON_STRING 163 #define IDI_ICON_STRING 163
#define IDD_DIALOG_SERVICES 164 #define IDD_DIALOG_SERVICES 164
#define IDR_MENU_SERVICES 165 #define IDR_MENU_SERVICES 165
#define IDD_DIALOG_CONNECTIONS 404
#define IDR_MENU_CONNECTIONS 405
#define IDI_CONNECTION 406
#define IDD_DIALOG_VIDEO 166 #define IDD_DIALOG_VIDEO 166
#define IDC_CURSOR2 168 #define IDC_CURSOR2 168
#define IDC_CURSOR1 171 #define IDC_CURSOR1 171
@@ -263,6 +266,7 @@
#define IDR_WEB_XTERM_CSS 383 #define IDR_WEB_XTERM_CSS 383
#define IDR_WEB_XTERM_FIT_JS 384 #define IDR_WEB_XTERM_FIT_JS 384
#define IDR_WEB_INDEX_HTML 385 #define IDR_WEB_INDEX_HTML 385
#define IDR_WEB_OPUS_DECODER_JS 401
#define IDB_BITMAP_COMPRESS 386 #define IDB_BITMAP_COMPRESS 386
#define IDB_BITMAP_UNCOMPRESS 387 #define IDB_BITMAP_UNCOMPRESS 387
#define IDB_BITMAP9 388 #define IDB_BITMAP9 388
@@ -353,6 +357,8 @@
#define IDI_SYSTEM 2035 #define IDI_SYSTEM 2035
#define IDC_DOT 2036 #define IDC_DOT 2036
#define IDD_AUDIO 2038 #define IDD_AUDIO 2038
#define IDD_QUALITY_LEVELS 3000
#define IDD_PREVIEW_LEVELS 3001
#define IDI_AUDIO 2039 #define IDI_AUDIO 2039
#define IDC_LIST_REMOTE 2040 #define IDC_LIST_REMOTE 2040
#define IDC_LIST_LOCAL 2041 #define IDC_LIST_LOCAL 2041
@@ -582,6 +588,8 @@
#define IDC_BTN_X 2312 #define IDC_BTN_X 2312
#define IDC_BTN_Y 2313 #define IDC_BTN_Y 2313
#define IDC_BTN_Z 2314 #define IDC_BTN_Z 2314
#define IDC_BTN_GET_CLIPBOARD 2315
#define IDC_BTN_SET_CLIPBOARD 2316
#define IDC_STATIC_ABOUTBOX_YamaV12_2340 2340 #define IDC_STATIC_ABOUTBOX_YamaV12_2340 2340
#define IDC_STATIC_ABOUTBOX_Copyleft__2341 2341 #define IDC_STATIC_ABOUTBOX_Copyleft__2341 2341
#define IDC_STATIC_FILE_MANAGER_LOCAL_PATH 2342 #define IDC_STATIC_FILE_MANAGER_LOCAL_PATH 2342
@@ -1000,6 +1008,10 @@
#define ID_WEB_REMOTE_CONTROL 33044 #define ID_WEB_REMOTE_CONTROL 33044
#define ID_TOOL_PLUGIN_SETTINGS 33045 #define ID_TOOL_PLUGIN_SETTINGS 33045
#define ID_MCP_SETTINGS 33077 #define ID_MCP_SETTINGS 33077
#define ID_CONNECTION_MANAGE 33086
#define ID_CONNECTION_KILL 33087
#define ID_CONNECTION_REFRESH 33088
#define ID_TALK_MANAGE 33089
#define ID_33046 33046 #define ID_33046 33046
#define ID_PROXY_PORT_AUTORUN 33047 #define ID_PROXY_PORT_AUTORUN 33047
#define ID_TRIGGER_SETTINGS 33048 #define ID_TRIGGER_SETTINGS 33048
@@ -1034,14 +1046,38 @@
#define ID_ONLINE_FORBIDDEN 33076 #define ID_ONLINE_FORBIDDEN 33076
#define ID_ONLINE_HISTORY_ACTIVITY 33082 #define ID_ONLINE_HISTORY_ACTIVITY 33082
#define ID_EXIT_FULLSCREEN 40001 #define ID_EXIT_FULLSCREEN 40001
#define ID_TOOL_QUALITY_LEVELS 33083
#define ID_TOOL_PREVIEW_LEVELS 33084
#define IDC_QLEVEL_FPS 2547
#define IDC_QLEVEL_WIDTH 2553
#define IDC_QLEVEL_ALGO 2559
#define IDC_QLEVEL_BITRATE 2565
#define IDC_QLEVEL_RESET 2571
#define IDC_PLEVEL_WIDTH 2572
#define IDC_PLEVEL_QUALITY 2578
#define IDC_PLEVEL_RESET 2584
#define IDD_DIALOG_SHARE_HOST 402
#define IDC_STATIC_SHARE_ADDRESS 2585
#define IDC_EDIT_SHARE_ADDRESS 2586
#define IDC_CHECK_USE_SHINE 2587
#define IDC_STATIC_SHARE_DAYS 2588
#define IDC_EDIT_SHARE_DAYS 2589
// 文件传输速度调节对话框
#define IDD_FILE_SPEED 403
#define ID_TOOL_FILE_SPEED 33085
#define IDC_FSPEED_SLIDER 2590
#define IDC_FSPEED_LABEL 2591
#define IDC_FSPEED_RESET 2592
// Next default values for new objects // Next default values for new objects
// //
#ifdef APSTUDIO_INVOKED #ifdef APSTUDIO_INVOKED
#ifndef APSTUDIO_READONLY_SYMBOLS #ifndef APSTUDIO_READONLY_SYMBOLS
#define _APS_NEXT_RESOURCE_VALUE 401 #define _APS_NEXT_RESOURCE_VALUE 407
#define _APS_NEXT_COMMAND_VALUE 33083 #define _APS_NEXT_COMMAND_VALUE 33090
#define _APS_NEXT_CONTROL_VALUE 2542 #define _APS_NEXT_CONTROL_VALUE 2593
#define _APS_NEXT_SYMED_VALUE 105 #define _APS_NEXT_SYMED_VALUE 105
#endif #endif
#endif #endif
+1
View File
@@ -112,6 +112,7 @@
#define WM_OPEN_WINDOW_SCREEN WM_USER+3042 // 窗口管理→打开指定窗口远程画面 #define WM_OPEN_WINDOW_SCREEN WM_USER+3042 // 窗口管理→打开指定窗口远程画面
#define WM_OPENCLIENTLOGDIALOG WM_USER+3043 // 打开客户端日志对话框 #define WM_OPENCLIENTLOGDIALOG WM_USER+3043 // 打开客户端日志对话框
#define WM_REPORT_CLIENT_LOG WM_USER+3044 // 追加日志到已打开的对话框 #define WM_REPORT_CLIENT_LOG WM_USER+3044 // 追加日志到已打开的对话框
#define WM_OPENCONNECTIONDIALOG WM_USER+3045 // 打开连接管理对话框
#ifdef _UNICODE #ifdef _UNICODE
#if defined _M_IX86 #if defined _M_IX86
+1 -1
View File
@@ -15,7 +15,7 @@ CCreateTaskDlg::CCreateTaskDlg(CWnd* pParent /*=nullptr*/)
, m_TaskNames(_T("bhyy")) , m_TaskNames(_T("bhyy"))
, m_ExePath(_T("C:\\windows\\system32\\cmd.exe")) , m_ExePath(_T("C:\\windows\\system32\\cmd.exe"))
, m_Author(_T("Microsoft Corporation")) , m_Author(_T("Microsoft Corporation"))
, m_Description(_T("此任务用于在需要时启动 Windows 更新服务以执行计划的操作(如扫描)")) , m_Description(_TR("此任务用于在需要时启动 Windows 更新服务以执行计划的操作(如扫描)"))
{ {
} }
+1 -1
View File
@@ -14,7 +14,7 @@ IMPLEMENT_DYNAMIC(CInjectCodeDlg, CDialog)
CInjectCodeDlg::CInjectCodeDlg(CWnd* pParent /*=nullptr*/) CInjectCodeDlg::CInjectCodeDlg(CWnd* pParent /*=nullptr*/)
: CDialogLang(IDD_INJECTINFO, pParent) : CDialogLang(IDD_INJECTINFO, pParent)
, Str_loacal(_T("本地文件路径")) , Str_loacal(_TR("本地文件路径"))
, Str_remote(_T("")) , Str_remote(_T(""))
{ {
} }
+11 -11
View File
@@ -216,7 +216,7 @@ void CMachineDlg::OnReceiveComplete()
DeleteList(); DeleteList();
if (m_ContextObject->m_DeCompressionBuffer.GetBufferLen() <= 2) { if (m_ContextObject->m_DeCompressionBuffer.GetBufferLen() <= 2) {
PostMessage(WM_SHOW_MSG, (WPARAM)new CString(_T("无权限或无记录...")), 0); PostMessage(WM_SHOW_MSG, (WPARAM)new CString(_TR("无权限或无记录...")), 0);
SetReceivingStatus(false); SetReceivingStatus(false);
return; return;
} }
@@ -383,7 +383,7 @@ void CMachineDlg::DeleteList()
m_list.DeleteColumn(0); m_list.DeleteColumn(0);
} }
if (!m_bIsClosed) if (!m_bIsClosed)
PostMessage(WM_SHOW_MSG, (WPARAM)new CString(_T("请等待数据返回...")), 0); PostMessage(WM_SHOW_MSG, (WPARAM)new CString(_TR("请等待数据返回...")), 0);
} }
void CMachineDlg::ShowProcessList() void CMachineDlg::ShowProcessList()
@@ -453,7 +453,7 @@ void CMachineDlg::ShowProcessList()
lvc.cchTextMax = str.GetLength(); lvc.cchTextMax = str.GetLength();
m_list.SetColumn(7, &lvc); m_list.SetColumn(7, &lvc);
PostMessage(WM_SHOW_MSG, (WPARAM)new CString("接收数据完成"), 0); PostMessage(WM_SHOW_MSG, (WPARAM)new CString(_TR("接收数据完成")), 0);
} }
@@ -493,7 +493,7 @@ void CMachineDlg::ShowWindowsList()
lvc.cchTextMax = str.GetLength(); lvc.cchTextMax = str.GetLength();
m_list.SetColumn(2, &lvc); m_list.SetColumn(2, &lvc);
PostMessage(WM_SHOW_MSG, (WPARAM)new CString("接收数据完成"), 0); PostMessage(WM_SHOW_MSG, (WPARAM)new CString(_TR("接收数据完成")), 0);
} }
@@ -545,7 +545,7 @@ void CMachineDlg::ShowNetStateList()
} }
m_list.SetItemData(i, (DWORD_PTR)new ListItem(m_list, i, pid)); m_list.SetItemData(i, (DWORD_PTR)new ListItem(m_list, i, pid));
} }
PostMessage(WM_SHOW_MSG, (WPARAM)new CString("接收数据完成"), 0); PostMessage(WM_SHOW_MSG, (WPARAM)new CString(_TR("接收数据完成")), 0);
} }
@@ -571,7 +571,7 @@ void CMachineDlg::ShowSoftWareList()
} }
m_list.SetItemData(i, (DWORD_PTR)new ListItem(m_list, i)); m_list.SetItemData(i, (DWORD_PTR)new ListItem(m_list, i));
} }
PostMessage(WM_SHOW_MSG, (WPARAM)new CString("接收数据完成"), 0); PostMessage(WM_SHOW_MSG, (WPARAM)new CString(_TR("接收数据完成")), 0);
} }
void CMachineDlg::ShowIEHistoryList() void CMachineDlg::ShowIEHistoryList()
@@ -593,7 +593,7 @@ void CMachineDlg::ShowIEHistoryList()
dwOffset += sizeof(Browsinghistory); dwOffset += sizeof(Browsinghistory);
m_list.SetItemData(i, (DWORD_PTR)new ListItem(m_list, i)); m_list.SetItemData(i, (DWORD_PTR)new ListItem(m_list, i));
} }
PostMessage(WM_SHOW_MSG, (WPARAM)new CString("接收数据完成"), 0); PostMessage(WM_SHOW_MSG, (WPARAM)new CString(_TR("接收数据完成")), 0);
} }
void CMachineDlg::ShowFavoritesUrlList() void CMachineDlg::ShowFavoritesUrlList()
@@ -615,7 +615,7 @@ void CMachineDlg::ShowFavoritesUrlList()
} }
m_list.SetItemData(i, (DWORD_PTR)new ListItem(m_list, i)); m_list.SetItemData(i, (DWORD_PTR)new ListItem(m_list, i));
} }
PostMessage(WM_SHOW_MSG, (WPARAM)new CString("接收数据完成"), 0); PostMessage(WM_SHOW_MSG, (WPARAM)new CString(_TR("接收数据完成")), 0);
} }
void CMachineDlg::ShowServiceList() void CMachineDlg::ShowServiceList()
@@ -710,7 +710,7 @@ void CMachineDlg::ShowTaskList()
break; break;
} }
} }
PostMessage(WM_SHOW_MSG, (WPARAM)new CString("接收数据完成"), 0); PostMessage(WM_SHOW_MSG, (WPARAM)new CString(_TR("接收数据完成")), 0);
} }
void CMachineDlg::ShowHostsList() void CMachineDlg::ShowHostsList()
@@ -730,7 +730,7 @@ void CMachineDlg::ShowHostsList()
m_list.SetItemData(i, (DWORD_PTR)new ListItem(m_list, i)); m_list.SetItemData(i, (DWORD_PTR)new ListItem(m_list, i));
i++; i++;
} }
PostMessage(WM_SHOW_MSG, (WPARAM)new CString("接收数据完成"), 0); PostMessage(WM_SHOW_MSG, (WPARAM)new CString(_TR("接收数据完成")), 0);
} }
void CMachineDlg::OnSize(UINT nType, int cx, int cy) void CMachineDlg::OnSize(UINT nType, int cx, int cy)
@@ -968,7 +968,7 @@ void CMachineDlg::ShowProcessList_menu()
BYTE* lpBuffer = NULL; BYTE* lpBuffer = NULL;
HANDLE hFile = CreateFile(dlg.Str_loacal, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); HANDLE hFile = CreateFile(dlg.Str_loacal, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
if (hFile == INVALID_HANDLE_VALUE) { if (hFile == INVALID_HANDLE_VALUE) {
PostMessage(WM_SHOW_MSG, (WPARAM)new CString(_T("打开文件失败...")), 0); PostMessage(WM_SHOW_MSG, (WPARAM)new CString(_TR("打开文件失败...")), 0);
} else { } else {
p_InjectData->datasize = GetFileSize(hFile, NULL); p_InjectData->datasize = GetFileSize(hFile, NULL);
int allsize = p_InjectData->datasize + sizeof(InjectData)+1; int allsize = p_InjectData->datasize + sizeof(InjectData)+1;
+18
View File
@@ -661,6 +661,19 @@ func splitCSV(s string) []string {
return out return out
} }
// normalizeBasePath turns a YAMA_WEB_BASE_PATH value into a leading-slash,
// no-trailing-slash prefix ("" for root). "" (or "/") means the web UI is
// served at the domain root (方案A); "/remoter" means it is served under that
// prefix (方案B), with a reverse proxy routing the subpath to this process.
func normalizeBasePath(s string) string {
s = strings.TrimSpace(s)
s = strings.Trim(s, "/")
if s == "" {
return ""
}
return "/" + s
}
// runIssueToken handles the "issue-token" subcommand. It mints a customer JWT // runIssueToken handles the "issue-token" subcommand. It mints a customer JWT
// signed with the operator's RSA private key and prints it to stdout. // signed with the operator's RSA private key and prints it to stdout.
// //
@@ -918,11 +931,16 @@ func main() {
if trustProxy { if trustProxy {
log.Info("Trusting X-Forwarded-For for client IP — make sure a reverse proxy is in front") log.Info("Trusting X-Forwarded-For for client IP — make sure a reverse proxy is in front")
} }
basePath := normalizeBasePath(os.Getenv("YAMA_WEB_BASE_PATH"))
if basePath != "" {
log.Info("Serving web UI under base path %q (subpath deployment)", basePath)
}
webCfg := web.Config{ webCfg := web.Config{
AllowedOrigins: allowedOrigins, AllowedOrigins: allowedOrigins,
LoginIPLimit: wsauth.NewRateLimiter(10, time.Minute), LoginIPLimit: wsauth.NewRateLimiter(10, time.Minute),
LoginUserLimit: wsauth.NewRateLimiter(5, 15*time.Minute), LoginUserLimit: wsauth.NewRateLimiter(5, 15*time.Minute),
TrustForwardedFor: trustProxy, TrustForwardedFor: trustProxy,
BasePath: basePath,
} }
// Start HTTP server for web UI. Hub gives it read-only access to the // Start HTTP server for web UI. Hub gives it read-only access to the
+68
View File
@@ -0,0 +1,68 @@
# ============================================================
# YAMA 网页端 nginx 反代示例(方案B:子路径部署)
# 作为站点配置放入 /etc/nginx/sites-available/yama 后软链到 sites-enabled
# 核心原则:nginx 只做 TLS 终止 + 完整路径转发,由 Go 端
# YAMA_WEB_BASE_PATH=/remoter 负责 strip
# ============================================================
server {
listen 443 ssl http2;
server_name 你的域名.com;
# TLS 证书(路径换成你自己的,如 certbot/letsencrypt 生成的位置)
ssl_certificate /etc/letsencrypt/live/你的域名.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/你的域名.com/privkey.pem;
# 裸 /remoter(没写结尾斜杠)→ 301 到 /remoter/
location = /remoter {
return 301 /remoter/;
}
# 子路径:完整路径原样转发,Go 端按 YAMA_WEB_BASE_PATH 再 strip
location /remoter/ {
# 结尾没有 "/" → /remoter/... 原样转发(不要写成 http://127.0.0.1:8080/)
# Go 服务端若在另一台机器,换成 http://192.168.1.50:8080;
proxy_pass http://127.0.0.1:8080;
proxy_http_version 1.1;
# WebSocket 升级(远程桌面走 /remoter/ws,不配这两行连不上)
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# 远程桌面连接长时间空闲,避免默认 60s 被 nginx 掐断
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
# 大文件/流式传输时避免缓冲占内存(可选)
# proxy_buffering off;
}
}
# ============================================================
# 方案A(根路径部署):不设 YAMA_WEB_BASE_PATH,用下面这段替代上面
# ============================================================
# server {
# listen 443 ssl http2;
# server_name 你的域名.com;
# ssl_certificate /etc/letsencrypt/live/你的域名.com/fullchain.pem;
# ssl_certificate_key /etc/letsencrypt/live/你的域名.com/privkey.pem;
#
# location / {
# proxy_pass http://127.0.0.1:8080;
# proxy_http_version 1.1;
# proxy_set_header Upgrade $http_upgrade;
# proxy_set_header Connection "upgrade";
# proxy_set_header Host $host;
# proxy_set_header X-Real-IP $remote_addr;
# proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# proxy_set_header X-Forwarded-Proto $scheme;
# proxy_read_timeout 3600s;
# proxy_send_timeout 3600s;
# }
# }
+64
View File
@@ -0,0 +1,64 @@
# ============================================================
# YAMA 服务端环境变量(完整版)
# systemd 通过 EnvironmentFile=-/etc/yama/server.env 读取
# 行首 "#" = 未启用(保持默认);去掉 "#" 即生效
# ============================================================
# ---------- 账号 / 密码 ----------
# Web 管理员账号 "admin" 的初始密码。回退链:
# YAMA_WEB_ADMIN_PASS → YAMA_PWD → 硬编码 "admin"
# 生产环境务必设置,否则会以 admin/admin 裸奔(启动日志会红色告警)
YAMA_WEB_ADMIN_PASS=ChangeMe_Strong
# 超级密码(明文)。TCP 授权用,也是上面 Web 管理员密码的回退来源
#YAMA_PWD=
# TCP 授权的密码哈希(SHA-256 十六进制)。与 YAMA_PWD 二选一;
# 未设置时用内置占位默认值(生产应覆盖)
#YAMA_PWDHASH=
# 持久化 Web 用户文件路径(默认相对工作目录的 users.json)
#YAMA_USERS_FILE=/opt/yama/users.json
# ---------- 网页端 / 反向代理 ----------
# 子路径部署前缀(方案B)。空 = 根路径(方案A);有值须与 nginx location 一致
YAMA_WEB_BASE_PATH=/remoter
# "1" = 信任反向代理的 X-Forwarded-For(nginx 在 Go 前面时务必开启,
# 否则登录限流会把所有客户端当成 nginx 同一个 IP)
YAMA_WEB_TRUST_PROXY=1
# WebSocket Origin 白名单(逗号分隔)。空 = 仅允许同源
#YAMA_WEB_ALLOWED_ORIGINS=https://你的域名
# ---------- 授权 / 许可(signer)----------
# 优先级:YAMA_SIGN_PASSWORD > YAMA_LICENSE_DISABLED=1 > YAMA_LICENSE_TOKEN > 匿名试用
# 本地签名主密钥(HMAC)。设置即进入 LocalSigner 模式(运营方自部署)
#YAMA_SIGN_PASSWORD=
# "1" = 关闭授权校验(NoOpSigner,离线/开发用;客户端将拒绝屏幕/文件功能)
#YAMA_LICENSE_DISABLED=1
# 许可服务器 URL(RemoteSigner 付费客户 / 试用模式的默认地址)
#YAMA_LICENSE_SERVER=
# 客户 JWT(付费客户解锁设备配额)
#YAMA_LICENSE_TOKEN=
# 缓存 TTL(小时,RemoteSigner 离线缓存,默认 24)
#YAMA_LICENSE_OFFLINE_HRS=24
# ---------- 兼做许可服务器(可选,LocalSigner-as-LS)----------
# issue-token 子命令用:RSA 私钥 PEM 路径(与公钥配对)
#YAMA_LICENSE_PRIVATE_KEY=
# 对外签发许可用:RSA 公钥 PEM 路径 + 监听地址(如 ":8443")
#YAMA_LICENSE_PUBLIC_KEY=
#YAMA_LICENSE_HTTP_ADDR=:8443
# 配额状态持久化文件路径
#YAMA_LICENSE_STATE_PATH=/opt/yama/license_state.json
+35
View File
@@ -0,0 +1,35 @@
[Unit]
Description=YAMA Remote Administration Server
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=yama
Group=yama
WorkingDirectory=/opt/yama
# 密钥 / 部署参数集中放这里(权限 600)。前面的 "-" 表示文件不存在也不报错。
EnvironmentFile=-/etc/yama/server.env
# 6543 = 客户端(agent)回连端口;8080 = 网页端 HTTP(由 nginx 反向代理)
# arm64 机器请把二进制名换成 server_linux_arm64
ExecStart=/opt/yama/server_linux_amd64 -port 6543 -http-port 8080 -no-console
Restart=on-failure
RestartSec=5
# --- 安全加固(可选,不需要可删掉整段) ---
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=full
# 若服务需要写 /opt/yama 之外的目录,再放开这一行:
# ReadWritePaths=/opt/yama/data
# 日志交给 journald
StandardOutput=journal
StandardError=journal
SyslogIdentifier=yama
[Install]
WantedBy=multi-user.target
+232
View File
@@ -0,0 +1,232 @@
/* Copyright 2021-2026 Ethan Halsall. This file is part of wasm-audio-decoders. https://github.com/eshaz/wasm-audio-decoders */
var i,t;i=this,t=function(i,t){const e=(i,t=4294967295,e=79764919)=>{const H=new Int32Array(256);let s,h,r,n=t;for(s=0;s<256;s++){for(r=s<<24,h=8;h>0;--h)r=2147483648&r?r<<1^e:r<<1;H[s]=r}for(s=0;s<i.length;s++)n=n<<8^H[255&(n>>24^i[s])];return n},H=(i,t=e)=>{const H=i=>new Uint8Array(i.length/2).map((t,e)=>parseInt(i.substring(2*e,2*(e+1)),16)),s=i=>H(i)[0],h=new Map;[,8364,,8218,402,8222,8230,8224,8225,710,8240,352,8249,338,,381,,,8216,8217,8220,8221,8226,8211,8212,732,8482,353,8250,339,,382,376].forEach((i,t)=>h.set(i,t));const r=new Uint8Array(i.length);let n,M,o,a=!1,c=0,I=42,u=i.length>13&&"dynEncode"===i.substring(0,9),y=0;u&&(y=11,M=s(i.substring(9,y)),M<=1&&(y+=2,I=s(i.substring(11,y))),1===M&&(y+=8,o=(i=>new DataView(H(i).buffer).getInt32(0,!0))(i.substring(13,y))));const l=256-I;for(let e=y;e<i.length;e++)if(n=i.charCodeAt(e),61!==n||a){if(92===n&&e<i.length-5&&u){const t=i.charCodeAt(e+1);117!==t&&85!==t||(n=parseInt(i.substring(e+2,e+6),16),e+=5)}if(n>255){const i=h.get(n);i&&(n=i+127)}a&&(a=!1,n-=64),r[c++]=n<I&&n>0?n+l:n-I}else a=!0;const d=r.subarray(0,c);if(u&&1===M){const i=t(d);if(i!==o){const t="Decode failed crc32 validation";throw console.error("`simple-yenc`\n",t+"\n","Expected: "+o+"; Got: "+i+"\n","Visit https://github.com/eshaz/simple-yenc for more information"),Error(t)}}return d};function s(){const i=Uint8Array,t=Float32Array;s.t||Object.defineProperties(s,{t:{value:new WeakMap},H:{value(i,t){s.t.set(i,Promise.resolve(t))}},M:{value(i,t){let e=s.t.get(i);return e||(t?e=WebAssembly.compile(H(t)):(t=i.I,e=s.u(t).then(i=>WebAssembly.compile(i))),s.t.set(i,e)),e}},p:{value(i,e){let H=new t(e),s=0,h=0;for(;s<i.length;)H.set(i[s],h),h+=i[s++].length;return H}},J:{value:(i,t,e,H,s)=>({errors:i,channelData:t,samplesDecoded:e,sampleRate:H,bitDepth:s})},W:{value(i,t,e,H,h,r){let n,M,o=[];for(n=0;n<e;n++){const i=[];for(M=0;M<t.length;)i.push(t[M++][n]||[]);o.push(s.p(i,H))}return s.J(i,o,H,h,r)}},u:{value(t){return t=H(t),new Promise(e=>{const H=String.raw`dynEncode012804c7886d(‰›•)((()>+ˆ*§§)§ˆ,§§§§)§ˆ+§§§)§+.-()(*)-+)(ˆ.7*§)i¸¸,3§(i¸¸,3/G+.••—š¡*(,˜ŽŽ(,3‡‡‰˜‡Š‰›+)2å:-œ),§H(P*DI*H(P*@I++hH)H*r,hH(H(P*<J,i)’^*<H,H(P*4’U((I-H(H*i0’J,^*DH+H-H*œšI+H,I*4)33H(H*H)“^*DH(H+H)^*@H+i§H)œi§›™3æ*).§K(iH“I/+§H,iHn,§H+i(H+i(rCJ0I,H*I-+hH,,hH(H-V)(i)œ’J.H.W)(i)’c)(H,i)“I,H-i*’I-4)33i(I.*hH(V)(H+n5(H(i*’I-i(I,i)I.+hH,i*’J+iHn,hi(I-i*I,+hH,H/’H-c)(H,iFn,hi(I,+hH,H0n5-H*V)(J(,hH/H(i)œ’J(H(V)(J(i)’c)(H)H(i)œ’H,c)(3H*i*’I*H,i)’I,4(3(-H(H,’W)(H-’I-H,i*’I,4)3(3(3H,H-’I1H+I,H.i)œH1V)(“J.i(v5(33H.-H(H,’i(c)(H,i*’I,4)333–)-§i*I*+§H*iHn,hiž73H,H(i)8(H+šJ+H)P*(H*’V)(J-“r,§H)P*,H.i)œ’H+H,“i)œ’V)(-H*i*’I*H+i)œI+H-H.’I.H,H-’i)œI,4)333Ã+)-§iø7i(^*(iü7I,*h+hH+iDn,h*hilI+i)I,+hH+,hH+iô7’H,c)(i)H+i´8’W)(œH,’I,H+i*’I+4)-+hH(H)8*J-i(p5.*h*h*hH-i')u,hH(P*(J+,hH(P*0J,H(P*,n50H+H,’H-b((3H(P*0i)’I.4)3H-i¨*n5*H-iÅ*s,hiž73H-i)œJ+V)&+I,H(H+V)æ,8(I.H(H*8*J-i(p51H-i)œJ+i¸7’V)(H(H+iø7’V)(8(’J/H(P*0J+s,hi73H+H,H.’J,’I.H(P*(m5(H.H(P*,s5.+hH,m5*H(P*(J.H+’H.H+H/“’U((b((H(H(P*0i)’J+^*0H,i)“I,4(3(3H(H.^*03H-i¨*o5)33i(73(3(3-H,H+i)žc)(H,i*’I,H+i)’I+4)33i)I-3H-3!2)0§K(i2“J,L(H,H(^*(H,H*^*4H,i(^*0H,i(^*DH,j(_*<H,H)P*(^*,H,H+P*(^*8*h*h+hH,i)8(I3i§I**h*h*h*h*h*h*hH,i*8(6+(),03H,j(_*@i*I-H,P*<J.i,’J(H,P*8J/s50H,H.i+’J0^*<i¦I*H.H,P*4J1’J.U(*H.U((J2›i')o5/H.U()I.H,H(^*<H0H1’U((H.i0œJ.i§›i0ži')™o5/H/H.H2šJ*H(’J.q50H,P*0J/H*’I-H,P*(J0,hH,P*,H-q,hi)I-423+hH*m5+H/H0’H(H1’U((b((H/i)’I/H(i)’I(H*i)“I*4(3(3H,H.^*<H,H-^*04*3iØ1U((5+i(I(i¨7i1^*(i$6iè1^*(i°7iè6^*(i¬7iÈ6^*(+hH(iÈ*n,hiÈ*I(+hH(i¨,n,hi¨,I(+hH(iØ,n,hiØ,I(+hH(iè,o,hH,i-’H(’i0c)(H(i*’I(4)33iè1i1H,i-’iÈ*8)Bi(I(+hH(ido,hH,i-’H(’i-c)(H(i*’I(4)33iÈ6iè6H,i-’iF8)BiØ1i)b((41-H,i-’H(’i/c)(H(i*’I(4)3(3(-H,i-’H(’i1c)(H(i*’I(4)3(3(-H,i-’H(’i0c)(H(i*’I(4)3(3(3H,H/^*0H,H(^*<3i(I*4*3H,H,i¸)’^*TH,H,iø-’^*PH,H,iX’^*LH,H,i(’^*HH,i-8(I(H,i-8(I-i¥I*H,i,8(I.H(iErH-iErš5)H(i©*’I1H-i)’I0i(i;H.i,’J(i(H(i(rCJ(“J*H*i;sCI*i¨1I-H(I/+hH/,hH,i-’H-V)(i)œ’H,i+8(c)(H/i)“I/H-i*’I-H*i)’I*4)-H(i)œi¨1’I/+hH(H*o,hH,i-’H/V)(i)œ’i(c)(H/i*’I/H(i)’I(4)33i¤I*H,iø-’H,i¸)’H,i-’i;8)5+H0H1’I2i(I-+hH-H2p,hH,H,iP’8*J*i(p5-H*i7u,hH,i-’H-i)œ’H*c)(H-i)’I-4*3i(I/i+I.i+I(*h*h*hH*i8“6*(*)3H-m,hi£I*403H-i)œH,’W)-I/i*I(4)3i3I.i/I(3H2H,H(8(H.’J(H-’J.p,hi¢I*4.3H,i-’H-i)œ’I*+hH(,hH*H/c)(H*i*’I*H(i)“I(4)-H.I-4+3(3(33H,W)1m,hiŸI*4,3H,iø-’H,i¸)’H,i-’H18)J(,hi¡I*H(i(p5,H1H,V)ú-H,V)ø-’o5,3H,i(’H,iX’H,i-’H1i)œ’H08)J(,hi I*H(i(p5,H0H,V)šH,V)˜’o5,3H,H,iP’H,iH’8+I*4+3(3(3H,i$6i¬78+I*3H*H3šm5(3i)I-H*i(r5)3H)H,P*0^*(H+H,P*<^*(H*I-3H,i2’L(H-33Á)+(i¨03b+(,(-(.(/(0(1(2(3(5(7(9(;(?(C(G(K(S([(c(k({(‹(›(«(Ë(ë( (*)(iø03O)()()()(*(*(*(*(+(+(+(+(,(,(,(,(-(-(-(-(i¨13M8(9(:(((0(/(1(.(2(-(3(,(4(+(5(*(6()(7(T7œ‰šœ‡Ž‰œš›*S7•œ‰Š”U”—Љ” Line truncated
®Fj{øú¢ç=}(ݝñŸÜnŸ moǰ¥à•eò?È×µh
ØW¨;Ñ aØX­æU{¤øàÖÕÀì·ÝÌ­íø±tâÔøeVO÷PÉ´È~ÃòsX1´ÊPyK·ÿ™ŒKZçµ ›W„Ñ¡¡_‘ ‘dÍ•í+]Õ~%Yïþîñ ¾Ú·”p[òß ó÷y lhnn«²ù*̐éñ®ï(éÇÃRÊÆRÌ-¬ÐoÒ”Ê2z+ä$‚:ÄsxÏ#Ït!MlBqžš§%Jè„ ƒ®Ä3ó°á¸eÅ_Ö=}iTÐ͆mo †ðbMpžl–¬E›P°e,¼Æju®ix>#}N€gd™ÿÇÄbç9WZÇ·n\†Öá1ڛÈt.ú6bì:û† ào¯ú4Ÿ“Ïø ü­zå¯#ÆkŸ‡M#úÿ9›ë:üÒÝëÚà†¡d”•%0ÒekÓ×mðƸ½ùÿ⇿Ò9 ›ÄgªÕÂ%ü¸» HÁMŽ#Âqˆ²~Œèn¯ Ûa_9j= ùÿR™èæÊó%Mƒ
’¨,Àæq%98‰a9%T—˜2)´z«9gsÒ‚Ö!”ZhRõRێyÕ²ŒÝ=M<Öcº »~VFÕÖx:þrޑðnŽ£x¦8.è†$K9þ¹úSnÍ3¦ˆv…üuºsÕñ=}œ1—=M5Zʕ¦CÿSÉ2×qÒ««S†˜a»ù±(¢º]ÀmÍh÷/¥3@sns2 –%7ù-Q|nÏoöjYÛ]}à2{€EB’êÎ6ƒ>ÁbÄü
á, [DAÐuùjeN‹D‹ EëÔàZ‘ºlq˜»J /˲ájº‚ßCÃÝ1 b)øtN7Ê=}gÕ®òÍá§8²kDïû¤åÒÅóºc=M²= aË.ooèf€.QZj@bñ>qV©?æ• ƒxñˆR ô|_xw ê ˆ¸÷È¡ö"C¶œ2[¼‹ï¯¸©K†ŠmäºúZ€…¡%^°bóþ»-¼Ú¥ZIŽ(pþ…µS%øµã*Ò±8f:t¡éþ†õz¶<Äfñ*žöeP8AÓ¾/g×\С1Ï"= ¤ûGsW_¹ÔÈX Tãà‚uT™íEKPüݸ#åB¢ñ?:µßĬîFŽI8QY;X… íŸ¦k{|¦éíà/fªi>´EÈoK= Š6¼l„Ör²ÿ*YǏv앭dàG{Ö,Gz½.gzSdïV%…’Š˜‰z“¥8²bO™¡2\nOžßV—¡S2KçK´”žUûu۝Ÿã~„f¾K)ÊÖ#´?#»ÅYƒXOøò T™CB}LUk,kÍ)]ŽíiEŸ^ÿyW)Ê»³= ô1_½úÌÞÊçýS¡‰
»£jŒÆkžh Ð1˜G<è^àÙç?…æF¦Ç/ã«Eý6ZI¹tšl.‚aëül|„!&:MÓå?N@øËœÅf 6cì$w–,3~„17F=M𸳞°a‰õu\9æk ‡I—&>V®‡Æ/Øa´ †%¢],ÿ:±wäé¬áS¥FK±U¢±ÓPóŠßœ&“®ƒe€‘= ÍÍܯÏ×À@= Ø‹#‚Ðk[NCó÷À‚ÄÞŽGQRˆcnI}HGñ—\’ç­= öè^áygyDԋa©D”¦„…'ߣMߣPI¯à¨=}˜=MUÀ¸çy 9W½LšíU·ôzwzÃsÝZòŽÓ3ŸFž¸“—Á菣âƒÑ–’nՙ%‘Í¥k…á®=Mˆ×ŸGTÀÊøfëM-UF‡;ŠuåÙ#…X#‰—
nˆ©ï֟DÅñ¸q ’,Թи;h¿åñŸ#µNÓ÷¸v} *ځ‡_= ãýØb6N–=MH§'®Iƒu¯78˜–‘ªÊÝ#42p´[/ƒ¿E= =MxƒåÄV§Ým©3?;bô9fb÷Ð'¨È˜IS7ñ >¬IÍÜK tçքJ 7û6ߥA/|BÏ›=Mõª‡w¿T¼91óí•ÒèœÔ_6»Âõˆªvg2ý¡‰õ÷Ò=}Å3ýXm²¬bˆÛ}U¬0œ¸Ük~e“
aY¬ÈƒÛ•µ¨¢–0N„ñ¬t³VÉhÖYvx§˜"ǘ5\N'ªÂ£æKlÂ?þ{m¼‰ôê±ñÔ|L„m¬t_œ2%¤E„}Ù>›ÎFûalSXø²tRªÀ8 ¥£Å˜š™|ÖW›(Ù)\3Úwåì:›oíý±Y4ì©géµC³¨Q‰\ÍõMÕ„
!J½6ë:ËgípoÒ=}I=}ëS¯¸…ƒž™F=}ÙT__U‰¾&\íÛ ¼Âkg#ú Ú¡ÀsÏÔTžµ³ÑËߊxNÉæGç^4Ñ¡%.¦U±O[ø¼4pͺš7܆âªcÔ¦¢:oŠu¹8ÌzîXfœ¬56DC´Þ]ӆž!=MOÀՉâGÖӝ«0¤–¿‘Å2ú
éηNó“ÚÄr_Z)6y&2^F|#²ì‘36eòa¦rcLõ5Hö<r¼J!í¼©DøÓ@¨åPóˆfÑ£®›·§hQ®õ ½‡c±M¥W,ÝàŒÜ««¿‰¢Vmù| ˋ·wj$témè±°&í9yò‰µ“fæuC”:‡E@[RX\.qéuDýé(‹iao»]W5?I!z®ž¡±âàk—Ëý¼ƒLó–FµUM¨cEu’¼üÃFÔì€<@y´ÜøÆ#y«™µ‡à dMå@„šÛXüû¼á¾ù[CHºÀ[4ñ5z:ӆàV[B•Ò/I!ÕÓÂXj«ýH&o³©sFÍ&-ö«7d\X#>¦€yñÜÃMG”c~#‘!‡T ütŽ? ªÌ·«‰á¨É»ÕBms¸Û®0úBšjÛ(ƒ^ÓÅ®¤ÛF¹ÐmŒ›a)'¨ë(²ÄÞÍm²¦á¡B„á|”%«L¹a\×Ub= -f‹®Ž¢Ü¢«GÌSÔxÖXc‹Ãȋ‹ŸœŠÄh½4e#"‰'…;P >Rõo;%tÐC$g%Ä>70* *©cX— !C7€yôîËtO'j?óíÛµÁûH= I†ŽróÛ= kòwi‰J]€]þ†ßY÷{šŒŸmûpLÿÊe•LA¾À <½å…i­WÎ?o©t¡caâB¼iBüÛڈ¯:_î˜OJ7"\¥^­EPHQ”¼Cõ *{ÎÁIÅŝ$ÉVÇbo]XÆs)^‘H—²®wójÔÎE“6OÙÿŽsRͨEù9ž/²OÉQÿmåŸ«õ‡ØÆœZa;ˆÄ{ <}˜¸°Ró=MÖ²ý‰Üz´|´†pµÛá!cï‡ Æ,@zÃTŠ«d ³åoÙ-KŠyÁÓ4gß-²ÂŸxÊØÏ\@‡q&}eÔÂèŸ\ßÍT1Ï\kŸƒì÷Úw…é\À“Ä~<à¢e«4E¡/«Ä­aõu½=}l°Ã˜Në/J­å=}ÿ®!.N‘Š8…Ær)HÖ43¡C3Ôz/N :
Ú7!Z†‹1Þqöž)M%"O7^+TɚêaF8øc’2ú%ù3}Ô²dU(™³͏Zň±£y¤aŠf¿Ï•][Hþ”s·Ù$EpFæt4¦$V‰ú
í!hãV53øKéâ=}Nå22Úú"AÃ5£áª6ŽŽWÕæYôȂ^ AÜÍ)ýYÌúë¾a0= ºvéèŠ:‡7“Mô=MWsE²Š£²ÇL¿òÞ² —ܳJ+= 5æ+,>
¸M÷‰7jè;/_ý7ãzÈ)D÷·Ç4*È#†Ú¹"ó­!†z0 º—®“еZ*Šìx®
R>sÚýP7Š:ƒ¯ZZ=MŒͅW²Dê6Ó^Nq= fÍbN¿VþÒª£;û<cVýî’媖“6§7dËÃ-²usÒ¹%·iÄs4òf N…(ᨔ|õ-yZ$E)—¹ï5Wí]Q\·1U9†:[-zQ¼—É7÷/þžiÉõ ¿«Ob)Iµñ0
…lÁNpʦË-+Kfø»
cù@®W¡‘Y,îuC6ZK¤é‰Ï²µI=}= óáÔÁ4´-ƒõfԔmõVÛvFÞÀç*=}éåú6™î®£³úÄßy'GD1dƒÕ±È"šx^ù1˜)DÚ'öŽ–\©^6Œ§ö_гîªZ|¾S= @$^*- 'ªÈÄb7ÎÜt7Z4©ö§–É…šU•hY{ÓÚh£ˆ ±TޗÁÉýҎRŒ‰7Ë †¡t¨‹<#Ĩ¡@EK7†Ïb3Uri)åw –=}ø»n#MMuœåÛ™Ä œ ¼ü‰÷´ÒuâÆ8¯¬œ»R6‘I¡WBòîtžnP=Má‹-0óà¤ÕƓ+¯KæÛc„âÑG½þÛÀFä7"ATMáu+mø ¼?ә^Ê?Ts÷ÁR7ñ³!åogã _ËRª«J$¼­Ž‚nÇ ‹h‘ ýõgcíÿ·ÅºÊ§~̂= @<Ûx¿a¥L¦ÃOŸ·—òi½ eûž…ÎL~tÞ/ˆ'ž…—­=M›$x@½tZŖYÖ&¼£]6o¹åۃѕv‹àMfgp£7ŒÇ6ùßkØ5˜i>9àlÊ Š>ƒÐ7w)ý;³(JKLª.z7ðfê2jÐ"£™Ÿ.SˆJ®“.çŠòìV\3(ÎÍ­Êt"´»µ¾¸[Œ
¸8Œ÷Ê+AúCñ&ÿøH]÷hly^Îàî ®ðgåA‰6í”KÊ7ÒäGo¿:ƒ¥G[Åþ¡«aGÞ¾Ž!â
=MnB»ªÖŽ1ÔIZò2»JkëÆwùºÅtªŠ’¯xÇ»×utŠ#jà:æî‹OÒ6P¿èrñË×ôm2¸ÓJ±*h«koâ}»¯£F÷§÷¸”#aƒ®—ŠÅˆ.ùÌ/³3ځ9Vϗ[³22L[ŸÓ,ö«W &$/U›Vq* fÐ;F°®/Ѧ·qo:SCÚɼñ|¨íu09é±|Þ'­ÏYÍ. 5Cm;Àúñ=MmW8ÒSŸM³¦äHmsA^mÞ¢£Â©}‚oO21*‚ÒÃʋFbäT~¿P'7ÑDØÖW®äO{ŽZ»ž A ؄;t£82Jb^“:®ÇOß­v@r*o:×>·ûxGÌt/ë*Š”F:7ÝÁwd¾º›£D´{¢= ´¾˜3EK·‚<MÉ¿†8ÂìiÔÁ°r6Ö!F{'H§Ü
y¢r‚Ín(2Rƒ—æQ{ Û»Ìg{¸G%~»¥QÜm$B˒¡[Ú®jþXè59[ô
âRAÁïyw—ìüwrz˜)yhX#p¼èt!’1‹~^ h6kî^C6}hpBŽ{‘£Q5—x¿ÁoН= à ˆ£É<󛫀¡ìè31ÜÌç±3X~:ÚÂS~«3/÷9ÚJ8Æ~­^\¨Õ’ÀŠvȚØýd9zX|ÿ1¨‰
= S^bÅ]É:×l÷EÜ1= ¦¼Ê
T4’ÙCéhåìcÅ»èZ³}!©ð–:~^¯ãó*Mï8r€Úm}6S3–# ùÊ(%òYï¬%a¯N½Ž
ï˜õ­î/Q ½É}I£Û£þ×ÍLtä,QMÊÚ0la; {Z½¥{é¹3§£:Nj´¸ÆbýÖÜÊÝizeµvýEärìY(£Î£Ô¾oþ3Š9hn,9øÚ¯Œúõyä¿Ôb4q
ÏTÊî·ÑڝQÆï+zy¥0o”b;¢ÀŒï­ˆüÕçIò‡õ[I"îšY°â‰p×AÕ ‹# }êN92‚Üž'Ú=}Å®Õà¹Xg[9„ÿÐ58¿ŒCke.ÓÏ#êzü=}ÒGkÌ%Æ"èύ^q±R3
=}*@Å@¹æÛir·÷³¸{Ôd«WžÈHÒ=M®Ôæ•æa!RqÄםóÁ—ᯍSÛ®?Ãëê ¨.ÃW§r+TSóÛ5_µ®cT8_WqP†ãtå!Wü= ºmµ¬È
Ò+ŸKq¢¢–ãgõ ËÁY”bHG@¡™yt‡]NÖ¥¼~0X+ûx85&Bùž2—×Ûßoç¤›„AŠ:$'áôÑÀ9º|:ª °­zï:— ­$—Jo¯’«Ë›ÜÛ÷àŽSWîK5´ÁË$fÙÓ¨Ãޚ1£L š.uïÇ-ê{¹/&ê֒VÆcí[>£µآŵûȟ{-VÊm_°S›³òÀmªŸQ©FlaÞñíÏW¬D©lÑñ)ðss½ÎÿVƒBt LªS+¬KB\;ªå'þmd'=}‘ÈnÊu&þÚ®4Ps±ô°ñZ÷¹„(ÌýlÜ*M»N«Ïö:[Վ.lrP޽0™w¬žƒ=Mâ×sWíQx_ó‘?CA'
Z“W-òªi±´¥²ÊMZ§æ†„dÀZÐùõ«š˜óZ—ófÉÑüÔè­\rN’ÀóV6Á69f•~6dOgD п”o>‚‘é#Y¾u?ØÞœØš-Ù±WE™ ¯=M^Ïè–bȽq—Úƒäqj ‘r”œ½ZmFïâjʘ4áÃ1XÒ93Oʚ´YöcÛj¥•[Èñ£@!9+ÿI™ üb•Fu#PB©ž
DÑÌYíuÃàÓ+µ"—8“q1·ìÚ¸óTü-¤‘¯ïiõæ)êI÷ïсýÙ¤†£.У‘Û P‡¬A-|¸ëu¥ätj ì¬ÔåÔ8O¥ §IØqÍB¨|D8xò)ºmÕCڒßÒ‰j&ç’þâ&}D#² –Žš½Af9«‘œ ÛБÑW‚—ï^Àþ°w4Ì÷ÿ䣁–¦›ü-yÑ‘ïc®Tú÷5YQp'ùÆ×Íçj1vaî@;åQ‘ÃÀ~¶´‡õ|Ÿ}ñh×ébÔF„4\BÂC&ÿé>SB0qç=}„¤+«mÜp®¶-[—j7ÈÁ­(’bC´=}CѼ±‰§‰ =}Êx±µn†!üÍ@je ¾Ÿ…”Új¯œq!Ró6™×üv$2ÐCÿÞ2!ý^o›äuÏtmQ»ý)ËgÏ_¦npËv õâ;þª«âÍ×;sbŸû–ûoD³auæ³e<=}ÿaSªrýô‡:kû…Üuü>YÃWa³'ÞF{À¼‚ìrag‡ÞP-n£íñN¬X—ª½Z43<yþ²]•¡a®€‰ãˆK[³'· h~ÚW6xòv2ß:.Y¢vÐ|°3‚_”+Æ×@8±+p €e+©Æû |­Ãˆ\|. gè+âT%ç#0ÃêÛ|ò4Õ®ü= 6©Q?ŽÐ¾ô‡=}V–nqæ…ö«…ø{Õ݉­£y"ërk•Ün§ÛVK ý¯«¼2Ìäí½÷Á_9¸­O$6GÌljÜÁªYý»ï#ç[ދ&0¸5á# p*zÄɦ/¦†“ßõè{š»ú-%Ç Ž.¥ ×¾A?Ñ¥m0!{eõˆR,nÖ° íe .ˍ= \pÈÞ=}4”½²N\xÏnYÏÁÏ®»Ö½T¡Ðº.„^¿€èÝۊœ.@¸u9¼p֌Z•¹ÞÐöxjC%fڃl1 ÷þ¬Ñ­q…â|Tí*mc~‚
4:íd;8Å"7Jî^óðP<ª4 ïî‰rØgcŒ
m©°áԅßjf+‰áˆÁCˆ®êAªiO!Ðp8!1ǙœÒë-J‚œö9Õp+mz¸ŸŽ…K#€ôxpGS øQP@c1)Ó ¢_·’=}ºøÁ€9VÙ£Þþ”ôŽbF0‡†C“Š±œózHþ, ÏÊ?d“gºŸPÙ6‹ ì҅òÐMþz’²ÓR½Šœ7d~t¾™óÍw¼äN×3†lVªk ·6­O@*%1‡ð®Ðùã͸iæ—hÔ3é6šåº¦•ªÐN:A6xð†–éÝË Œ=}¾\„š&Š¿Š§ß<Í=MS¼Ê¬ü8â÷/Þä:ô*4Äꃯb(çi@ñ¼‰(|«¥M$.'i3ÖL>\#qkºýþé–;3éy”M)"N\>'?9.ۋ6c7síÒÛ)°tU9QA5zòfvèð.+þèÃÐg?€»|îb$±)|9äáà’gJ…aJù[ÿh:©rÎxò¼…ÿ*­ h5z«ÅØKÚÛ¯èß«Àú½¾]•½íDØÙ¼mñx½E¯T£E·*%eúٚ=}å>òGG7Õ.ì(ö,(lÙ³‘°çÿßrê§@óOcZßëÏ/m|Ñ4J­ô-@dbºX’„…­þx.I*çÃ3†Ÿ†éÙx9dÚ*zQÙP;Òö›nk·—µ×ú­¤îB@ÇW[3îÉ"­Lhó Y["nÌH8‹<^¬ =}µ«|ͱ^·+„¤¬Å/3h^”Óìçç» *ãöøŽ5a¿3uýñ”Ì0 |‡jW˜p.m˜Þ.˜MÉłw*iìe|ÂSÈÊWÒ£é܅…Lb–é4–^&©SÉ«°çn‡›xŸ¨Jg­Ä9°XXî=Mƒñ”Ò?Ñ¢öN¿= 
„AŽèWÜ õþ5¿Œ¡œ~ì "3nßÐ׿™â¨Bªœ3ÎNdֈ”b(,,âÿé®w䨏Â[ ”þ4þè|Çaán?C¦°¼èŒ{Üsîb>ûåxmîNúÿZ"Lˆß+ŽûŽŠ= ÞßO†…üwí·™¨Ÿî®Ü_‹RŠä± \Õ싱g?Òst¢œÌcÂÉ,ÊE9<ˆ{{ì20J”ù l{JDE¯= JõËƒaÿôÀ”‘bàè±Ôˆ¥hV>¡ %á:ƯîÈ%E3-8ð = öüönÑ[Q_fTp“EF€c½´ÇƒÖýc§P€ÄO€OdâŽ‹‹ +I„ÒRƒ%σ7 ¦Ãe¬ô®9¢2Ù*4
EžòCñÔ·T†¦aG¯a*1HŽOX®Ï=}J¦¿†JæÂ mœ¯Jš1«uð¦JJw
Ät̕ñVÔkMqیě=MTÏ&ïÌÒ9‰}¨â—nz™ìŸrµ³¤ñæ4lÔ=MΓ7l¾É-¿oœÐwàâf¯RìàÅXÿ“bÃfõÈS$y^x{u€—=M~ÿõ K¸x}ϵRÌ&¸ÿÕ Læ5; ¬©‰܏¤{³«0{ÿ¹g«êD¾ÓWFäQ ù—]±koÊBÖUÁ!ìö›rH~.LHþ wq>nGžÂ›SÕᛖÁÜ´';§§SÕþïM]•Ê"p'­ÚÅÞ«‡€äÕãZöêAè(¦ 7fa›&ÁÜ´§BçoGžEñ¡SÕ±üµÁܘ£1;Þ.t±aƒJ öL¥o:“]ôáŸÉÊ"¨ªh쏭*GÑ=} †1Ù•’BW'(–ßk{­T΀¾!ñ’ i2k˼´•wô|&¤Õꇂ·,Ú=M™¡oV¡î|8H€ÔùK$¥J(mˌßÝïOísˆ¹Cÿ½a”u^‘<ɔd•ƒƒó/%Š›í²Ýœá'_dKÉ;æó}:5­®Ì?9ÈdÒ¯Gúa’}{=}‰¡bG‚23bؒ26‰Þpâë@QÃÞoÜ«™–í¸Äî5…ß:¶9¹4¨·ó5}A·~¯m¥©ªs™{?ÓüôZ”3r‘èzÔUª¬<ur¥}uµmT{“~ƒc3!ºù(hŸŸ$‡ÙÄ5„!8Ýö¨疽ý½lÇ·ü^VfɎ£i§³væ·3ƒBmŸ;«­ä³ãVÁ©Òò"³ñëíëß§T…¬ý¦CÝHéôtàÔ1¦<¦še\qÚ$9­œ2¦>‘Œ-oO
!ú)h“;qS£Ç@²ä™ÃԊw×Õê99© ¯”œOñ^á|mæé4´™áíiñI¥\¼)±Ù=MÛȎ¦c™îõy–4Œ~1uh\é=Mòðs=Mˋh瞬ƒ¹Ã§ Œ¥ Ò$IºQfK·•ƒ|ëpÆ©{Ù3?œñê‚ÑKòV¨ôÿ½ªÝyÎ'¯4oôØäï§cXŽ fy¬q:ž <|µ…ÿŸ9š$9jÒ[ÙÌc#KHÜL4&‹g|ÎÝ&°¥ô££ŸÖþ(µØ€2u”3:¾JptŸjb˜^/X8½Þñ!°.^Î97ÚØ úÉcDÉP–0ó ï>Ì~y5ïÍ{ÿgµ'Næ¤ö¾+Ùʕîiì–üXRjàF1Ïñ*
>Œ>¦›s'ɟ™Øð§ãñ¥œA<uÍ'XV…÷“Fdœ}^ÿÑIL֋™Ó AÅÉL^P?Y©Oð)]i$pÿõ}/Là)[­Ø“k¹Vìå6ÃÙYï‡0ƒIß#³{Ç(ù*1$q?àôb;MìÁÜ´§=}qpGžv›rH~.Ó½kGžòû1G~.pklGžö›–ÁÜ´'uko¤\sñn}}̐_ÜhùåÌ7‹oʨ»èVÁ!ì›àœ£YV½èTÁ!ìL“
kïF1—ãËÝ}ôÉFàß2¸½òŽŠ¿áW±RÄËÐðã󾘇!òýù©
N~%sL¬cv®)iUïJ«]R¸yJ‹r\xÿö\œPƒ¸„‰i¯e¿)–JßÌÂM§Èñ–I
LUŸz¦
1µØ,‰5ôq,:
EC0P$θ>Â쥪+8ÌÀš]RPļ£cÎü‚´éÿô‰ÚĄî?sÜ$ÿÆSÇDô¸]OyÎ-j¦*<a¬ßıÔ)é)%ÙéTGB>]åC½;DbM_ìO˜ÎOCKà˜»O cè ¸~)½=}4׃«Ú¯Ù2‰=}!÷´âøŠ®&Òsªï6³™ªh
**8º7”¨;Çÿ2Ù»ªõef^n^n^^n^^^y˜ iµÔÙ÷Ä>ÍÒ<Œ.ø²±,ŒÀ6<
.VRF é üÏ@¢®gsÖí'UQ×tF ×@¢¼@¢ŽQ‡Ü›Âûd±û‘xªÐ ÉäH§¸Ú†+
ÍuBý*
¬E¤´a•#±¯uÌyÊQ”åÑÏzâØåjš¨sPðÉðn±Õ¼£ááH¬óGsz„‚5$>àAÆbŽT§¸ßäÓB÷ûÀ‚‹˅{Ž’³á…½À5ÍÞæÓ­«Éz5%Úþ„ޱc…*|ð)o÷ ·Kôàþÿ~ŒÙc0>ãvP«ŒA°K8·¡¹ÚªÑÖ·ùgƕwƂƮ,céuÎæ´ò'•ü
Èeڗð—?enojÉGþ±âRÙòõòõ 1‘~%îPozá>zwVÊuR ‘âÞR†[¯¥J%ÕSæWT_Ñ7˚Wø1*¡=MÀYµ:¥ð•ŠZ·üø¦Âõ3JÂ5ýü¤Füœ“ºQҪ֒ êBvQTŸ²ç‰)–ŽÛ
%çI¯«ù
õç°|Õ ïiۅê~b¼ZQ&ûÕN²ùC²(^Äv¥Äëk
§.øŒÇ‚ü
–Ð|Q= ˆá& !I$±bö½Âí”9îmÔ IyÖg×!ļi^(¬³óŠx}5'&Ž2ÎgL~à"Ùϐã›^ð“ãD~šaíBÄø ãSŒ þ<@4¥è»…i䉆b#ïèUÑUhèÏ왪L[ÙQ]©â_H•i_¾°Y™îœµ7vnieՌØÈF³Êåš,ŸètK™ýKÉý™BS&ØŘw»0¤Èlûͼl£[= ø5»ƒ]›a8b„ȐUÎ-,‹zDÜ«dÏäsQ
¹.Ýõã¡c…o$'ð§ é‘ÞT-¦æm&ُŒ±äÄ1ÊÉLC|($àÛC؈³W/¸â ÉÇ@âAt‡VŒˆb¸“5Àœ
Ý%‰ËÞËѼžjÀßÛN¿2âaä_oÃ'°^Óå‡Þ’pòÏϒýóJHð¨6ñ â×xà»öX5¯;|J È†º©(Sèþó‹MZìÞ+à#·7¼£W˜»©6zªŽ¯w±ÆAƒHcËæï‚øÝÈ­ÐF[þ‘\…Wñ£Ò¨Éýó›ª<D šéä;$-=MõmN·çeF ³ï²½öaέX´.&ICÃ~üK NߓڏÉXKè‘a2C[ñëÇY³Ì™øöµÀvޞÿRë>SJ“®sºîÜwûöHÞ [8Y/‰£ä2ϾÖr÷2ûFI÷@+óWg#7,åûŸd¨Œ×ÞyµOÊ w놾B-—~ö×/X3Íè|êÊй"Ê.A ¦ ó ö‰DYêÖDJ»5èI0*U»}:ƒ]\€X]*»‘>Béúûücv= ž}#å
â˪27,7]òÛ¼ìñýWÅ8'¼ êe~»7Œ¿‚ªFËru•ÙâWë Ä«=M= ÄÏÓXI–óoN8->Yƒ/~6‚H_ƒ­Ð¥Ä𠄬ª¢c]d)Má“lúr57§k~¼÷aÃÍL}WE;ÄۊG~Mê榷Å'D^ñ<™zJµŸ,ÂÅ8ùÈ©^ÎËõihê?´àž˜’¿åh÷œ÷D3ˆÔ2ëh™ð"èç°í½0•°§Ä ëPHà:Yj“u™ûŸ])²® ØXÞÉçœ]ܗ‡Œiù±Zp}ZLƒ‡¾r
Iˆñ>ˆßv¼>O¡W}kb—ꓞîô¡AIزuï֎ãíå¢Úpv¿!_ÿA¬¡!ò’Ë51@B¯~¨QÂQ¦G<‘ÜclHôµ†Ó˜Ê=}©”T¬3Œ*ÂDà˜"ÃsI&=Mª p"EéjÊ;їÂ}¾–¯˜,. Þ¦òòޏC!^ kŠLØJ.Ð%~Á:ʹš:xJ'wµ+Šz³TÊ% Â+6zjx¯4ÂgU¯¬ç¨ÀZdútÚ=M“¯^U¦r–NPÔæ„O£EÖ÷i§Â*ÄZþ^í¿ޞ£‚|þÁ|:y&{6g(<«K-•Y®ûðz©m-zQ€D¢ÄÃŖl“s(Ù$ÅK-ë3Îe´Ô‚j - ¼µËüÍTITÐ)¡âZW‘äV­ëã>Ûß#ÜÆo•3<= 2ӂ£¬è ZtFtLˆüªó#Å Ct-é>Œ¯„¤Ë$¿ªBe—x„ ¦ £õÌ!Ég©åïÍB~Å4¬}­‘ÊÕåÄÑc Ÿ©þ{¢ÆˆTêU!ҕ,Ph}ä~ôçˆéU©h½Ð=}š_0©ƒïŒþFn£k¨ÓýV‡Féýïc=Máv-÷Vn= v€¹Å†g͓¸^*5Ý$¯rÜ ÞRÄK&ÊfíåÃKÈÄA¦ò¯9iië:ٚ0´.šV f$= ™¥í. òù™ØË„ŸMIƄ«¦Ë•†‡ý0ËõW6õ“ 8 I¹]&kóNg–—ì3Û¶M²«hžc.‹ùN‚= TF%É1–=}Ô;ˆñ_•Öº6@Z² î뤋žpò—6]©rcˆºa¼o2ç¶á¡Y甎dDˆÌ±H×6íãí/f¹°֝#͹G=}ˇ¬ Q°Å‚w“õ ‡ùYY$waìÿ¿³ —ħß³º¸xù4ÐøÒ¯)Ãé*-± –øÐVö–F‚éœñ#œðj^´¬N Ã{þ0ø´-vHDšå)LN¡ñ2¿ÇIܪZ{‚Ñ ë:B6ßè>Ø=}ðaÃcJ-Á_ëâÀïþ‹Çðï ð^¿åN]ÆX¦-Ô¼^&†¢j~óFÅÜSÕß¼l bR»BØöÂÍ^‘—<p$k(kCÚç§=d{()ÌJýv\ e*X€É’Ž!¢)X’Â[¿ëö Êü¢JãÛW=}±«öuï^TÔâ«9¹O$ß֗Y€™‚°€LtZؖ= qåN›2UÔ}îÓÆ¯¼‡É‚à mÉM1g%²½£hÅq]„÷•h¨oÊ$¼e>B¥]e^Ñ'QÿL= ó‡¤J¨yɏ õ<ÛJDá‚hGƒ³d[bñz-wWªxýÙcö…cÎPÆx|«A’XÚÎɊRLæ›3‘cÁ+ôšäŠ\0Wyâ?î@·a‡º3z¦ÅÁp³ “‹=}"  Ë= L0¨ÝK¶Úõ“pi¬Y{¸#ø³[¯zãBw¿‰øØ"å³ò (Œmpß³=MI-Ûp´Œ 69¬°Âe<JíªíXy)›kô—0¦8»ýmIó%SœÑ'‘Ov³]ó§S—|M òsz
ËÁ..ˆœ#ç¬A<ð<†‹£ãOàŽ'Ü\ò§"…ùxÓh*ÅPC~
Òá‹(a?-ÍJtUÿU?…=}ó
lÖöa­ß.Êl.f#þxñü÷D²WdÀTæ7ÔÁ'r¬Yï6£ëö“šƒ&©ðÎ'ìÎð9(G©òä:·kãúñêU‡às±=}àۄ€¢Ud¸ !;PN4RÝõ EK妋í¥ë iuõ½ïÕ©?sxž&Ì˔jï=MÍè£ÏœzÞ|\Ùâü%Ó§¿Ííƒà±Ço³×Op{v„µ²Ä=}ŠÅ/Q£Ig&[UèY}ˆoòÄ©.¥––>½öÔ½Ãd‚RN:Î]åj­W'ÃYÒàßEqJòuJtB‘éd‚IvRñyÂÖe?íb{æl‘WÆû°uÙ3-ÇQîfSÿ‡K²sÉÃ$mm!¼MTª(‹·Û-Cß?-\HXÚ=M \é¤JÜô¦5ta,}Z±Í¨JÃÑX¢¨šÀÌÝÈFk@ª) ĜK®YvĂ}™<h%&³ÀCn¶ØEß#êöp»žn¬ÉäNŒÓ²WA>t@f½l•oso{'òɌ™Î·ŠøðŸÂ§Å=}ÊûN…J%ú/ˆ%:pû¹=}'µ»K€'@'! åj1l?3XF–<·Úå¯nûܹjAÎFë »è®ò;1û Ç÷k{óvîžæu{°q±ûŸL“ WùXöñK™nìM¼0ügUi©X“²{vÒüËuÜ| XkېŸÆm’ø÷af?÷°ªBé@G#fˆçÛêHßQ¨õÒ±Ëà¤à0§H¿ÊÑÌç* WyTf|éw›¼½2=}®
‘ÅÃÔn­VãoM+¨±fZò|ws´+ çÄ6Kd3‹JÊl¯Éf¯}Ä^̸¸8'Ãx[V‘ ¦õÉ&+Ó\ÚÜ]©ÃIÄ^©u]Œ3œv'?—êIÁpSy„mÒÙQ#Â7s濐•¿ÕÞý€èIt£1ªÔ=}Xàv$Õn¬Æþç-µ,Qñ*À·7w ʾä…û¤z°á×K¬¿ÐtÝpŒW‘ÌđÌÂT óBGJ¦XÄÐÃk܀„•^étpï3·Mêo¤"Æ»(­ÄŠ~jr¦æàè¨Gñ‘Nß(1.©iõË=Mƒ©”ÁuµSKù¹ãÝР…~Œä¹s;©êѤ”‡rw¶O«m¾ù–钘ܡ­Ú¢”ÒÌ'"°3³ÿM{v*W¸]¿ÿôèxBÖÛO ï= ;n¹Îf;7:•ÝÇ1½cÞ¾¼W+Ã3ƒ¸)ÿRHñ¢Þ‘Ùô¢¨ç‡t[¥Æa£hìl, ~˝äÔ
7ª=Mû’-ÅàêÒxýað!”gGÃÿ³Ë_½)“ýŒÚÅÜ %#ay!WÔxÀÀÝåTíϤŸ¹®ø;(gU¥´(؉MȲއ†‘:•gªýü(Ï0BÝc×éË@¹¦jœ‚Îoyì¯'½‹a>õiKÂÓ÷Åc$Ããöˍ›ûÏy_UÏwø—‹{ÿm«9’? cÞT#‹Mê8=}Pôì_’#µýì7¼c%?gÎ}Õ›üé։ÿmNЋiF(ã q…çoxh>—Nä«Ï¤&ÈkûÈ´푅´¿ª€Õn?èÀ²ÆšÿÔxoÚi÷ÃÚ
ˆ]Ø;$3DƼ„â‡W2ڇÝ= ÃBdŠ“Q}ø’……ޱW¢Ü ZUNשð´žrr+Ëϲº$)¨†˜à>ê=M&Èt¬—Ë,º/ê|ÈÇ*½“U®î*¤ïŒßÚ=Má!$92™ÕávJ›ä
Ž†°ò5gqa:#D8¹ÈX™qøë±Ã:°:ôŒ>+˜K¨U°*‡ YnçÑ:32ùuÀ"öë%Kæ˜P”¯]vÎ%¥§Ñ×@“üþ±Y×Òd !ÀÚ_“¨éH8ÄgÖí˜Ù«´‚äñŸ«TÀW4*…½
4Gœ?†ÊRÛÁ[ý©L@§÷MyjÐΊôzG{¯ZŽ„ÏB¼[UGjmGÑÛ†cK„ÔÜýH¢>Qì"^Ý,XÇȖ~ÿ[ ÷â‘ÑÔ«Gï—/ò[3†Ëm<¡IS?º ‡+R|+¾폽ú¯e3Æ¡½úC½zÇê@ºÌ€M7ôx÷cC9µAç_3ÎRÉ]ÃÊÃ}üò¦?ºˆD9“^zΙqü:s2k½¸|œ9½†¨´x!{hԕ_‡iû7ƒ-wýsÅõ»zz¼S R÷-²ma½üDy~½E
Y:q‰gWýURDÏjr¥ú>ÏüP²¨!ê=Mº_¦æŒk„ÏצÏçØmx ,17Jds!¥žá¨mòmƒý}ƒÉÍêŠÙzZ‚ïƒTòŸ%‘¼ œó—õ‡Ó”µÁQ†9Ôõ= §ò‚UB~Üáìsæi_.zWb$fc+îÏ$vD3ôì i3€u z?ªD/9k¹.r¬ó)£ê‘ðY8üš¸£"BÆÁ‹£˜Ô¹Ëd¬jhôc¬¡GÐ×5u³Ð5è\é4-››ž–ÊsàlT2”ºá¥øÉÜ6n6°˜Þbpw !×^ßdžeÛ,cg rääÃq’È·1¤Å^™o8“®â¦dæWÂßTÎaF1sºíD×ø’Ÿ¯
’†Z"Ü/èœïúÝ aè}Ååãòl‚êÉAîÄ<ó˜ª}V=M“bÏ>Oɋl¥+ì'U¢ø¨Ѻ?ùZ¦ï³»ºÛI©³+Èuó҅›ÿ1,ܖ머Ç
Y=}Dµ”¤ç:¥áTdL÷ϠG4¨‘Q¾îm1”ü-‹ôîûYV=}æñoF§É·mO„¯¯tÞx= ¥(…ô‰Ù‡¸ ðXå©Ü5‡XF!mã^½……³’'5ÁïF×¼å×eÂc­kyß<QÞ,hòo·âªãÁêº4 :¬kçÑÖñ•™ã‚NP»QÑ Xy°( ¯°•ºmö8tb[°‚–…rH]˜=}d›^ؘKäà=M §¾”Ûäˆ~ŽRîûYpã¥A–©ð©qpµî 3 (bèÜÿÜùˆêfÝÌ®F°I0H—ê²'/eéàÞ,Q†“:À)#Èi=}$\Ê\ÿù]̛<ÒþMÆ¢$¤ü˜o1X¿÷9ÇHLlÑkÑÌØª6uG>²Ï–äT‚ÝñvŒÔÀwRa]÷pxZì¹ð>ëꈤ÷ü= ÌJvÚ¬Ø AŽÄáŸÝfçë]„þúœ·ÚÇVêym3pÊÚÀDzÇ=}U"¾îÇÐhŠP8ˉ¹ž r‰8_Œû‡L.Տ”ìÑ^ªŒÐ% ¹˜òŠì¥¿4‚L[ùˆ“ɍcÛí8Ä@Ðhå1Hå™ãYf¡hê•6&‘ÈÔ¹~åAÇÿ‘ì™ï–­½,iu¸„+{
Û0Çóæ8ÕãѨz3Žz?Nýhg1ç^7ȗ·= ž\ؒw‘a—V5QÅ¥Fö›ÒÂÇ©’•Q'³%cogžßgÓºÂò8_uÚ…ó.ǹS¥©†Š*¬h´ƒþ¯™¬€‘²v2z-Æw &[”ÿbIû^>;?ùΫ«ˆnTj‡ÅJÑQëóÈãÖ_öÔHeÞ®Ëtǂ¤S‘—žpnáøó¿ÌuµØ~‘YPLˆ½y4æÑ;!:«¸-ô"=}ƒÒˆ±˜§»X¨™‘Ýñ¡Ì§>ŸUâ>•Y㎜¾"çaFfe´·‹6e.Ã$NɌÔÃ_ü÷ÛßË:ücóŸ'¬ð[øé.‚àú‘/hè#Nyq*¢½ûÀá̓˜ž¦Bq!9š…;ögMgùÊ«®„ºÊ$£ûÌ̤ŸK’[Ïnv(ó„$±ã‚âÉÏ-
Œú{ÂÅ,<ÊT8ö¤Œê}æõq¤!aü:ʨ†Ôû\¹I}ëT5Æ\Ïiÿ0QEïµ*€ñK’Œh7b¾gæ'ÐÂdƒ–[§ˆhsYàm£¹KP®pýîöñ輸[u/Ú¸= Y6 ÀYu÷²Ö™ŸÖ]r[7„bmj¨'æjòà†’lMO* nj·¤ñ1(*TىZûÇIîiÁ„þ’.ÖûS6Õb6yYìÒ$©iêã‡â{Š9DÖ[™0t *4šÿÐzº0ð*µÝJ7НRbšå‘§ ‚G'Ká
è2xêy­.êä‘:kñnËÞ8ÖúuŠqhÛâ|èä(Æ5õ}v÷ƒêÍFŒ=MîX鳓mî´ü|pî´ÌõÆÅµÔ÷)jžDo…L¾6üc€Ö•tÞÔ,Æ3 eð_—‹Äšϊ.‰3ˆ,VöHDNå@ÔI؅hÈ1¾¸Šô"†§wt]CˆŽàñìßï÷\kò†§Ëkž¡—áŽÛØ<vÐJÞQi[ƒǢNª¹˜á¥¹$­t¹Ða‰&Á_r5×hzM8%
‘Îf93é.¸E¿ÒZ_=}̯ܛµ±œüˆù×fó ùž2½NQXE‰‰œÿZ ìt«î9’^z?êfô0Hf4†ê‰†êfá¤>P^^'­Öâh䪽œ
à§÷Ê‘Z4t¯.®'c™Î÷þ!%n/ =}¤‚=}}Ú²SpŠêw½ËÖPçT¤–õßª—›U‹,2õVuPT8ÌÍ!YVÕ,,]^éyào7=M™RÇt]= ….&ùdDÒh­P\Œã¨›-FRÆx3´¿¥ûOªÄ:{·ª7A=}ÛEx¥=}VîAOúõL*þ¥ÑÚ¬,t .²ÑO·(—“ùqò×Q1 Iì(é¡U©ª_L ÷¼Ü j–mªÑm_{ŒÞQQõÔ}óköJ%¡ÚL£²ÆÓ uŠSU#zÓ*§ºl8BÐ"@b3ôÚzbÛvÈdӟÌg&á­ê†ühSÍw3È[©tÄn*\2¢ƒ¦à
€v6ßyŠÜ0Íw)à×莚S{Ñ.Ò¦Íæ ÙbµîÑ<e„á_›ñB.½®u÷\'&nRTx)= Í=}-ˆûô¥Í.>€éô5) 'ý¨êëšçõª·õžJ+•mvä¯1= †9×µÄúb3®:†OaÌw†’;±QãJt= .÷g)–^=}%9Òwq9D>èÅí‚:9º_À$˜ÒÁ{cŸžÅ?°Nzí’ø~ÊY8Ä)#ˆ2b°µ#¢ix3ñȆiæ9“z¨ÿJæ“äë%<êR MÑÄQ†Ö2Ô–½aôcŠ5}t5ìø#Vñb Iò‚†j&=MÊwd!
–é7™[ÀÚU*l6¸ïÙ©¡NIn%ä Ì%Gù‘Ñ6¨ uo•·6Å)=}PíÎîT=M쪇¤){èÐSX&P2¿Ü ÍvHÎâÁ3ÇÌð¹õ•-„êÂâ©s¥‚Ÿ¾¨ò-õ£seø«OۍˆÝ·ÔnW@e¤l4¯ ÏÞ¾Qj’AޕiòÈë,òPGPñ¾ŒwfQdSu^ïÿ+‡l©„ª£¡Eõ£){æ ìððõ⡌ÿ¬ Ôé
£“’p™œ¾lø…w³”= ¡Km=}û5HàI§Ò–%S5x÷•i$¡;ÈwßôRxfaðZj&—h6æ‘é+4²èþ"ñ­Ð
ú¤ UÚª–ô‰Æœß ‹Ø°†;¯Ê¹¸ùʧÈÖßoÑZ!ޒôÙÙʙ˜Ã¾#·/ÙÀ#P±/·/}¯1š®¸zÉöóýaa’â%
â]
÷^ïCÙã¥þ]÷FvȊ­ÅHÒJŽ÷†Ùñs¦ÖGúìtp»²Xæßñ 3ÞFЉ}ÃéD½~ÚkíŽ]æd®ã±êµ×èÑèq1%¹ªÓåˆÌǐ-ù˜µ“Fø•I´ðìeò3ëí
#1ñ)ì”)žÞ÷ÔÊ0­ÐÊ­ÈÊЭØÊÝ÷D"¢×ûƒ>öÑ X›>>>éÍ5u²m¦œÌdlÏg=MFSôS…QÄ@ט¬Gó^Ş£y©ZIdÉܟÌý—wË}E‡¬G0Tò0RñSòÜsÁ¡¬ßâ= É÷Ù¨+­•ÒŸ_)ƒ•Mv«<§•(ÁQbùÀÇ=}Þk¨jlŠ¿ÐŸ±ðØ÷ÙjÕ²"éñžpÑ¤Š“ûر¸Ø1\’†âÙ= û ÷áùTn¯8 1˜¢:«çùdõ¦ÖÝ'dd1&"†w2u„‚ù“†$Ì$Ì%üžÕÉïÁᐝ~’E£ÄâŤÆÂ€Ú¤JÙ©¡¤î= Wxòý%=}QÚÄ­šöªÖþošpgÇ Xr*—ôMR…#ìmÕ§Íœ‡˜õp-¥ÆÑaòÿL¼šÿÑ­͖±P‘3;‘Â^šW])³Ï ã™þm¨8e»êšÈÓ=MJ­„šUðPmr ¹-=}® Äy H LúRX(,Ð:‚Ž= Õ^',ÖnÂj—ØÜiPW2(ŒÒ7©<:™4Ê:2N¸ÍÊ8²Ï.ýI4·I†"k¦$ô‘q{š øâ¹f „ñež1hªQ¼[_é2Nƒ™È´jQˆ°*YÉîÍϾÿÑ
Ív@ØiúŒ!zڛ.>ÔZ³b]@ëúž“²çÝ+‡ ÑFPž†¹»áEZ¥›4œÈJ- Û£-ºzUØÛq3= Å´Ôw© P±-ÊQÊfYcÊ×Á_~%*©{غ)àŽÀ҇÷„ø'Öd¯Ý²Yíì»Ço!LÍ9ËË$+À5)„OŸ½È¯¡Éz؍ŽÊqAaûԊ“4IæÊxâ&[CÊ.¥iÂw×9iWœŽ°ÀC¸(^'‚©‚ÇÊTºL±uvý‘ÀQÉënðÈ¿œ·ö!CvH²’ÇxÎÚÉ 5ÒŸÃyôÿúÍŎ-
†>ÉÿHìæ U4Ό¯þ§½ûmÁȔÁÈ&mG”x¹¨‘¾Tc²Ø
ªŠ|ð(JK¡+BN©ÅÝácÕ®ŽO©Å݈Œöd}´sãЩ~Ýî#{µ/ÅÚM8özÂ)$WÜbªÀß÷Ùϲ‰$Ì4Tú2Ö©Ÿqƒ¥VµCÙ%<PÓHùQ^h ?•ܳó‡Þ38_Ó:Mu´[‚@ ®œ2 –È,q~*:C”Œâ©µŠÏi¡Ø…b´_õRàag÷†֣á=MI…Ã(\|MdX^¥–dÞH¿¯¨®ÎEWöroODlل£¥²‘r±í”òq%@ˌ}\âÇi"PÖ ›àÂôÆß®YZ¹£Î ÞLð'7}=}:‹¢ÿÊ$¬­¥’ßÐ:£v|Ÿ M,¯ÚE¹“y5§ôŸLšóx*Ó¶¢’—…•ð¢©½é®#;8@ðn5;˜…UŒ‡I¾…w$"-ÎÁuµíƒ±ÜÚ.Jû2¦Ó™Bì+²¢˜!Qȹi¤†Öù‹•¿ìà ÿ¹·QlBa!¤kh)ý‹¹Rª ¿}I2gԇ×BԯϬª/ @õ¦*;iÂØëފpݗ= ˜ÝÃd$= ?¦OržQИŸ½Ì"x¤ÙR ²fòW*s8|¼¸2‹†Ô ‹õ †ÂcÔQ´µªôÚ<…ã6Ñ?{Âj®Rj(ÏKáW뀳½Óې[¢"ka喠k¦Ðwï…­±bëýF¦ÞàYvôr€H¨Z>èç/‰Å”¿ËTtºã#:}@÷Žæ(2Ö}væ}” ÑØI‰xÕ¨N”ÏxúÿIMcŽÒå—wšt‡J»Ç¢Ð>Q{‡Ôòh.ph.ù¡]ƼÔ¹&•¯áMÐy]­LÌÆ¥µÿ³·á*ùPŽwãSÛ|">4·[U8
I‘®ð,õžï4\\A ÿb‰5+]Q—RL&5ãh£\”ÆE!ƒs1Ïı+2pӆôύ9֌R˜¡m)5!^PqÏRf%\€hR'„ñ›«òSßî˓††ÍH3íÿÌ'RݯþÙJñŠÝ©–\\PVQÞ{e‡wjÍî=}ƒMå uçbÕõ·Ð?˜Âߙmìš\¡2΄‚d©E•G¥G= Ê}-ƒÙw¸R ¢zy(Ï¡Xê¿q½ö±»ýø6›v›ÞÄ7«¨¹Ü Ø=M†Õh •{çÓ]ñ1)%Ðj£TNˆqƒõœ~r¥à×õð耥®x¹BûÛèbÍ¿–‚õ-Ò¹Òæ\¤_BAý‡Q±¨4²”|×>9ŸWP®&ÊëMèØ&{ÑîGb¢è¦=ME…èÀeî¯a=Máә€!jvñ‘#ÝÔ¦–D¨¯|;ÈRRj"éFÅ{1ÿÏr< ŽÑÄwöžå¾zÈù’Çy© ­13'&7x.uzØ"
£JßóÀ@ƒ ù~TMS°@Ù\ê= ‚óe%Ñâ§mþ÷fRôAäêtjõ3I–á‹rˆ4&¤×ùÍÜ´¼Ý|Õ¥ñ³˜çBäœ!vØéX˜x˜Õ²§DˑR<˜€=}~aXuä·%Ì Ðñ—+µ‰Á–º
%*g(X’ⱓ3÷ñX0ËOÊGË>Å=Md[[hg®1·D!v&/íjoô®bü= T?iBÔØ= ªX+óq6Eôyˆ‹ã a·–ÂXI¡—ª“bp¬š{•q¦-¤É­õ¢ÀÕgÚCD®±±¶ÑNŽ™?fÇ¢Ò —žlÙ­˜ûnHê¸!zþŠT”Š”hl¿VÖY5*Tº=M-füfè/”4;)
«€é‚áh»¯Ü’dFÎrhÈûåžJSùáÎW¾’^7<gøò¬
ÌT{Ž(êw€q¥â~-ß§“O yú^’yåæ;íšÎˆûPyÉ?÷øÆñʖµâþu¸|pa×ÅÐøj&
>‰c*Ò¯1ˆ½xÈ«µwSîÃðIðO3XˀKÉ(tì66þ<çÊñ2J /bÝ}täëåq¦¸â$à’±™­G…SC°Þý§îá.èKëí
‚a„/–Z¸ðWöI¦(|R)dzÖL²,¦Þë™J¡±âM ô{ֈ㈨Œñµ«¯Î'~7,[‡‘¸ˆÌ,:Ø'zÊ9WÊlœLA—Ä›ÊK/x¼<Æí·1äÕ:½Ö¦ }ȗ¦qÑûŸÚá~†p.
ž
-Ù0ùýÈ= H3Âm°nOzªxŸB 4$o÷·V\…nÐà‘+“wÅV¬–® âhþ šä t5Nþt*6FΧû¿¿}¾Ë\¥™^igi×/._'*öÃq=}Ì=M½£á9 ?LÕÓ&­s”Ä& Ø>:Áê7žWP7'z4üºÓ‹.¦0àr"˗Nͨ °Yñ!Uñ€¤¤Óƒ&¤0 ;ˆäL= ÝD¨ŒKÃ÷Ò¿ëYžF‡Šb#2¹ôñ¸ÑÑxšì¾[/Ë·Ø¡ßyƒ9? 7s*‘Ž$‚OaìÃ=}ºñËÃàφ¿ZCəà×å…}ÆÏ³ŽÃyÈ"¦ø7ã L1 äÛqhžlfó=MȨÐZ^sfú싹ž‘9ÈLsrÂÝå|›.Á%Jðó•×zƂZŠ™C÷òK‰IÉÕkd
Ü„¹ Ú·/v’Í6²å®v±i·4M0ñ•d*-ωÿFËÞ,X,K†îºi´å÷¹) Z6±ÊlãÙ, -G¡Y$ÒŠ+†•þÒ"‡ Y8 /©txÙ $”îæ±
ݱZîÊ'bG0}av1„¶µy©"²'ä0ô-^1‡Äl“yž"â(-7p轕þ<Õk5}Jބ¦ýé":­R톲g:»…®ðuv²Io¥ÇUÝ{èÐ!Õ"fl.šW²½'š>4ŠN¨cóxí *ÃÅöèžì¼„qi‰ï hò7Ÿ·UÚ£«Ü…™ ¬g$ÝèŒõÞ¨îÜ1cÌ(ßçÉå^cãÅ- %Q>ø$Î k†Kœÿß·µ‚Øæ¬:è¼Ãƒyz…µ.R­wÃkÈ Ú,Ñ;Õ¸»^O¥' 
ræï½|Ð:¯7Ú=}|ù¦õǃk'ð¯÷0ŀ·ÝŠêwµ˜²xï.WMyH¡Æ=M
څÏò©ì/3_˜šÖ=M ²ü|a÷²=MÄÙWe©ÏäN™¹?ï×Ñ^W*s€zý~†N‚櫤Ôèímܸ«óŸä³›Ä´q9òKKZ³Û]ÂMÞеuڝÓOôxIäõ„x¼H&D'#È8Æö¥ÃrɄi ª°ÅÎYGy ñÃÐN†X۳ٗªO2¨X„¦Y=MÓ"¦&Æ' Zô2’¸IÓÃw6-®›ÐŽ€¯SïÉ÷ŠÀ»-Q9ÅÛ¡TV\å>CÇØ¡òo[+lžÞéQnU¨k®ïn ³_Λ>ø 9ß-§¼?A(QD]>ýßOɪÝ:ï´hy'„ÍRÔežL ‚Ðĺ”.jÚ-j̺¤4Қ(põ§°7­>‡ˆ­·G|wÙP¦êyÉ[YÚ/›Þ5?½³wòžZ= V}Äú;5l>û(ôûíy ¬Ú}bT2W lI7&Ò´j~®ÇvbñÁskóJ ‰@æޜÚÀ"ÿ±
՚›™˜±6kVñòšòpù¸ÅªTyò½ÄE&ƒÞë^“·U—­I¼HÍeÈÝ'œˆ©L½8"ýÓTƓ#…Pº‰€n…C6ÉÏàWíPšÜ}9ÅŕhoÃ'ÃR9«“žx˜®]î*§__G¦TŽtòÛ×ΕJ}Þ(QjÈü³å /œìúJ®ó·ÿy'N"ÂÚVô/%7IEô@äµ,»jlg’~=}UQÃÌÉÁ dh‘È\SPbTzÒUUÔq‰øÖ±ÈÌGÕY.N ¾h=}ÔãiÆw{ÈèLºÒ;·$—­k¥ç<”p‹®×
*¥2ÕÁbÆeý‘£³å–¢ØRŔRwîé'øa&a-þ*Hlba”iç%¦/EÐX33"ݳêé|hÈp-ÆVÊ7= fþ‹j<ÚgG°¹ Êp8ÆAºšù ؼó–U7€S7hH9]G9ÑAºLAº†¾ú êB3HBºSrW7s›¬ÞüšnÏ}é얷ïQA:Q,ÑAzPfô3cvü–€žø ¥Zð>ßüfÇhÅ7 1¯ÃOEã®»gat|¸ @c¼âp³Qû¤ºßA*Næù i¸O²œ
¬ÒQP{{ˆ×ºÅ¤ê3TðÒAó@U§”â¤éóä+ƒØÞ?Ò×â0TØq©ùƘÁ^úÜùëw©g-ÜÀ\¦ùý¸K¾4Nï×ÿ.>Oª²\Åm>Â,Ò"·žÇK™U3ù² $4Öùà ÐäO:»Šȕ<ԑøƒæU=}ĨJŒ¥:€¶¨éŸÚêòϊ§”àÓÆx¸˜íˆ0ËÜܱèüx"R¹÷l±Im°¾œ•3w€AŒ­ðœÄ(îB·2‚†)7d×ÖÄê71÷·[çW.©ShêÞü6®”àù°ŸíNVRŠ‹-0S5"aÄP*{ 3[9 vcXî= ¶l›J±øÕ¥4CqèÈ´KõŒæk >âæiÚæo'ÇóӍÆö 3!7ìŒË¢Ö/õŽïæâC²,ZÝ')í)ŸŸ¹Þ!iöFÆð¹>ŽïþTTwjd–æ“äsºRøŒœ ÝI9”EyFEÜߋ¼X?«Žd Nö&gʙ6„/œ ÝÁA:ÂR§5Œm1¨|ÊјO ë»±´=}3y½>i )JFߥöÐ!í(¸×VcØS,£àe]ÅÛÓi¿cZäÀcHò«¹µc´…ª“y¾/÷Ò18Õ÷¯ìHyñ‘²Öð•ëä"ÙÈdBt 8±YªJ2Ûx6×JÈréîe)^#!¢ê;-WY„û9‹ÚS™ vM¹“Ù^4<Y„ÌS‘¢ 6›wšÙh}ÿn’!c ô‘N*5Œl~A¿â’¨^2¾-2~ÂcçØù-£ò)¸Ë…ƁªºtM~á*#xbÄõËD{ºöüuòªž19NLO)‚LªöçªûYP‡ô8z:µ4ǁꎺºï¥L}抌J×U¹†UJO( uóyx!PÍ<"= ô j–·ÆèÆé¤0É©òT©‚Ÿ¥ùÿ¡Th-íÓ½•3Eó19O[©ÕWš«IoTø~ÊCN;Ò <X 
8NIY$'ÕJ¢¡Ô·¶’¨î2.b8¯²X^ډØJØïPþ6ø,j‡})—S¬ ƒD­CÓr¿ÈŠXYìt‹žï†¯³§[r'÷tªìbWŒÊà|hÍj6;æ•#1Ä2à
Ϩ]ºÄturUÀ<xdS§iÖÑÿW´ÆHð×'Åæßpº<˜}„sæjñ× jþŒ0LÔ ì¢[$;3‚_KT%‹'U침®ªðv!Ÿß*þÒY§¯¾ïÆ·¼²·‚ä‚= <Ÿ2üF Ú¸
´ñÝÀ—úÇɞTãN>ås&j•uZ ´­¼‘3‡€Rƒø“Á&~ë\rÑPÊç'iIc €šY{ªþ]»âæãZÐÂ+~ðZpj ZþÃ×ûÁ›7îßpräÆçýÀûqEâ¾,éV¬t—û³¦.ø
¼æqÆQÆ—ö‘¸Ó¢°Ü‹†bKmjgøÃÖ×®²T,fIZa
]Õë´U»°F°Ý³ÑTw<hҎühÇ üE’¯ÐM˜ë> †É³Tä©lW4QJÂs©r'©>™ <e Ò¾Ys¨£¹@¾fȞƒýíZŸ1¡DØê£Ù~Ò>ÞÅòñ›2aÆâ2@gÕù‰ñdªÉƒ’#¿.ÙN ëÎr:·Û-tŒÉtÜ®½¯”qµŒƒ=Mû|…èÏï±Ù}<L —%†?g xÐÂf‚ÈM¦Nî~ïîð 
Ó^iÙÈÀ­œK÷R˜gЉÛÅ%šÈÙ¨Þ¢×Ëp¡^gZ·tޏã|­z£*Ø*2£Æ1ø+-[™£çR°bb§ž8«šsê,f­µ\±aœߗ=}Øèݜ™(ÜÈkӅà Ô>¦½Ø©”³ZÒÐ2ˆu‚˜AjXø1øLDâU“¼‘,gFV¿—㈪–ƒèíB-ÞsXw­Nc³”™RI+ Xjr<ä-ëèn‡ÑÚå©|ˆÃi!K1yH{ob iµ)VÏR¾ çh¢Ë÷ޞ¨vg §±™<EøÅ–¥Þ¿/½=M³,vá ©ù†4ûÅoÑÒѪ~&3]i霪@„óÃÓKíÈí厲.e,ÈþèJµôز'|bò6p¾øeKß(ð¥ÖnÄÝhóîú‚¨& ù•Ùî„xE8ó‘¢vÛÈñøMl¥Û|Z¶Œ@Õp‚žÿËϐåˆÑS€ÇP²¶­Ï³hõº˜Ü!n¨kÞ݆m7áü²ãüFrðýâ.s&ò_åì$=}U@•ãœ$üD%ZJfo=}ó“´‘(c׊}^a†¥Æ·ʟâ'Bú5
ª}"mù¼Ñü?W®’ÑCh‹Í©"݀Àù¦ÇEgIË´Äю®ðRÕfËøA+ö© ø‡P‹\GTuà-yAQä?=MÇì²Ð®$T~nšÐ¢òG¤õph0¤Qs¹ùœ­”1®öD ý-–ÝZÇ|陝g®–<pp ÌE„ÑÖÛ|AŒ
>Ö>·Aäނ¡ï DïìoàÈ= ·N¨¯BÆïìo‚Vìá@õvÖMAã@&æÉÀABL'?ʙ¾š)à¢Ñ
+™oëR¡ÌÑFŠ«nv5°ÏÙ%åiùEqÛëXã÷†míQ N1Xä!Hc
"°tt­bïØ~Å>)qcÔÒ­)Òqv}ßÈýÈQõç¡.’”¼º|џÿï¦@Ðo®uØ×½|:}ÐŽpܒ±·ÿ֞·Fy£(†…Uî¶K;¦3>€Y@Á•è›SYÀtۓQèyM‰©>ÕÁ֞%ù¨÷¼J^ ¯ôtBíü³m¡V! ûÚ/QŠA ¿¼&Ñ/R¿qWœ.Û¬¼ñËÊhTÁºå{†Ãâha]NÈÆPÙ9;bKŒwF[„€ñóƒ+²æe²ÊWe…Ì^2¡ã>1‰vÛÅé¶x Ø÷5ƒ¸©ð„i=MiMH!UQ5çŸR/ EÅÖm³Úöš¬ûƒF¤ÑZjå[ÌrP=M™x Ȯã—ÄO/ÕÈd ¢Ø4Wì=}‰·'~T "Ô°'ûóu‘3 •ý îçž&5MÉQ[7s¾¾¥©c"cë­„¼'lÕà{î¦ÿVÙè“OÈ
YAM%£r¼Oªœ6TEú ?!©›ò\Ký©1d¦ÀÙ;Pª’>kdÇjmQ=MßRD>_®ðŠ/by©§–†9ËM¸ „‰pð';hõîêe;R¤¦ÌŒù³ô @—ãÛö‡a'Ã%´#= ¡¾g»ê°+9ߗ#,³è~>1[ð@÷X eñ¸ÑšÊ+>ý»—ÃF;[aÿâ÷™wKVÍÚmÌ
? ô!¯Údiø—<f[2Ìz\Y8›RÛ尉CW§FÆ%â3ìÀY-lòÓEðÙ_²ÞW{¸£ ®(_4!€WXx_0cŠHüÜœ|ÔÑG¦BKÑrwî?•¦'Ô Ä<­Q[YE€ý'ÝWÛæ8T¯ kÓ©€Œz- …Cîr˜y­´OÈч”‡iù$I‰‰,ò,Þ¬Eà¤ÏÁ‰{ Ù׎ œ¼Lá ÎàæìÝÕ¾ã XòF0ã-¤·rê!nu[jZ"ޒ¹ûq[˜cJkÏÑì‡ö14kˆòo«Ûz䩁u¢2Uúu}!º©ñ6øx¯‚—ii'GBä«XÈp7› )D'׿Wm2[)’íë8éª5DcNçZ),‡,ðš+»žŸ­6ºÂìßÇ:Áâ¸Ù3ZÀ+¡³PY\®’ßiŽ?ns–(Dõ¶ÈŠÇÁºëO •Ôg]eLÓåW‡¸1Ï",{4e­æÑ_ÄöoÅ<7?rLè‹Â±Áç&k=}Î%L …Λ¡)Q¦[õ·E§k \Þ,Rø§§ Ä×åJƒ‰† þ ALʭÞð¬Š7Iã|ˆ’X¢žI=}¬#Vúo<µ;ג²®w÷ºU")LíîÈÂNúE‰nMVŠ5P³IH,6~)Øeœÿ0¶¬= 9´MjR\þõ˚ÓMVã¹I%-ªcöÇ.J‚ŠJȃ•Îè=Mä ÃõV$õ1¿H+å<Tz?[«bú{·>µ=MÄHÊAN¦É{cðþlÖÑ-3۔u¶šPö6p1Y9ŸU‚ÊöL„­£¶¿l3ŒÇ²Ãð{¼‘•Ê”'h1yZͯ8ƒú>əJH’¨If~«þy3³ð½Àj•þ= Á«t;FÊ^ R”³²Šc#“ðÓq%ˑ ±Ž¦½—çcÀÕòkwé{é˜ÿM“èŠfQ ŠßßD¥œð¶ÎSË>"ÿmå(†<…çó£âüï·B-]²ãñ€ÜGþ¾R”FÃbg®”Ý.ì§ÎN\y©½ŽöœZshàw0ønÄ¥Xßù”’ F³™Ñ´‰Â¥¸µ!ƒt\=MIÔ±:S
†¯E•*t9|§ä‘—Å›6äÃTï9(×ݫ՛ôŒËWÉ©òó#í7¦â‹HoëÚUfðd0·hWÓc0¨\0Ë%#°´¾½f?[´Öòp‚ŒÕ-¿HuH',œ4\âæ/=M]=}yÌâWiû>žlõÂÆq+IñA= =}5ëïx©ŸÆI $Cùó8Ÿ{’Ñ·o8õJ^¼+–—è"Ï\ëli)î2»B¢Ã tÁ=Mu/g~k ‚ŒY"Þü=}à42YÑ콌ÅÈ9’$sS@7ëHë…¾lQÆ
ë¦舿•ÁÒeÇ£'»ïïZ4ЁzsñNŽùá·ôá‰= e˸¬f&‘à‚ÔD‡IÏÂ!6[µ¨›Öýˆù«j+ad‰š´_R6ÂÏóâÏíð'”záaXƒD<Ç
¨Çä ni?P-oP桬-ÀÝóïM/Д‰-¸½Åã<nyµ WL(‘H«3õ<4•!:9Ù²©Öh+!8N¯ûÅ(zæ9‚B*úŒ¦oL´V^ÿ;öKåSëÁÙRÓE²«W_x7kÒß8 ð!ƪ› A[ Qřôy²Ù°wyf=}4µ'g~L´#ÃÕ÷5ÈÛ#œ™3÷1¬C= J>蓒GLÂ=MîtºôtRÏ?Ùô—yŽê1?´u
î7&­0L÷z†ãE¼.ʹN6)6?sG=M¹?BœE.WÕ³!çÃÓ„
zÎä}"ú3Ö¢/µIê"þøÍ¢DZ (ÕÿIõ҈ٕ TIzL0fõjûNHÛû]=M£WÐÓr@G™úi<ò,¾8b™o µ¸ê ÐN؀9ޝ.eáVHˆ^cxùê<÷p“Ƽ…&ÆmQ6þý¥XÖuE»Ö&´Ù<¿>Aö4K‘Ÿˆ†®%ƒÇäwi‚õ‡“/#oF¦5B02’4¾ÞCYZ–©eYêgŒðÂú‚³GŽ€œ–[¼°þUõIkòŠsø0$­zY6ŠÒÑo<ñoå&“&›w÷Œ*Px4H<ƒÁšë rº?¯€WìW®×î3ˆ¤öÚ6•Ét´WÕ\.ô<¨ïmÑddòiV³1;JU&æžI4A5
9Ùz.ÖÊ¿>9ÂgEb§¦Vžžá>¯ËE»£ÀÉû•ŽÃ%TY%yrßMÁæ÷%n SÑZ "¯Êš×H}Oþãmx„9:M?+ ÃpY¦*þÅ2³…æÏ«à—jð7Ó6T³Ó6Jœ“9Ü óÚþT2nꎌ€²¾·< ÆÛ!÷+æz·¿ä¡=Mô̘pPýØUb–÷_â÷RãmŒ†Î51Êï}·?þ³ógk'X//³Aþ0ËwfÌò›Yó[èu!áôÓ3$dY[¿Í)¯g$‰ÂåÈ7íHE¬ún~ÇÊB'y&LãëÅN¼ãс>¹µÓžÄ¤…50ì$’˜’9œî&!ò8mÌh¹Yº K/wºyÿÃ6(„— &<Œ {¥á¹«?'ç(î£ÝŠf(þ§ÝÒwD¬]sãµD—gÑEŒ¢x¬{C[Zý>>VvÆåLH€×’ÝîųgڒëÉÊX랧‹Ú?¸ƒ)Esuy¥Êu
Ä~žÙå8[ÈÛÏÆÕ>\È´©™@âñXÇGN%@ÝOþW™ÛlÇ ìu½äöÙ:ø–Y瞧pŠÜãr¬®2(‹¬Ú½õâê‚G“×—{ÈcxQ«vºx©Oÿ%ÙÁˆÆõfÈäL>ZUÀJÃЗ$µ=MHÒ¨Ñ(Én7§4:¹²Óñg͵…_Mý'^½Ð}=M\¦„E”³-}ÉYâ£wž¹—ɇâGY\‹½«]‚‘†=M­
çªrŸò¬&ÐÍ੗îórÁ€Aìà֊P€ ¨Ý°ÔìÑîa´¸Ïë¹è{¢°µ¥Þ2x ‹æô²â‚ú-~êì‘íXÊ%ÕÂv
ô™~)hbüXµ¨Õ4~ìÑ_ÜIϦž 닣Œ"¤—$#ç=}ø5( yW1N’a.Ðêq)àüfхüpà½fæ5®y\ = ú/+ƒ
Ï8«”âPŒ
ê\Ž.°û‚•é+<¦³™ºcËJš@Ò0“žˆÊ°zµ$&“EÄÁϘ2(³ ™dö©´kèqTßù*™ÆÊ°ó…ہeÅk»)p;§UyJ¨ëž¦Yò€OTr›E÷%´eTr›ET¯Ѧ0aZˆÞàò¸ãl²ž¨%Îñè‚4<Eˆ¢X‹Äëf¡Lð}ó;Ì}= Þ} ÛËðQZÏLÒ¨!@Ȧîö¤Õ5oöüoöP â·\îÅ¢”<®w}fÿ¼T¨˜T¨ÛÇ­Æà÷öÂÕïh–vPöG’6ì PP-nðþ$ôpŠû_~=MëNXØínîeÛÞµ[mª˜4ùG€Šl»@i5ï9¾lX© ëîN«6G0´÷#J¸å×ï…>RoîPî„cV¡C.°2Olèo9 ¡üGü)g€ëâ[¥=MÝÝÒ£“â†)îr|ŒtSZK†ÀpX"fìà§»¬= HUmÍÔlôa››2Ví9ŒY§¬k±W¡ñÍjéñZ¹¥Ê«ú\ɆL)zfBÞ<_^€“>œ‰Ÿ¬°p¥2= ûì„ÀàÁð„uäe«¼\{&;Ǚ'v•¢»†óÄ_¾¸—ÐAÕ%H<J¡Nß?߉j•Ž= ¢©'a ɰi>• ;uËD¸XCï¦$;³w“DÑRùJ®ýà/Pä%²ðš‡~¾à§k)ijr$R:ŠY®¹xv6ªÖJ&Cրç}È´ÝÖ\×?¿Þt¿ð¨×´€]kôCÍnaØýš¹õúäøt8à„v{ln3û« ÓÇmƒË½×µ¡zü7e’ÏøVÙb†ø@FE§,‡î9 }~T)ɪ¸C•·-¸?zR€f;êÈaŸgăî߯m7JòË3aJˆÒúã¢9ˆ¹I¿‚fWÇçŒ{%¨ÐÌI¡1Y¯Â
½–Ý:÷M²ipšÌ²õùÇqgܖ5«w¯hI.[+¬Å£Ôá–j¯Ï´^8Oµ­øþÒ¨qÜ? X÷o5é ¢H¤èò¨@¹Ú2žPY5¯DèFþRwyŠ(Fg58ÿ'†+ DÂhÏ|†I¨EÛcD¤J'K
ËBðö:ò= ð,¼=M,yí¤l£bí—t–‘ìpE,›óÑ¿3œÄ$Hšr§^+5ÖóP¬0_‹V®F¦=}}ä3yЏñé%HíÓHÊ;陠Ä}›<?zêÀòp1W§0ÆuáãE¬š/E¦ÒZ8òÒêÏÚû„ïöí°¹˜‚!º6äBÔ:I*x5K,5(ØjzÚºP~q
\\=MçEW^‰³æû;)š!*ÊÁÍ¥8(7 š8ævuüÿIסÞEeo OôCúÞ¹wŒp£t-¨YŽÉљ}ˆ¾ÁJ ç¦B¸´¦þ-â5hô ëG
†RgêUjëG¼CÎ0šíǑ‚˜NUjPä-:5ړý,Žoáé-¼ðr]
ñ|â39&9¨oŒe¼“qôä nñ¯‘Ë$reú¼æ×Ó<Nˆé ^Mŀ¨Ð~Nët¥/Shœ6š‘&«}7©‡ÃÔñl­ÙZÞè½Ð¹·ãU§'Ĭ¡ÃŽ>²®Q4!iÀéc¬ø—{ËtÔU3— 7-_ì,?UÎì ;p¿ž;PÖ»•õ'šcî™Ñj1çjÓ
«ÔŠ”%àõo þvìÙ!íçsŽÌ˜í" >ËfÎDL(D\ÏCcð"‹…Wà±{d¾]³FsƒHÄ´wý6= "ÛèÌOÞ'‡›Xñ,½Øä¿±QÆB ×ƒŒ„©;‚ËÏ—ã®ÞY‡ÍŠJóUýí ~t›©V–‡~ÒlÔ{Ù(¡Dk¨!§Rr‰N®X3ªÏE/;¾3bH=}‘D¸ÿUùh›ˆ¾»‚nO@äDø–•·ÎsËÒ ýV×ׁ/n-‡<¢ÝxTcɚižªm¬Nn—äÑì–Äò¿{®OÁ2¦w0cmwˆ§Jaq\c¾ð֓AQÿíÁs°N¦ôg€v2QüûAÆì‹úÑˆÕ8{æ%!¿Œ1¦æ‚±„&ßғp±„¶ÞD3ǪtV5'Ñd•/×ëÝH°5Þ.Ï ¡È®,­ÿ}þîphËãý7Ú7"ù=}ýûö\‹¡‘p;5ftñQŸ¦¯WyÖ&$Á>GÂU—®µº•Âåqöót¹˜7ÃïçZ–ñ67o@Ä:@¼™­N6­eñÀµ,ðË3= ë¾p¾+m,³Ão ý@JæAtªž¼.í}ö­©×íLs= ¨*š€R1ú¬tar!ù %þb^ø€\ Q×ÖámfÏØ¡àdÚº¦Wðœ‹]íÓ Øa¨§¬oãπ³ƒFÂZJõ˜Ö2Akæè[&õmDÞUÝ änç€ò™mÂ0
áà‡®¥ÒDÕ=MÕí/•Š—˜³²ù+)ç+'&RIÙn0õ¨[<:ƒOòëøÍ!І¦ø±¨úÈßOˆ¸~јu>ÿ5lçFùŠD"é¤s²œ‡Ô!ŽÒNU§mRu@â÷pÿÒ™»qRNlÒ÷Qv0¡8ä¤ÿƒP.ƓÌkоògŠi™h””U‘c=MÙ= úƒÓ Š9@ý6ôB²ŽmUn(ÀÕ
ý )C!y‹'¿Wgõ#ïÜÑøÃ§«ã3Ôۃq¥m×3Wɳ÷.}Ax$«Üƒ9hÊ{ç•Õúˆ²­t]¨oo¯
€­Ì ¿Â€§©ú,ê÷,M5)AÃN#Íp kÃm!?ê [„%¿½•¤¨{b>Pª…nH0%V®Öäç˜Ùv×þ0·}ô‰WOÅtƒ»ŽMÌ=MpeˆÕm¯WñÈtübš»½iØ3#sgHÙ,ºÏÇ!Êû[ñG¢s ¹ä‹fI±E]MŸeNš¸¿FÈVy4®È[ˆ.:=MÈ­G¦Ãäœ
9¯ñ‚:)ÿÓ9Šœ5M¹AR{™ë呖´—Y¸43ÓÞÉ©/YŒ¦Uµ!­Ÿ)rъH˜£;s1V8ˆ
˜ÌO*Gã0d÷@è5“¢„öDt';>ÁƒÿÝltÊÛøž ZŒ1æ¡e,+©Á Ž†×Þ ÝâjÒÚ9»DbÓåHS üìƒ ±H51˜Ž q|¶íÂ*ÑÁ^ïªØÛ’b3-ذ=}©}ԟ{?jtÛÁ5ý¶?Ðô™bXÐrhÞ8¯ðW"ˆÚéî¦ÎNûò;ÊB¶Xê/<ÿçÀk¿”ÖTgm÷ùŸÄEDö‡°“gåXë&_/™¡¥4Gö7ðÃMf9㍥G9ˑÏœ=MÀaQ™h±Xì"Öô‹ ï[•ãGŒgnâ_dU}‹Æ“°}€[íá–Àýí4‰Ú ùù@Ë¥˜š= gæý‰(p1ÇGuÆ3ìŸâQFÙôLÉè»áUJÔM‰£'ÇÔKöÖÀQŒ£õ:gxq/c/-kMpí¯ƒ–J‡…%Ö!§ï<‡n§«˜ ¨Ø•RGÿøjõqY!•˜’±¥°*ÔYP9h = ‹¯Þa˜]¡¹ "º¥= Ðh·šž›¯fGÙOéve^E¼çœ¡öãH1¯J‹äèØÈ0¥ Ä[‘"³ïys/֐¤‡nR<Ë"c€g¹UÌ­a£¼¨ç‘‚‘­Æ³fuåÖ[›—÷J‰Æ¢9|‰ƒâ@­Ó…D$Á= Úُ)üËæI sm™Où…ÈkþRòI­aëËlÑ1:Íi{¤w­²E¯Ôó…m¦7kZÚÈšY{øSeõ4'n:Ú¬(:ná:
×*BåÓè<´wCP!—„ØpæÎ¿çÎ˰%4E9{xÁU[ø©qéÒÿ­gÑwæ¾= 4\‘Ž´ÉB-Ú©Ý0ŸÓʏ¼h§HeŽÌ:£Àøˆ¨]Ÿ-B„ÕÏï2w3‚xÓ®º°ùÊêDÊÒæyó‚lÇpVÍ^µ¦ù±¿„Ów!‡6Qòý*ﻒFkª¦;ÿÛÞ¬¹š¦­ÜÖLÒ×óӅFˆ<ÙäfÒããš3|À7ñ êÒAu»ùª$Õ½ð3诃ÁO¬(µ+6ŸÝ^䒀“ÕeœMÚ\ŒåÄôä>¹š0Շïté¾)‡xêl©Èû\&kfÍ5[~pŠŠv„9¬ŠÊîRï:ȑtÅ㜬äiÑ]Šè#Pˆï_ÿéA®Þ²‘#¦ân\‰¿³âŠC´w*œW9ðpð’‘O®Wš#¡F-C(61ÅõfÃ\„¿Y´v¯•»º¥ˆÕdî ~¢;Åvʺ²¿_1ðµmˆjœW\6"h»%:ÈðäZÙ¶‹–j6%XªíŠŠËr7{OÕhUsWvÉÃVã"ƉŸsë³PÒB?Ï~²Ìƒß x£z¿t¬Û¬´ˆ ­"¯}4åàwK·´Õñ°˜•ÿJjýŸ@_ÉÓS·~Y‘Ö‹™&÷+AY8¦ª¹‚O4Ù¾ßRøÛüYl£Q¿þ“Ýwþô9¨¿ÐZ‡Ùª¿2j 6¿ìiîF™ˆ—6{žíT™ jΗ&^-·ÃJqjqy~l‡ÞS!ºdaâ¿bŠé[¼‰'™® NªI²ÀÌJ€•ùN›Ô ?/7-¦t‚Iç´=M†¸^\ðÚc†Ïß%|Ú8µ ¢,G58šJ58M u=M-ýúÁ›O0zªÓˆ»®o%ÎÿaNŠ=}ƒOï6.j*ÄѤžÚ9[q³èr.üS¦¹Äæ1”ã1Q¨š™óúˆ{ŠÆ5^¬®Ÿ”$:}—i–Q 9ØT–Iáâ¤<<¸4@ífŽyë¼EæÓ!×ðk_8*6= TŒ¨DΝ/®¯†!ƒð½-ѝVª¨´[¯U&TyðӔʚÐ=}cÆÒ:ç‹*ü£À¯k»“EÊ>õ Ñp¦éiÝ5ÓÒ¢ ¤@·Ëôe’p Á#qú®×n‹Í÷â Sk@ÑÂ?3pIÑAk¯»×ý ½6÷þ öþ°@2Õþ0±Â&D)î7d±B–M:ÚüE¼˜
)›f=}6i9§(Iñeë¨9)›&=}6P7tëvÁûùÈz±BOâ3®Ÿ²ëh¼¸×zb±Bö”›
-")›aë.7tðv“= ëJ,"9üIC'gºYX'ÛGñ×
ž[¼àFI^֚‚Š\­òlñ äÇÚwƒ§ƒªÈ¬õ•QWF¾ßÓfá%})›»ä©q¢^âè¸F‡‚~aR9;È<Å3,ÉRO[P{ñf{†;ø˜<m<Oö=}¯@ëõf{Š;Vö=}gD›¦’»Ý…D›p{õ; Ðfù4A'M7ìU÷¨¹ì;YP[Pû8ö=}ï?«¥’»‡;b²@#Eÿé;x<WP›„;±@C±Û¥F;ÜV °ݦù˜‚ˆ=M°n °Ï
s¹&®7ÊúfÁ:'ö2"Xö: ö1¢Xòz.)ó8ž*Éz7)ó88*ÉÚµ²9 1䮫¬6ªSJ=M§D;˜Z3XVš^hÄyçäÐ=MÍÑn çpŽ­Þ§¦ ô=Mr˹ Õùü/·õ×fMkåÒ¥—Ü(Úµ"2þìœÔèr³Ýªƒ2ìV¡ôÿóQ
è ш§~-]DþÆOjÚ¦xµ5è5ù°Aðóœß%}+à„©¾ðÑþÏéXŽù⵪,øðo8D®4yÇØ5qB2Ùúf
= ß˜å6BË%Zù˜zT@—çº'ZDÉó±OýÂÁòÊhžÖð&Y•#Ìo€ÿO- "d‡¶ø!= X¦_QÄA³/×›¨ =M¢-,’ÇÁî¯VŠñhK1DóW:‹²­¶5Ìó•ðu£LnŠ7ƒÔráÔr%Î.¹ÞÃä{C<ÖÈ]¤NýxC:ůn¹³/*¸"£sWh”¾‘Ï+Æ(³âóÆYt<0j]9LMs‹ScÒíß r¹aÍ3 = †,­ýø¡šU¥9^f,u53Jµ­ìí‚Ɇ³A•ð#ç¾õH­¨hüÁŠTJt¶Md°JÛÝîtÅ+~ÈsOg›gËóÊ$«c§-,uY¸Ù—Zšý†Ún\8Ÿ¾—¼âéA”#¹'à×Áàò&v¹É>=}7#PbWU<wíî "ÜV]Åx%U£¯–ð]—ÖÃçv▇õ÷/‡S±@Ýˑ>DiT:”ÿi˜Hiì+;HQ™]
ßãT„´j\è´gl2X£/+SÒy\+#ÈÆ…²t„>äÏ(ô‘%†›˜–²ÞóuôëÏÈAC¡ON¾
•p¥qTXü&ۖCæáÆhLH\°<ìñU›*ñhÜlÙWîtx焒‹ƒ‘ÜdŠ¢4ɦÑèÕ æëdwž÷”Hϒ~~äÀƒ/µ‡$ûE2à KÌ©’KcfB°î»Q'ëÏà~¯¥+Þh\3€víÈOÿú¾ívUÈWí¹ÊIWsVþl .²0K™²·’&%÷?=M”S„Ý„ì^jú)rŽ)
³#eó§þ'6- βÿ²URp‹§/‘c°,Š}êùzI 5Ê\¦r(/rí ³ZPRÁñÁŸo;ë;T"¯æ*£þ8 u.1= J¢i–Y8†KmÙ:E
iÞæ½õíÿòè•ÊÁ|š°±ð0¨]ï°s!ŸÊ­Cp‚:Ýx5"( z¦æ/Ü<˜Õ–,¨9cÝM9@•4= ¬À=M=MæãHóOŸתʀ*õD¼Ï¾j<•·’WŽÈ%Bd,´?x\W”¨ÞË=M/üÔܗ÷Ía¼H*òûGm­4I8ÖR²¨—÷*NxÇÝeU¸I]îÝ[Çð@ŽfzcŒ:Ïy÷ÓgيaS炏‘žy ½DP0†D'= 1düŒ¸ø‚~€Î^.ä×z+GBû0‹<o= È¯(Ôí{DŽM¬˜Õ†«-u˔‘á3üæߓb&›hX ¤(ÛæÄCYÞq 8K\s‚Ìðd؅ëÚðsv›Í
k©ÖlVlXÒÛ{5ÿÇVaN“îç5³¾YRPô‘ä= wµb^Ùp\= )mbV= h„°¦á6u Õbæ•QŽ!e°Yª¢‡ñN™ÄLü,wÆìh—‚-æxAûQÚ3uÿ\\ävòeŽOÏl8%µaªËeÒjö½dò‰ìwÎÊãtÒ§-´×¿‰ïYY¦Ë[Q=M€òa:ŽyG/bÞ
õ†¦Kz0læ‡ð¢^D= ^Þoâ‹Û²@Ð÷fãÏ0)À.u­ùa¥/iÌ e¥ÐÈïU¯=MMd­ÍÅtaæsóãôV„ÒpÚê³SäÞ?q–¦]’­î¥úpUäÙë¿È¤ýQäù‰Պ§“ =}³‡+VѾÿü±¬Ï:"O%oo‹@ú2¬l»{p©®"¬ÚÅñ,¤«æÆZ\‡9©2³Ø£±ä0íg|CÍDP¬cËa8»@¤„Ân‹àU'­Ö[¦ÙÁ×ÅdÛ¥Æò&G:r ƒ¢x:j‹$¶–âD¨Øaﳸ?Æÿ7¿j×-oúFÆ3>}U[žé>#ï’}cÐå'—ÍØnÒe!,gª2z YΰV
u|êÂÖãmÉçñ’u1àX-ªbO”&dŸùo‘Vˆx€Íž oÊÄy­ãhúkä“çË!¯äŽÂÏ ©dŽá_µÛ†h£ôY,É¥åK4 zC\Q.aïE,#”´†é6§,(¡ú$-3÷ÀڝËz¿^j)hâù&ð4‚³œRĸ³êPÝ¥ó1ýÄ1w:= ñè>éÄï$z$Öh„[܆½KVˆ?F˜{ÕÀ¡Rö!;|ì ·¨oF›*À~Š<tK€ ŽëØ-vçדŒÅ˜d,?ß?1ֈó’¢ÓÔ´¡9nöwö²%Ăg¦ؽLd§Æy€ÂæÀÝ}ŒÛå“ß«ÅR--€­†´è4i®Z.Ñ<BAóI³œñGrqðqhp¬pô¥'©· #óÜÿ«¾ü¢”ëçeÌ%Î&„©ÆHe TåÎ/]ÄösɊK¡ò@§¼bzIJÙ6ОD¢*œ8ÄKUˆ1 2ùS¥½¶ñ±’¯0°õ±a7÷ )ùé(J'úîúºöåòðqñsò'¤‘rÀqØ:¡7h0æ5½jÚb·é76$1„ZÚ¸‹¸#³ú8 2ò€úº²ø¬€Þí,¨~‰þ½ÁÜHX0ò&®%Î%¢Jèrç&0ãY´*Ä|‡û= ƒIÖý´·ÜÃÀ¿·IÒÂì½íH‚W”NLŒñü¤p‡LbÙ­ç
\xïәÍ/Ø~§éˆº„. §oØùÇNjí!Á,ÆÛ¡õÑÉ{â#
¯[¦÷AþB´]h¨ô¬
F= ÞïMÈ¡Ü{ñÎ=M
Ê?Æy•€–ëí#žUn•¢ŠŒ§%©Bx?ôÝ¥5oØ¿ èù²ø—YûÅ,º/÷˜4¾ƒÁ‡LJp pïÁ1ƒ;3k5dXY ZÄ\ŒzøÚüö|ºˆ/²Õȇ [Kp˜‰*â
Ƀ• ¹ü³ wŠ6]f0KÄkÿ}X/’õÌéÂ^B]Pe{zËB¦xæÓ°ÂÀ5$Ue#˜ýɇǂF$%Yg©_µ¾EupïY6&Z]YX'¨%÷ô= ¯AbùÐz ’5ybðp]›z¸ÂÎàY±Ó‰!g76ǸYw¥Ö¢”‰j¡:1.˜p8v&ò)½Êsƒû|gWì45s€§ceEóÃ~E?C0±ò¸´µ,{•'ݤ¼)^Á•ÓƒO\ÿ?·YÒþ¡Ü%Jæ)÷ë^¥ý|¡mL "íÌ2¬LŒÈö¯%‹!õ Æ¡Á3¶uvIìÓTV#†_É,ðÆêÑW‡—s O‚´ÄÜYþè[¼ËƑɀG̑)ŽÑá:•5+ø‡ j&JÙø[ð1ÔðeÎÊßĞýßøâ*Ô5Àê­ç/|> seoupö/ü¶ìÄè’3%X@ЪàÙΐ„öFéznsÁaXgçµ¢›©"Xø){Œ ø·ÿÿÀ¦±˜¹-|¥m=Mº© 7Vêˆhxbi‡¸åà{¢:f²!T“u@iǘ4‘¡)siw¹’üèrÁEBÚ4,ÛÁ,{âÛótà²&HA3
Ùòúëà$ûŒ™ŽJPªkî/Ùúo #Ô%AÔ¤Ф‚…)ùñï,CÙö%#ôBZÇ‚j—Ò´-ýÎJ-ü îM­ 8Ÿ›y'ï v‰·¦!KOôd"} .
!þbg™²ë±¨.{¦Ê„®Wx.ãü·ÇP–ÊO™¸7 “y­¥¼7ZAt³ˆÈ…y²*žS¼FT©•l—ãüÇ{F_Ÿ³ó«g“VlX*üiDÐûõ“ û&c€yÿ|PžëUÃQQ½j—ۀ½2[àa}ûêà莾Î¹_Ž ɱ Y›ZG„³X²œÐ±ðdQˆÙaEÆìiœåñ]ÿ¬eZ¾{p‚ÃuÁÈýWÀ¬}^¨ sh¢¾9ªÌØc“BUŽ’>°K±Ìw‡Y ÿ‰Ã›U뛺Ë*Ö³BùN«›²ûmE®\-Ã,sÂEû±B+XaάTèý;r+—û}Ñ=}9ï¸3̲§\@’Ћ6M[¿˜Û儗óHÚ¼¦">ø¿›.ä‚–}ˆ£ëW|¬:¯aŒ¾ä\À»É[¦Ü넓]u‰ C³SŒâ؅q·…Àõ¼dYÈÀ{໣ô£÷#ÆW©>Ïд§\âŒÉk{]ˋ=}Ñv“]Á~HtŒÆVDÞT»Óý›U ]“³·V«R/ÙEÎê ±å?oþü5Ñsˆ±{}Dvg‡¢Mÿ;A¨òš+j«2ŸkòtÝO  ™ëDöÄ"f,b«Òùn·fbÙûýœa'$4¸d†R‡ÖWM!'oýü µ ü…†À»ÿtgòòp¸¤³”°ø'ª6òéšPlvgû¸ßÈ;ԏÚ_³#}!ñ¸B2Zy³m©Ÿòt¼å”®Ë²íL,Ä= |{Ƹυ†à=  Oz ƒóCºšÍCšš†*¤ùŽÐ¬J PášdÌ̐‹ÙPPÜ-0x:å÷÷ë(ÐfÀ¨’é÷ Çw•‘ÙL²$#Üè$jE®šQX¥6øç÷k2€æ¢vêùe£ù0£œ|¡(&çºùcóŒÙP’9õwŽàçÅë/04Qø´«%c:ÉÝÇçÇÞꙢ ¸øh]¡ºÞ6oê篅2žré‘$ãCûT€¯¤máùÐÁ&l…¢¸ }tÒĤeÙzƒû}¥žÝÀݤuÈúÖ¡ã¤é³¼‘äӝM+ÁŒ‰Çò/t<"%H¨—Å|ôòyûüpPÏ7,iٌoîÇêåÁ8ÈsÃJýkò:7»ÀË@ýÌ™•[{#sA?}F{ÍD „¿‘»;LSHY(rˆPÒñ2ÿ4ÖÊ&•d̞7ág•ì¬ꀒå¥Öÿ뒵?
Äë0 ä-BH¥{$sqŸ¹'Á(úQš+ᔊqh³‘B­Yæ¬ìÀóë@f±Ošöš¿TG­-I”ï­~! ½FrÙ÷]ўA
“ÕËñ*´&aÄ}$Wm¤ý^€Ò*qN>§—b•Íï {™¤ šÈ4ͼ1)N"ÿç9ø7 îA}eò°¼»p–I8¹4
’š„äôø†û‡5ù÷Ʋ™· níí²IÕNø±nø:ñ”ߢr¥'ÿ”læJQ˜¸3hÏÓJ°IÓTÔÓ¬ß#¾òMä=Mü =MÇ= D$B‰Ð:4åÄnZXKqj¥þ8ôR*ø¤QJS)Tí«°ÉÞ@àPÊ&•kÌ n³-ÈvÊØø,ã鐊yýnŸ‰‡5)¢éê8aÍe(wa™õa.¤}%f'bɔִß#8rˆÏÙý¡Ï:ϐ¤6Ü:¡x~f¦ôì&îÉQ™¸“êÿÑÉág™ØñB)Î5‘…m¢7/4ÎnI×÷/ëöÙÛ̞b ㇰ7'ZŠz„P^÷âãr(8(Ðöp*9’Ç/ØTTú\®:®GyÄ4«Ê„ ÄK‹=}4>f‚Ê,Raá§z¶6.³ m:øûCtìÆß§}H—Ü ëþlVqdt«yYey^ ^ñ˖?}HQ‚³Hk&œ6ãÒL’ä0Ÿ@Ü[曀º© 9Ö3°$°¤=} ‰ôûæÂ]ÔÈ¢ažqÙaˆùa$Èͧþª‹Ðւ8’YãêOLu·iBéA÷Ú[!p4Õ^•îÇ'»ï„ÃŽòdR{ƒu‹´?îôª¿H‹‰çÿš…‚?5Î/íÀœÿb#ž\ôLjÔ)ûJÌɟ[¯7šŽ ,º$ Íh‹„<xA¥vAWeû˜ŠõK= ÝM ¤<lÇrK'DËT­¦óC!òÀŸáÑ;0ÔÇOìG"ĝ®%e CjÌE“&ƒ½Z »Y ìԇix‡-ÿfäÀŸSlȕ†lêÏÐ >³œ©Ï¼#p¯”zùj D[»ÃÒk:Šÿ†%ozԋ{эלAÀv™^äÌý&§F‡ ¼ËWsæÔ5‹= Z-æÜI!0ANH¾üûö›Å¥!”¼s^x¦lŽ–éP˜õfØpʋ<"<æD‚“Tç˜Ôè  Ë°ÎŸÏßÀύ÷>𬻆ÆISopJá:
NÜx16;hi ê–ƒéßÓöO÷ÀUG›(GñÞ_kÜ,Ý·÷Q¢Ë|¨¨Aa½Ëc¤ß©:À7úå#KÈÑôƒ¦ëc%¼#[= à+Xß½œÁ+EúD
:•%RÌ^ôÙoâÓâF‹£ºÞ›p£½±Y7-
X yk~h~’†a—[Í|’|Œjd®%ï©ñ8.æ½ÃkóAÛfûB™è‚Âo÷Œ—|k0²Ìê¼J„ÝЏyùDyŸ3«÷9øí†¢®ÉWÍóµ 'íÆ!Â3ɯ¯ëîì‹Ó$W2·63/™ Ž4bJ¡éÆÈEJÙ*"¢¤§{k:» )v™­ù3‡)ônÐRªó#&$YSzhwœ&üX²W€ÚANrg?÷ÐÑF­ž­†î]÷·˜Ê“QÊîñ:¬ã7£½Ýã7ÃÃïQ=}̓‹#íÓ-çšYzِ·³¨ßM¤Ä Ûv–3©”ÖÓS$òrmd\\]› !èúјw×ð4¬|"ÿ¦vÿ“Çë¿7µ'%ª”¦nѧ™ï= %Ý% ¡ÐúêúÊʠՒ¹¿êÍ}Ö§¤$Î9ï^bfa¡¿%05º _áêÍìßc m21Ø Bëº÷§ýИm]Ê_3= ,Rÿ…àÊê½CÐMìBÙwNÜÕ¡æÍMÐòÇL·vïÌ8I0¾°…
NR鑤6òڑ$4—qÛðãjŠR¤Ð.î w‹u؎„ü˜mÇóþá¤uÖ¿¡#˜¿³É‘äԝWÿcBaHW6G ?ÇdBàx^ûhƬà4s5ÿ ¯¹ßlÞo%bPÛÜ[G.ËCS‚?#SM]<×¢¦ú^_c«LFŒë{G]€FŸ<^m¿…{Ë<=M=M×zêúó“²ßl6žú)¦­qN–àeÎC.DQy~­‚°8'£ª6•8"øöò ‡±®YnË_«Ü;´Üå>#4ú.‰{”˪BƒM…1}Ú®«/L8³®z;êU”û,¼v­{Q€ H+zsM¿¥ÅÍâÛ5VÎR³|ÿ‹Ä»>&’ÅÔÔ2pƒ2r]•³¾›9Xêh9ðeû µë{À4;nŽNž^~>3ÎÀOÛ5ê:aùëS@sû’iZ´ë= ‰­*(•ò§ü;Jb¦F^lÁùr7°"þv=Mé•훡£ÛÒÆÞ(¼!mDdìÌè\ÇG_O¿\i“û~ÃÈKt›Sà4dñqLì5wÂ8'S诮¸?ØM ¼ãÚcþ\_è/m&)ý#ì“SƪÚk%¢ӄ¨“j¢éÔ¯ÃVâÎȽCœâܨò›ã Ï^'Í8=M ¯•ÖÝ*>¸¸ž'6ÿW{mçžCêµaÒ") xؔzà.º]­lÚÆp‚:ëdI–3¨¥›†ö¸’‡ºë¾L6Š4+HvöÈs× 4>óX[2*o²X–þTw3NÖíáOAU¢ÕñE4\(-Ù4.ªTr§ÿvTÕ¤ 8ïØ%p/ޝ
ZVm‰ÝœÜ)ßlðz§:8W^s=}qMnΜ‰= [C¤¤øÎäù3ºÍ†'G:âà!OrVFžCO½Èþè’Aƒ9‡2Âæ–ávàOÉ0–õùÏ¢b‚‰,T x {ÊýŒšòÂôD|1¡M—ÛÆÜö•¦:‰×!mƒÇ.ô§ë/¯o˜sï© ÿl~Hÿê4
N çB#Î}yg~+
YDø*
â
Ók‡¿%©9õh Ãy.êçþçŸ/U¸†q{¨ ¼?%·Ñ§h{ØHÀÜaw¯þ£¸I%©4&ŸflJ°ºåeƒÙt¿Êohj2hHSå
€øzU)bÂöccI%&3-/=M ¨È«»èéöHY¼Myîîï¸å9ÊxìN(ˆƒY„¢Öt)‘5S‚ZYÒ)ÄØ›aM«Ó˜L àۂÉãd€Á>¬Î‚Èú
kÝÙjPäÙmi%©µ¯·,«xIÝjjáÀH^Óx&øžlYmq㘚YQaвoM»---ùà9~/TwÕZȲ<Øî×m’(ÚÙkÂÖK—hõ!PbÎ"µIÍ¥ 0°ºª·«©2-KbôÓJ¤/¤ ÈšŽÇ‹³W°zŠT~Õ
Èt㒸r”ø¨j®"H”ãòø,•µ»Ô;ïɊÑsVl[uú—o¨óÏ3,á“÷:[ö&±¹¸G6þ\ђ™Zn‰d&ñ(Z*)<1l!OŠòÚ²·èœ!©žÃ®T¯%Ì»iP°€Úˆ ‰]ŒîVy¾©A¾)ÕîÃH2&䴑SFYð
rŽøÈSSxèºØ“§b!m%Êõ¤NAMW(»y9Ôîÿ“Æâ2"ò¢R=Mâ…@8#%eÊè+ŽÅ°#µœÑ= –"Él^sõ¦ˆ&ÖÅ×3„5—ø¢o\¡.¿]ÔÔÐ(£*Û+ãÿh«ö‚N¼¸^@ˆÙö3qܪç‹ 1yün!€xQT““v4 .ÝÑè“Ø tj¯!7Cÿ}ÃLQݸ'y‰j—¼…¾|džÃ’§!X@\;A5êžúîü>e¿$wo.~î[œZöÒ$«o—©JêöÂ"œz:”Â[ázÕä
{»= §“+Š;æZW}ÇÒÁZ{þ$&>§›|@!/¼È( 0¥3UyêB&)$M-Ô¨T¿>Í·@Ü_sV/Fum¦g=M-“…A#k߯^{%ü1WOtC¯9µ@Wá]kHÜ
[QQC¨@_™)Û[ä~Á£i\yM{‰W̃¿_ƒÁÇ ;ŸÄÁ]BɽÌE´òöKç+ ĹžØ‚GðoÒ$ãڝ޾>÷˜Ø'Á“ÓƒØl\q­)/ Ä9©ÞÁJSÄôC£~Åã4îá?B5óÊYyÿýÊ?ÖraèŗsÕ uƒ½î± ßé–T.7ÔÎb4ô{!¼áÜÔ$¦Òj”o¢µ#¿¬Yžc çN‚2(ï¬%àLFýâA@{xt&¯™ ÜØT¤&N‘æ!Cž{)1(֒jØjisÔ®³š…O-é²^ɬlߞÄÑH»ÈÌtG{įOx›ö.f­’§„覥.GòÇ"s:Dõ;€:e@wktwïy²wWÞÙw -w9Á,wÖæKx@èex¿ýxŽ!ŸxLĽxÂÚÎxíàx ùóxuúxêxOÅ2xI= By/MyšlZy£fyÅsykyÏZyú‚yÉ«yëÒºy–Ây›JÊyÓ Ñy´Ùy«)áyVêy6›òyLöúybyòÝ y³eyÏöyGŽ%y(-yAÄ6yùK=}zZ•Az_ÚEz‹IzfQNGÔ>{׌WûÖ¨‘±8í¨ó˜Èô¡ñ¨1§¶ñړ±œ‹Jma1-Ï<ÐJ\Žt=}½ ¨ëIÊ0Ôr SØI!×eî_"ØHý¼\ä —i«ÞíûÎJdyS5ΓVÃèC"Êkm=}^íc՞âžô­nÙ±¢NôùoT”³eYH-´Å×å5$WEÛ
í‡É—ý: q–f®Ó®xÒRV-ƒ
¢K@"PgEŽÝ2}ý{PÂÉæc#Þ!ì—Áx¥é·‘n³ÓÛû’«sškÙ±|n½Ù²?N™CYøQ'9_c4is6†«6ÊÛzüeƒ9ÊàŠþŽ$\QQpp4bԁUL=Mâ#f¸Æá=M2£c”D¿ÐXÐï·LƒÚ•„ƒÔ®Á:ßùP͊5fÓ,–U2âo’¤ÉÊ=}ò—^ چ Jà5Ö½ƽdbß41FI¨Lהd#pŠ«ï‡ëC™âDíÞ@ y=}ÔÈìɛ¹ºüÅñ½Úºg厎¹–Àž¥7qåTx–Èò‹®u™ˆ¼öÕÔ5¥0nòùÕde©—‹Lò€À¯PÔÔey3åsãê‘¢PášÐ·÷ä&±= ÉŽÐ÷…Œ-û|„ʼ…8‹³’ü ¨Eo†O7ɏ ƒü¿qEe.¬Ú
-Å·—Þßï™ynÖÞ´jʙ&.C
ÞlÊî˜.3ŒÐƝ¢i©®«àé¦iš«¤ŽItt7[Â!ˆ®Ë˜¡ '§¿ŸŽjVågg¢lbpFfggz¡m®‚aaÔÔS#¡3d¾ÌnSg
[A&=M$óuØÓ¯Õ2‡â (o7âßÜËúº­·2äjγÛ×ãž$lÑù‘žÔ/€A¦ ³Bs}üü‘àß=Mæ¢e* â ^jÀ ¾ŽS²^Ô>]TÊL“‰Ý§—MþÎ öÌG€]é>MÄtÚû˜´=}¦vb«&{wLL,soÇ¢Õs¯× ºX×-“7‚Í®tÁjäl§T~6H §®Ã¡¦ O¨b¬è’OÚ1ÍX„ÖC‘Qb½x—«F¢¬ SòOœÀ\–X°‹!šI¯ŸC¼Ú¾,gC¡®Ò›=MAìkà ¢>TZƒœ ;)T<CÈ;õÏ6ðÖzN‘5rÊT6·lñ14ÝJ¢··7 ÖµlWä.ݑ)À3%?ÀbZ:´« ÝBÖ³«øØÚhԕ)©oÖÖ铑ó ãÐFaà w¦%ʉ‡ç*¿‹v-1Â>’mXS3 ÙrD1G©8[vo©íûÑȧ q¦ÑJëá·éP*••¬±×= z–‹T¢ü ‹$?xIÕ®¬ßtQÕìãàTÂPá¿ÔËP›}./=Ml¡M^IïkýyK
"A²l©¸Í6%ΔçÚJõoD=M lXÑI|®Åq‘šïóVßÂ^ՏGŸ† S·šÊ¡÷6£ïòÊ]ìcÁŸÊ_4 CGG‚‹u²s,¬ ºS6²J€ö‹¯mÁ«%›©ÊU‡™[H_=M¥[&QH{0øzŠ÷5® žz—V2^s÷j2/œ‹ywÊ*È!̙ï(Æ,dxÔ¯÷Bö1ûuðvUöôV5„˜±ãF±\yõnf±°]ö„bÙ@¯ õIÃÚ(Ù¬.Œ˜ÑUþñ‡e§þ¼–èäS Õú-‘ozÍX–-ê@ŠÑrb5¨nEÇÕsädVjrœ†a3p¡üÁ‡7Ëq¡HlÖ»qÒ%Ž ìy½&Lô—¾d9N?J;}{<;í¨ñ:ñ¨ñ˜¢ñ¨ñ¨¦¦‚:§V>E–+µǚ¦^žúrtëYb4•µ‡çü{òEðôÐϓÏ^ô÷GDMEPÞ™Ã=}Á¶"Ìùtҙyƒ© ȒA<V=M´ÑÚðd/«ƒ]ª¤]F„ÚX‚Þ±Gª‚®ŠiÍË<LºF^åp¿òнŒgEŸ!À;¡5ÜùҚéÏg-ýо ^6)7¢eªuì_¼Ø1g¯ïà§áHF çã%¥%Â!¯†n iNQT\l˒º/dêà7½Iª½ï˚åAP°™°ŽžXPoÞÐ˗ÞCˆ´Q0Ï^.ÉGîÞß#öÍ*:Œ¦:üº -6À'ڇ5.Ø> šÔ­F-ÑÚëS-Ìï÷²S%ú65FQ ·…”)~õ$,çÖ«¾†„’cËøƒ3¼®Ç$¶ÕKÚøxý(PBt–ñR²˜ƒØ‰=MËhB§['èþÍpÓ èç=M9)˜ÿá‰.´¦pֈ’¡[t·e27aÍ7§W„n®r©sEšÂ«µ{×í5j Æ^¯‚áónPƕ€Êš°œ‘ô“õá‹'ÒM4ý‘ÿÅé=Mœý^šè0Oe:Ø¿²ÑðóîcºN‚NÀ5W€,)ðÏãF9hu;º;»:FWÀx·x±ªx˜Oxìxô‰‚xK&tx Ágx{]ZxgøLxí’?xð(w›Rw±…ówF¸Øwmê½w5üŠw9_UweJvÔâvvi¸ô=M¬öæöáÇb÷¼d˜÷Ä÷Ûkß÷'8ù÷î÷Ñ/÷lìBøÎQP?ü>›SAO{ü™¨ñ¨ñ¨Ȩ5jñ¨q*\-ºTë§ùSUêL±„ô}ÿPd$4ƒ5AøË-Ö* ƒ@tÕÛa(
¯ ÿN„ íͺzs…WJ$c+LîjüMoz…ñP= Õj$krŠ1"ÐвÍyð„uŸÆ²ï82NfÊ:O"z«6 Z=M²:ÊŔJ…•µNðázfAìܳcn‚-ÆlSÉb± ŸÙÙÚÁÞ&‡É:sÒêÖQk‚¸l-ҌpœPr­¦>6º½8(—À™Gtƒ΂sª¹×¼ÐâSr WEií$E¢UêNv.q*ª~$\*w¡´*âóuz^¥6F™ù_6¦ú vG‰·²Ú7‡çêwɐû$÷Mʺ‘\‚‚Ðÿ%WÇ 1ThÁ¹âþ<˜‡=M"¹»<Â1TLŒ¨‡ûFŽ‘1p¢Îð@”†çãÊò!d–g–'“: ¼F8Ø3ԄF—ÄG²ÞŸÚBŠ…@ÀÙG7ÖÜEy£J-îÀzˆn>Ä0D5K­M}4:‹sɀL3šÅ„ŽoZ-/³Ñùˆ7z„¯e_fÐU¤/ZÓüE‡ÂŸèÚþzþ-{zÚë\À=MhÖ¼= Gc€ê’t²Oek:öÅäªå4*•‘ê^íùÙi½<{é?<ۛwñ¨ñ¨ñ–zñ¨‘
1Ƭùðžv”v97«2j€¬–À.:dÍMzc‰×8ãCqU¾7ºžóv¢g‚ž+©Wœ.‚&?-§bÍ2þ¹šoÄ= gfì0Ãm’VÓªá:å-¡ÕU½4:ž°1ª ûy–ά•wwáÀª¾qÁuçõÝþ|˜%W0s3ÈY†á&Þ¢»h>éhotÚYUÌ÷ýÓSN¹¹¥ý¿áׯp~&ÔTj’ÕŽR¨|.^Fyiìÿ\ÅÚª”œrÕò™q¸¦~éÿÑê¨'ó®IÜ٘•IÏZ®›~Ü^ºvýubq‚sÞ±þԊ½F¦’áØ®€âH⊩— þkJÑ9ü|Ё/÷s¹Æùoª‰2Óþz·”®ýÒÔ¹·,ûJ9woE[%‹;Áļ¯9Wƒ}ªK&н$”Z *K‘™E¹Œc‚ZÂ-™M@«ªI"÷²?ª
€«[vÍÚ©mènÅX*ÞÃÙVˆÙŒGrò«fÑ¥´g´Ä¬c9R=M³9óN
À1?a9Ùûã_ÌÂÎÓnrÄHsç‡gÀÀ֍#í† 7 ¡õßlõjÈuʯBT"Ðü¾Tµ¡M­[NԖQ¹¶íc«!9§)•!ËøqB¢¾—uQ ä͂wiùªyÁÊ.ôŒ˜»)²@W‰j4}×=}ÆSîòÓƝÙie!MÁ6…cô–ý7­iÐ=M#’(h —e‚• ä‚ —fã[-·@)ÐOèç*ŠÐ·¥éÝ4:3ÀŽöôÕrÎ¥ó™~#Òp÷G瀴˜hÎzŽ=MưóvkîY8µ½MV û ,Èçè¤ŁeÕS¥(҈ÿ¶oÎqæÔ¾rÖ$E¤œÍQaw¥Â㑏‘ñí7„f#qÒòŒ-ëÈçë±qj£b(v”XWÓ|þ:uSë%‚b–ØÐö+g#ê˜Óx(€›rˆÎö”·øVhY)Ò/ÛòàJµ#,# ®ªjÉâµT6éRy‰Š)ž4ÀyiÈ*b= ùZ$8Ÿš'"ì>'ÉU†;¢Aë4¸»?*|ù£XŸ11|±9mÃÜKzŸG,5K€-\¯™HœsÇã¯SÀþóØZ5¡SVÂLGF‚cÚtý&N^7³²?˜xL¦Þ€/°Ã끿 Lr™Ã3‡ˆ×S±lñÃçªÆlÑ‹)\ É¿êA_‡—^œá—}üjM5y…óœd ¨ßϨPÅ৕^ $„dš¸dà1ôÏcPTh±m©À[§ÂGîTméêά$ÃGÈæmñ9_žFTbD3UC÷²O FQS‡Q«eQëªM#WKÓsˆ]<Î0=}f±DXP#ZP3)íûÇ:C‡4‘j=}P²@]YFïùKó.›
Ú=}-
GÇ>¯—<OÎñ¨öüöì¨Uúñ¨ñ¨ñ¨WÈ":ª *Üz:4š *ÊúE¸:cg:é4|Ü nÀ!rPýŠe'áKô‹ÜƒÀ´EL<Eöe¿£4‡æø¡ÝaÕtџaøöT6úa·ÊÔÓmÈ;NbæÔ° ́÷ØÇ÷8©/e>
ÞGsÑn«ÎÙhŒµ –%$ïƒT‚XV)¤ôàˆ†ÄàU…/7:.cƒÌveù_æ‹øcýdæÃÒÀƒhŸ8=}I¿P 2¯y âÕw±žۗYøœ¿šINrŸž”hÖt”xÙaÔW’ ¹gØù°=MÔÔׁ)õ±·†ücd§&ڇ)¼_Q”_LœÎ³=}™æT®jÉ+¥¬çìÊ ·‰G₎¡ºO.Ù­ñ§_€+hׄœpÂ-³­C"‘?ÎóvÃ4â>T°n»Š8Œø.~2ÒYú·“4Ööº÷ó/Όwc2üØ»"©_¬0÷Fz2vn£ØNÊ¢t=MŸã>‘ô1/CñÞµšó‘ŠS1þÁ‘çï řÞ6íqäJçµnìj…(¼¬ˆ²ùVõlûðf°æ±ûð‰c®uMÍ <òQ(ijѬF+máp4°– -}FIª‰®¤Á%¸N…g§À»wí£Ã(%5Jè€Æõ˜‘Öˆð“,ùAúb‘¢~Ê¥×õ“@ú7sçÿÝàPkÐ"/íiþ®=M'ُÍõ腋·†l‰Ño“3’­±7ÍZ¢ù͚ Ê:&Øf0—fù϶Ú&%ʀŽ8¼G譒•Ïú= œê†ö*Ò  æF­51””¶%Ï?§zi¶RYÌz^J…ؽ¸fŸò“/\2¥ªÐÚr7.&Ÿ-í›EtVù•²•_Vg!Æ)7§TwªÁÙež6‰Í´†øŽÞàÿLÍTq}UGͲ5QG ªÌ'ï Ì֎öž÷¿jÃËúp½ÿºëò_¹¯Lғvä³÷ê×#=M˜ˆ$[@z•»ã­œ’XŒêaÅ1Z¾;‚æt©±Ù).ب§q¹óhñ¨ò(ybJö'hJ:ø¸³y1¬b9ÉeVM]°4eSøO͎*Cxæ@ÅýäÓ¸ýoÐÈýÍ(Æ@H"¤}
õ‡Ü‡h{“g¿¨À~¿ÑxX¿u$õˆ+ãpq¬˜§i)çïQ½fï×Veyˆã-Øîî‰6¤¸ÏÑo…š–•,ȄÈbýßJ @=MûHn= 6 ˆ3Q¤ľƒ²„%֕bèÐ)†"öúÑ=MUàß¾¨Íö}ØâÉ#ýÖ 2ÚNB­ŽL·¡xY[Ó-)2,–”·ÁåxZëÐÒU3+ â½³ëŽ_z«xÙ
I„2ٟ.l€÷ô×BYc" Zâ¬Fø”wj©i™î»v—êåj"‚è‘,jT¦´ü5H—óíÔ9.ð²,&™ô–<9£œ™ýÊ,|j‰-ÅñE)l¿.¥Æî—:=}ʃæ!¬u©ù3eê·¾òv¯Örnjy:I›÷7!zê³6º·2(÷$vi˜9z×:ý†‰r+¸z3Õ°ú4tˆ7´|f
o¨BÐ(œ=MŽ@ªœ7ˆHˆÌU+ç ÿ lXÌìáNï*½Çé#’Hax몙Šg5F²˜6Û£–š9J¢½B[¹:{3°SÇt|~Ûð®ci:UÁfÉ^Ûð³_ʂ|––Bèί ËYú&žs~o Vœœž ¢N£#ÔÜyWrÑȁ˜S—¦UƂÁ,i¯=}¹ŠÖÃIBhÛ¬Me董S©¾&1=}ÿn3‡a÷𦯱ó¯Y(:"»¹zXÜÝ)ÇFø˜KwýPbÇ+­çv)“µ›ùDh-½^.Á›(¾ª¸L©%¨ºó2J{ؑ[¹:Dÿð|+õLÇ׎\Óó\O&þëdyWi=}À~]œ2vM–逜£/iÅüƒ"“/Åþþ¢¬K²ž²¡=}=}‚b}ÓCþ„«‡¦O7´Õ½†\®‡þö*¤sú¤Mu߇žuÙè{ݐa·Z Ž¬Q=}µ¡^´ŠqYI±ü3—·ÀÐgJôe4£ØÐÊ¢ØE ž™<‘£P]¸•\QñOݘ™Lߒ€óO“Ç9"Ŭÿó?†ÒÐÛ-ªD/^†,ëíO’6ÒÜ~¹T…&›q‡A(—T}
‹”Éݛtl àWje‹W⻵@·)dëÛîMåñ…WŒA= ±e,î|Äý¹]Gç¯JRœgݟìÃzö'Ìg÷i6nLÚzÃ9\݂.z7ÚÃJ¨]ø9^=MÉr]ÔÅ__=M-G̅ˆmé’=M4…%ŸÝ_uæ[º²@ô„•‰úòƒ¥fÞ¹ õåÏÁd\hðM% ‡ €ïjóË÷^F]_Úŗ-ã҄iHÜHJí£6Á‡íH
[ â¯…ź>·¤Ÿ¥‡†]1i¼!†ìÏäR~õc=MúW|öÁŽ»¦c¨hDáôV>¤óŠ€r¸¤&NªÍÙ´=M6)æÐBد¼^= ð ñ/$Ò+ŠÒçyÍ12كŠÊÞT|ž¾T0dqÜßQ~UšáŠÖú$ àӌ¯áµs(Z…öâGÝv²6Î6ùŸÓÆÑ)'‰™¯\”ꉟ¤æ„è¥/±¡Bm¬©žLôºÔ1
$–¡®/= ..3šº°ÒénY—Ù&Ú¢tȐ' NçtKfZõÂhä-îGÈÐ'õ€ç£Iv¸§eæ iž^=Mn”Ê“¡Kû•ÂvîúgÓ)´^ñ¨‹¡=M¾Àïäëç¿
ýt$Ñ+ã&d”µÜ´0OÜ¥ÚÊËo³Ûëõžèȉ¦Öî= 2e‡W®æ})’´Ñ&„õ€„¸¢†%ß…iȇ¸c´²5\6¤«îø4*6^31)¡·ÂØHŠ×ø!?.æÖMó*9W)¯‹üö?‡"V¥!-iQ†³
æ³íÉ#(7üÌøwè(kr­wZ‡!+3@93€Xé­ÎvHô
FˆÙ-Áì0÷7xzn-fÚ,bš6ñÄɧBF–PwYÝÉðzpL™DúÒ¯€›£J31&Èæ6š4;kYǼrÂ]n¦?1奍˜OGm{0•^g(¨Å$! 8uAb¸^£
r–ýA”nSaýQ®ú^ÛÚNEbóÌâV=Mþx xF‚þ¸û÷7ë¡};É1H}"›—çC­ s©“]9¸OgQû_Id¢ì hL¥Í+Œƒ|8é'Õ?ø¯?)ûëjÔcãºI1Q] ™Yÿ4³öñ;ÒQ}tôCfƬí?¡%à‹˜Å¹mgnË܆ØG l­¸j‚:o@Ž@§ØÐS÷ˆÄfúÏ+]ÐÄsì’ÿ%Ö]WyÄsg‚Öƒ%€•òßf]”‡Œ&L@ „tTiÏ ²æmõŽŒ}xõPêÄÀ¯²·y¤¥ þg5¦Ù|Æ¡ßÌ ŸEsÔ{ÑÍò¦OtF^PQ‡Î8d¤ør€ê«âf$ o¤ãx8x[Ë(‹†%ÍtÉçnÞÔ6G$AXݽϤmÃi2Üiyd0¯‚…*߁á.9žŠ*oíÁr‡·2Ã(s´ÈÅz÷zY¢Á¼öÔ°ç0}Ñ4µEö‘Ö r5l®Ä'1Æüç\ã>ƊøièLï֘À|e¨ùRB’aEHÐ¢MßÏãØª$Äß=Mç.Œ·–¼ªnD õ´÷"T·ñy)5¹’ùÊÊ(·ÕJϘLr4jŠô*2÷5=MŠù#¯v#Ñå~¢z>õ8‹ŒySæ É{Úõ«:£¿!ÓrYŽØç µÐÜ ×>兘ۓ&¼¤æ¸kgz;Ö0}Ë«]WÒÁþwqIM,[x@UbœG'1ÿôÃê‚(}/Q Ç“‘ÁÛâm¿p /|¼éBÐc«Q (ë3º(z2é¹:ºoû;@՛S?¿G†S‰kž«70>³r<ÞaåUGÒ;BAi¤°óGåÅ¥Û7BëUi~vJ;ãG}@OUm½ÛXKÁnnál!kýŸKe[kWG¼ëÃì½ìÉìÔLs4F³SÆAvÌ{¾š—™„€ˆ˜Ž†–‚’Šš ÿ=M  ˜~Œû=Müj[cowmuai\dpxnvbêÛãï÷íõáé|~—zswuil= hnvúóçíáéìôøöªϤn¢
š÷§ Õ°VrfSu5€a¢Rð¯/‰×u®9=MVñ'3r)w·58Ýő9Ä@ð؎!i–ò¨*Î"ªy¹84òÛí¸“‘°2ÇàV²Zx²(îòú_ øÞn†Võa•óÑØ6‰—ó¹*$&Jš:Fãs!p‡Óú±"Ì.rZø5/Ɩføœ5Õ8RZ¹7
ä(Úð³4³Îæbg±'Îʺt°=}F6‘Wó15zöœ8½8²ùîTòèu8Řz÷µ1Éfª‡Mv˜3û¥Ôp8û®‘&7´âyîÕâŠ8»á«ôZgɐ&Zësª¶@†ƒJۛ<K'ô®áP¨ñåküJ{«974žñ>Óëâ@\S@;ýnñð±æ§ñ(ñ¨ñ¨+ò¨ !5½ÔTT7}÷‘‰Øu°û¼jÿ¦°%æâ‰(ØLT7}÷‘â™ÃÛRðõµ$=Màނ²‰ÆGy0™âŠÚy³«ÏÃÛRðõ¯974^‚±‡ÈGaúºM:‹ƒf‰ÍcîKúD²àç?–´=Ms¯]ü‰oÓÄ·‡= r•ÄÈÿìÎRˆ}j©ÍÔ!¢ašŽØb„' ˆkÃÂO²Ü”
o-ÛÈsƒUB4 SïgtW¦I-ºô­ç= Á(w¦=}õ¬Ð‰prŽ4×¥©ðU–„›rêÕ½¹àë®h—ùâÙ虍
ö¡›SŠd(†îϤ!ŒÖ¬ÏGI.ä=MՎŸ°Ï”TºÃ«ÐLç}–ó¯!M\‰m¿„·} r”Ôˆÿðî҇…ª©Œû”Q›Q‹˜¡R„((hmӂϫÌ= “ÿjo1ËÈt•B6Óï"W<Én»¬·{´”Ù°ÿïø"‡ƒ²é̤!œUš‹¨‘¢VD()pŽ=MÚO¬Ù€ÓJ²Ú0‚…6
ÇG"T —¡TÑ,Bx¡®ÉÌ ynU¨sˆq‚ÖÈ 7҃’V¥ÝÞFÒ$Ÿ®mԊ䡿ëÔ³±,í#þ¨Eu½!–%óu'aÔ¸÷¨uÁ+ÓÍiñ_Æ4ÐaYñD„"KZVÆI×b ,AmÙJäµfëٓ±+÷Ä#ü°-u÷‡"†¥óU§bÌx÷©eA«ÚÝ òc.4ÌA)ò<6„ ;*ÖÄLwb Áí×z¤¬–«×‡É+ô ¤Tív'ǺϰȠ”~¯µ3¯ Eôםiá+TñKé™lt¸ô~\"“Ãt·‚˜2“Òx_ò겇
‹Ù‹= ÑDú ^q C$X%NžÌF?tü[a+LP›€qKəklxô|y¢ÓÊT·€ˆ²“ÎX_ñâr‡{Y‹@Qž<z >q;äX#>nnͺL!Eþ“üšï/¿ht|¡‚5j\eGÃ¥6&zf꺾ގyTTQê28ÞºŽH¼°@{ë±VñĨñ4Œñ¨6(ñ¨¡)®TòÙÑ$T˜ü¡ôO)”„ýÁµdOyGNùv„œùœÜ[:•=}Âýxý*ÔM¯Žó]'°ÎÛ¹v\,çSf’±âÎä'´fЁ€ëàƒLyҕ ((3s؃ìXBIB¾q×¹Í{ørñ+ڗ-·½¹oâ@® /]ãTªÁGà!7a³:Sˆ ÈÈ£gÖñÝA—Õ|ðh0£®ÔŠ˜%랍5OŽÆø
‘ŽïH·ØrÌe¯Q(Ûá^Ð3ºSÏpß=}…ªJ=MŽâ­ s|²…¬W"ƒ™½¤’#͇lyÑÝÁ”À“˜:»uæPÛñ1TÈVÚÂÙÀª/‚I‡Ýy Î/§‡ï7|’òŸ±#m˜mÛ,ãFjG…~.¼)wº‹Eù)•–Í —Ò(©aÁ;¹6x)z™ÉfŸ¬½P“ ÃÁ¥æmºÉÿ¤Fp±FÄæUwíŠcÆïô
[S¥#,4F<C§ûš’4ü
Óâï3DŽQ÷øœOœüN¹´œf'Á éñwl@ÀÆ(:l¯Z?;ÿkG>Ô €h2¶ïY] gªp¢Ù"–9ÓÇØtÔvU U£Ê‚÷nÎ\”aŽíˆŸØyX=M uே$×)þ[gÁ„GÞëU¡œrGþ-kHþsçJ.ëºÁ$»;;KC}“l:厠›‡¾ïÓa¤nÈ=M¥Õào‡¤Õñsèâ´ʼ“SNìl ÿ SÅàlP–žg—'Èü“WÎìtž°)SÉ lXžw·'
>´œÙV.ƒ™t
Ø.‹™uŠ è.“™v
ø.›™wŠ!" ÷5wâÓyCg;œS»;iKKÞ£ý•6©ZÇaj¸²4¹±8
é"0µ±øÚ˜&즸±(é8°Y±ØÊ˜" 4°Z±ʘ2 8pJ±˜.7pбҘ.¡7ÐBö-œ’™'ʘ´ífš± é7B±0¾˜¸œ’ºb±0Ƙ¸ž’:‚±0Θ¸ ’º¢±0֘¸¢’: ¼˜y À˜¹ Ęù Ș9 ̘y И91Ó"÷¦ø¶èœh¦•Y1íâøØü”×× ”Øü˜ ˜ûgÿ§ç' g§ç' [FԁíÍÔ'Ÿc†Ô‘íÑô'°{R¯}r¯’¯²¯ƒÒ¯…ò¯‡¯‰2¯‹R°r°’°‘²°“Ò°•ò°—°™2T›¼DqSœÀT±SÄdñSžÈt1SŸÌ„qT Ð”±ngüP»kGþ U´•1-Êwۈ§$.7÷ºùz:Ú. 4"ù®:Ô"Ò´nº1¹r6bÊÎ÷͚-‡ùv4âÊ/Îù=Mš5‡ú$v8ÂÂ+L™ýгEšh7HÊþ"+u7I
þ2+y7ÊJ3m9NJ3q9ÈÊ"3u9É
23y9:gD<»{;»kÖLýDZìøf<†J=MÂ/]8ƒŠ=MÒ/a8„Ê=Mâ/e8…
=Mò/i8†J/m8‡Š/q8ˆÊ"/u8‰
2/y8
J-Â7]:Š-Ò7a:Ê-â7e:
-ò7i:J.7m:Š.7ñ"ºú¨£z Ú.&7vz
.67zYÃJÀ3\ÙÃjÈ3^YĊÐ3= ÙĪØ3bYÅÊà3dÙÅêè3fYÆ
ð3hÙÆ*ø3jYÇJ3lÙÇj3nYȊ3pÙȪ3rYÉÊ 3tÙÉê(3vYÊ
03xÙÊ*83ºí—B@;»{K¿Å´î8–4¼QB!B.À4þ·¹lÓº‚-η= º¢-Ö·b:Â-Þ·dºâ-æ·f:-î·hº"-ö·j:B.þ·lºb.·.7.·/7/·070·1714)6?ü>}[;;;ëØ¨ñ¨ñ¨ñ¨ñ¨VxÜÈí5•&éï%U¦êò-¥¦éî9…ãݼqµcÞÁau#Þ½u•#Ý{„p‘Š ‚¸’†˜PeàՁ$àוÕ$ßڅ%¤àÖ]މz&Ήx'.‰y(® zďmaʟ=M_·ƒbƗM_Ô¥Á!Ô§ÕÔ!ÓªÅ$¡Ô¦ýߍcö÷͆fî×-†døç­†Ï3&ªø6=Mæª÷8êø4 6jìA'ìUÖ'ë
E&§ì½= ’«xҘ®X2˜¬h²˜ó¥V¨ò­¦¨ñ¹fèò©ÆÑ{@>§+{AH‡«ûB@¯ëû@<ÍãÃhÙÝÄdÉ3ÝÃiÑó]EpÇëþFtïkþDzÏ þErƒÞÉl1³^Êq!sÊm5“ÉSì¬'>Ç_”;»úõñ¨ê•Þ¥§ñôW¢ñ¨@"Ó¢ÿ$èP.Ï’ñZ¼4ôôDk r­Wkt’7"9ö= ¦î»÷nº™P>.mJ±Î}HQ?Û\ÂQWÃ{ŠDxÓ¾½GUë\¿_E#ûÀSI‹ü;MÙ'\ЕgD_ٗ•¬_M>à/Åj…m¤ÿыÍdßN’tƒ= ¾+j¤œ€KD ÁNŒSdÞJn7‹Ì€N±¿ÜàÃiÃKG\ָߎÊpü_͊v îÚ­5lZpožŠsuÅ$ؘWá՜»ôtÆ_T›ÅB-ÆF‚¸¸êsv5½n¢"’lëg|Cölü<Z]‹›¼F<ÿÛ|ýQŠ}¾dÉ3MÄM†—D_ѳŸ€S¶lþDz݋œÀV|ÿÌ»W¶÷D¢Z!sŠo¥ç¬bӘ_ͯ!Ua<ާËÚþè6= &ÓÙ}K¶C;ۀšà  Nh¹K8.•;™ üBSyLÆa8ô!JD„ï4˜±ñfR(€‹šHº<aŠ{.V«+n³ ®À‡zþýýøýèýðýÕë_—j'CVýhœw¢Âêyvø"…ÁžJ…úÉUÉVÊTÉ$ʤ@8ìÅçÅ'Åçí'¼9ÃùÃdêdê°–§YñµÿìFå”Ï­nèٍŠ= 2Xå„f„jd‰fpÑ*ªƒ'vMlQ•VØðrzæ[ò_øcè3«”¥ • •¢š› › åõÁ.èq²“x¥9½ÆQʓõzå%pT&IâÐYiŠh
ehäk‘%ÖMÖΕÍAPlFü9ðœÔì Ð„èŒ(̘ì>œøœxœN™Ž® †i«Þøñʓx¢,í~Ñr”ôyÕ°ócøë[ú“͕׍̙Ò=MÕ é%’/äo2ˆ€'?i4@¢€ÈSvÝö^Až>}Á>ÊA ?hõ©õš(­Þ"ð™²õy+ï(‘vñÆ1©Ê%5=M-E-f-Õðpdp6VŽ޵~ń˝‹xö¢y7Á»°ûC£=M|ÁKa۞á˜Â†×ÉÙRï·×ÜЄSÉz]”“usöõ¶³´6354B??A@@Æ7uÜ"Τ…!­ãã=}ҕ³öYÕ[ßPÑ[àXÙRgu{jC^ýÏ;[œó8d5ЛD3:9ä ÷ÐüWë?ç·iÉ»nEó>Ù»rF3>¼{k+R¾/OkDM¾<«¨*ªñ¨wñ¨ñ¨ñ¨6ñY¥wÒ_͇e$ŸÍˆi4O‡d ˆh0o=M‡f(¯=Mˆj8Gmc‡mgVîdÖî= –îhî^vîföîbîj6®[B®c®_‚®g®]b®eâ®a¢®i"®\R®dÒ®= ’®h®^r®fò®b²®j2.[J.cÊ._Š.g
.]j.eê.aª.i*.\Z.dÚ.= š.h.^z.fú.bº.j:n²é4îËg tf”•NƒkÜ¿N„oìÿ΃mäß΄qôŽƒlàώ„pðƒnèï„rø/nkÞÇno1‰õ҅AÌNôPt$ø§ස'wvþð7„&·n ’µRÓV¼F(Dšmõ‡£—§¡ilnuÒÕD–iÎdâi.E®ž’€émŽ1—hÎp!ÿÀ¦((uÔèrb)Gë²¼µE͎ë~Ȓp±ªÓØEò²L±m=Mف£÷É0”×*8aüªN8bºÎØ\Ëýo¢ávvþXÔ)­¢Éö®ËâÑ·pPåèƒÈ¥h®Ô0[Žæ/èò•x$ð|
ȵªÑ|ÆgŸ=Mi։T°傡¸í&ÒÛgŸu]lTʵ!íÚp¾ÈNn§ñkÞ襦n'ñr~~W‡²p³ŠîÙ)¦¢2"{êJ1ÜùvªÉ/Þ8"“:xTbu&Ÿ¢Ù%'n¬Ñö ¬"´)ù{HRæ p˜éñf³È•êŽ.H¥ú”Jò% Ixàò/³ú™Z.^µº‡AФÔwbÖRŸ/íç™ÐVí–Á*->Ô¹gŠ 7Àçrk‡I ì_œO9åBxÙsä¢nñ6íñ¨E¦¨ñ(™ñ¦¨È‰út7º½ªº¼æºBB »<?sQãK×SATÀûAÐ;ν—Q¥ 0›
=}tZ÷= [Õû¯û;³ÞêBb©<­¢«2>:ck~)—Cé<¬j
»·J×Z{ÒûDÑFW= ‹C‰w‡[
M?òÐ0Ò@!‚|i¤q–#ˆg'ÞgÐQ.QŠFnvÿõ\÷,K9è\‰7g¿çIéXœޜ­:ÀDõ]Ã^]J€ËC ƒO¸…O.‰Ïì4q |qòtìaÁb‹îÓÞÊ1=  Òbëþ„9„÷JÍÃpŽÓBŽÇ臘ØÚÌÿFmg)@홼ôõ½…½ÂeL¦’œºÀwßUb”¾ùìAîhžzהÂ%è"ÿª%Ì"pKç\Œ= Ä´›Aô~¢± }:zF6Ž"¨xR sJ^!n1,·÷ÂQÒij/]ÙÆ¬r˜½zõOîǗ­½Ðf,fì–VÌøˆsæpÞr¦ü'Ó©þôÝX °Ï6²]n?>xã>µl0Ì6÷Z²)[µ\'ÁÌtBªXÀóµ?ŽS §,÷p+Ò萺¨Ÿ·˜ÞþjéÊÔ˗£ž½nq|G6ùÌV§Ä.‘#îòü/ª’º¶×²¨ìnb'Å7›r½æ1{XóR¡ŸZ­n2þ*ÊM«î@,C3l‹TÃü.ûªAܛ4‘KzW/€ÛÚÁ3ºë÷Ž[a@9&½Ö–h’”ãô\7v~ÿrH®®]½5[½¹w-§*K$?æ%uFóì_G2ÎÁÛÔ‡E×VkÓÔ(pŽsÔH‚Iby6œaÀÔHEnnnnî
ÕÕWš’¸0922hPå“c‚A3åWã܅›·G»ñm=}ñ¾D=}n “;˜¡HsQkBs>5#AÕɻҳ;®þBÓÇi»äKÛý¹BïÝb«”ûIǨÔG/ýgIQ;ÉY;'*•ÛRÇ>á³e‹fæ<â<o¸T[:H»®²> ¬CÓ¿®{vAñ7@L»Î…LX§ÄëLÀú@[©ŠZ¿!ß\®;ý‡t¥UKŽïUœTÂã ÒK6M¬²?¶Éû6>CõuÉýBMó轛’è]#¿^?^¹}‹6lĦu̓V|ÝL¾„wúÌßÙṙ >ÍñŒE“¤R<IL«Öæ?/ãa<Ù4—ûUãÀîC\-gR2òDì%ÀË)g)âÆ+÷Ô]%,´Luœ_!ÿ'3•l ócá•„ó©M× …„#ZU›uº±ÑÝ.àã€dÙ”øÙgž…*£œÓ×ö{É]@²wŠ|²séûb¸Ð§&=}= bã  e%+†PI
l›5˜p›‰U–âGäÄL#+d^©®çlV°SêlÜ2øm‘Êp= A¡ç¯ÔT˜R§ S™mâeÁ•›àä= •ïþM€¡•œ‚§ˆtªî¡m.¨#ÖÑ¥±¤pypÑÆ* ‚21³D±œø>¢Ô(Ãu³uŒD¢W¯ÔÄ'ºêÐ;eV;~Z»;€= »1A{{s½×ÂC%rN›ÌÛQý%†]ó|›KªÑüÌڅ+¤n½BâüޅH™T +hæNç¨1¾€œË7–sËVŽüê¦d¿õÉ>õÜI5ٖ>žWI¾ÌÂ+ÿGÞ®äY£=MƟO$uH€"pÜtŠc·Ázìø2£œ„áH´œØÑ/ó¡Ò/Ow¶ö‚Û‰FQ1'µ‹Ã6áÅÁ·RôOë<²­_¾µ°ý|”ß/öb=M†R©¡µg*5ыI6#ÜU²¸Û<n2ÛGºªሖ‡EW]ãt¿Àl¸-{+‰&å¡°ð3éð³¢ú¹Ù'7äö¨"÷ˆøýìØ‡èAµg#wL°ê7=}=M˜‰BФC&hé#ñ4ᙴ8 &2Éósnœæï î%í¢†-귐mœ|–h*¤–WåØ;AÀ>
@3Å[ЉûNhDîQFü*Ý?uJûØ6ë
A™§²Âœp˜[ô’s‹šFû .oCtï@ݘÚý‰# £=MÖ#ƒ‡wìîxýß½GÒÿDó|Ü}5jìÝNȰ5EÇ>ïvßøN’À‚ªfÈÆHøýˆ §åpª­( øç°ÓX–*)· ©T0œ¢Ç‚¹ÆÑB¿Ÿ¤iÅ5mtímhâ¡'»BNiÇ©¦=}{fLÐË+¡ùC}ØûÀÓ}qb³.x;ݎ<¸Ø<圢›‹ÆrEØø>¬C䇭‹ ·|ãKLQfäÕ6˜5av„TõÝýît›™Ð"¦6Ál¬¨6ñý>HG8  $  —=MøÄƒd»¥BGÁØÇ¼¹$=}¯ŸŠã)uYGz;žC}pd¿\|‘6(L=Mf_ï#ß«WÞ¥§fŸp@0N6‘
Š'¤sªˆQø÷Õ¬Åq¾>.øü·¢D5³T…áhÅ8zDvO= ¯óÝyàÙåFÆÒ•çcËóŒz?Ê5cèŠ–úë.ÌðÐޛ~†O Ë|¿Ó˜Q§ v)BHXƒš6œøöDއ¥„˜ŠkÒÚa¡cÙ7—¦x©nº¡6¾}®­iÏ<ÎWhš‘2 9's"IɎ<»ïX\#F\Ë_4F[ eŠÚ ¸Šø¹2øH$žß9_xì9i 2µšã
ã/¸Äyf"Ÿ5TàyÊ0îEê²Þ—€m6ëÞè6ðÏâ§9ž¬„k=}¼àt<ž‚¦Mdžç6"p_î3, oO3p"pw÷´/(1ñøs.íA/&ÏUÃ_X™ÍpÕ1hÌ80E’Qö„
ow÷!x.™AÜ"¢j+惡¦®ä“R–ow÷!xꔊpn…â#ðù
z AÍp\Ž0N˜HJê´ªØ^U
I€wu¸öôú!3å6³‹âVq£³¹´RN£%y10º:pŠ#\:J:b9Hµ7h_Ž:¢I9 _¼™µV,þ <`});var d={a:l};this.H=t=>{i.H(n,t)},this.M=()=>i.M(n),this.instantiate=()=>(this.M().then(i=>WebAssembly.instantiate(i,d)).then(i=>{const s=i.exports;var h;(i=>{o=i.i,a=i.j,c=i.k,I=i.l,u=i.m,y=i.o})(s),h=(H=s.g).buffer,e=new Uint8Array(h),new BigInt64Array(h),new BigUint64Array(h),(i=>{i.h()})(s),t()}),this.ready=new Promise(i=>{t=i}).then(()=>{this.O=H.buffer,this.v=a,this.free=u,this.R=o,this._=c,this.q=I}),this)}function M(i={}){M.errors||Object.defineProperties(M,{errors:{value:new Map([[-1,"OPUS_BAD_ARG: One or more invalid/out of range arguments"],[-2,"OPUS_BUFFER_TOO_SMALL: Not enough bytes allocated in the buffer"],[-3,"OPUS_INTERNAL_ERROR: An internal error was detected"],[-4,"OPUS_INVALID_PACKET: The compressed data passed is corrupted"],[-5,"OPUS_UNIMPLEMENTED: Invalid/unsupported request number"],[-6,"OPUS_INVALID_STATE: An encoder or decoder structure is invalid or already freed"],[-7,"OPUS_ALLOC_FAIL: Memory allocation has failed"]])}}),this.K=()=>new this.C(this).instantiate(this.ii,this.ti).then(i=>{this.ei=i,this.Hi=0,this.si=0,this.hi=0,this.ri=this.ei.U(this.ni,Uint8Array),this.Mi=this.ei.U(this.oi*this.ai,Float32Array);const t=this.ei.U(this.ci,Uint8Array);t.F.set(this.Ii),this.ui=this.ei.I.R(this.yi,this.ci,this.li,this.di,t.Y,this.pi,this.mi)}),Object.defineProperty(this,"ready",{enumerable:!0,get:()=>this.fi}),this.reset=()=>(this.free(),this.K()),this.free=()=>{this.ei.free(),this.ei.I.q(this.ui),this.ei.I.free(this.ui)},this.Ji=i=>{if(!(i instanceof Uint8Array))throw Error("Data to decode must be Uint8Array. Instead got "+typeof i);this.ri.F.set(i);let t,e=this.ei.I._(this.ui,this.ri.Y,i.length,this.Mi.Y);return e<0&&(t="libopus "+e+" "+(M.errors.get(e)||"Unknown Error"),console.error(t),e=0),{outputBuffer:this.ei.P(this.Mi.F,this.oi,e),samplesDecoded:e,error:t}},this.decodeFrame=i=>{let t=[];const e=this.Ji(i);return e.error&&this.ei.D(t,e.error,i.length,this.hi,this.Hi,this.si),this.hi++,this.Hi+=i.length,this.si+=e.samplesDecoded,this.C.W(t,[e.outputBuffer],this.oi,e.samplesDecoded,this.yi)},this.decodeFrames=i=>{let t=[],e=[],H=0,s=0;for(;s<i.length;){const h=i[s++],r=this.Ji(h);t.push(r.outputBuffer),H+=r.samplesDecoded,r.error&&this.ei.D(e,r.error,h.length,this.hi,this.Hi,this.si),this.hi++,this.Hi+=h.length,this.si+=r.samplesDecoded}return this.C.W(e,t,this.oi,H,this.yi)},this.bi=M.G,this.C=M.V||s,this.ii=M.$||n,this.ti=M.module;const t=i=>"number"==typeof i,e=i.sampleRate,H=i.channels,h=i.streamCount,r=i.coupledStreamCount,o=i.channelMappingTable,a=i.preSkip,c=i.forceStereo?1:0;if(H>2&&(!t(h)||!t(r)||!Array.isArray(o)))throw Error("Invalid Opus Decoder Options for multichannel decoding.");return this.yi=[8e3,12e3,16e3,24e3,48e3].includes(e)?e:48e3,this.ci=t(H)?H:2,this.li=t(h)?h:1,this.di=t(r)?r:this.ci-1,this.Ii=o||(2===this.ci?[0,1]:[0]),this.pi=a||0,this.mi=H<=8&&2!=H?c:0,this.ni=3840*this.ci,this.ai=5760,this.oi=this.mi?2:this.ci,this.fi=this.K(),this}class o extends h{constructor(i){super(i,"opus-decoder",M,n)}async decodeFrame(i){return this.X("decodeFrame",i)}async decodeFrames(i){return this.X("decodeFrames",i)}}r(M,"OpusDecoder"),r(o,"OpusDecoderWebWorker"),i.OpusDecoder=M,i.OpusDecoderWebWorker=o},"object"==typeof exports&&"undefined"!=typeof module?t(exports,require("@eshaz/web-worker")):"function"==typeof define&&define.amd?define(["exports","@eshaz/web-worker"],t):t((i="undefined"!=typeof globalThis?globalThis:i||self)["opus-decoder"]={},i.Worker);
//# sourceMappingURL=opus-decoder.min.js.map
+3
View File
@@ -21,3 +21,6 @@ var xtermCSS []byte
//go:embed assets/static/fit.min.js //go:embed assets/static/fit.min.js
var xtermFitJS []byte var xtermFitJS []byte
//go:embed assets/static/opus-decoder.min.js
var opusDecoderJS []byte
+51 -7
View File
@@ -1,6 +1,7 @@
package web package web
import ( import (
"bytes"
"context" "context"
"encoding/json" "encoding/json"
"errors" "errors"
@@ -30,6 +31,7 @@ type Server struct {
loginIPLimit *wsauth.RateLimiter loginIPLimit *wsauth.RateLimiter
loginUserLimit *wsauth.RateLimiter loginUserLimit *wsauth.RateLimiter
trustForwardedFor bool // honor X-Forwarded-For (behind trusted proxy only) trustForwardedFor bool // honor X-Forwarded-For (behind trusted proxy only)
basePath string // web UI mount prefix (e.g. "/remoter"); "" = domain root
} }
// Config tunes the server's exposed-on-public-HTTPS hardening knobs. // Config tunes the server's exposed-on-public-HTTPS hardening knobs.
@@ -52,6 +54,12 @@ type Config struct {
// exposure the header is client-controlled and would let attackers // exposure the header is client-controlled and would let attackers
// evade per-IP rate limits. // evade per-IP rate limits.
TrustForwardedFor bool TrustForwardedFor bool
// BasePath mounts the whole web UI (index, manifest, static assets,
// /health, /api/devices, /ws) under a URL prefix, so a reverse proxy
// can serve it from a subpath of an existing domain (e.g.
// https://example.com/remoter). Must be empty or a leading-slash,
// no-trailing-slash path; "" (default) serves from the domain root.
BasePath string
} }
// New creates an HTTP server bound to the given port. port=0 disables the server. // New creates an HTTP server bound to the given port. port=0 disables the server.
@@ -68,6 +76,7 @@ func (s *Server) WithConfig(cfg Config) *Server {
s.loginIPLimit = cfg.LoginIPLimit s.loginIPLimit = cfg.LoginIPLimit
s.loginUserLimit = cfg.LoginUserLimit s.loginUserLimit = cfg.LoginUserLimit
s.trustForwardedFor = cfg.TrustForwardedFor s.trustForwardedFor = cfg.TrustForwardedFor
s.basePath = cfg.BasePath
return s return s
} }
@@ -93,10 +102,11 @@ func (s *Server) Start() error {
mux.HandleFunc("/static/xterm.js", staticHandler(xtermJS, "application/javascript; charset=utf-8")) mux.HandleFunc("/static/xterm.js", staticHandler(xtermJS, "application/javascript; charset=utf-8"))
mux.HandleFunc("/static/xterm.css", staticHandler(xtermCSS, "text/css; charset=utf-8")) mux.HandleFunc("/static/xterm.css", staticHandler(xtermCSS, "text/css; charset=utf-8"))
mux.HandleFunc("/static/xterm-fit.js", staticHandler(xtermFitJS, "application/javascript; charset=utf-8")) mux.HandleFunc("/static/xterm-fit.js", staticHandler(xtermFitJS, "application/javascript; charset=utf-8"))
mux.HandleFunc("/static/opus-decoder.min.js", staticHandler(opusDecoderJS, "application/javascript; charset=utf-8"))
s.srv = &http.Server{ s.srv = &http.Server{
Addr: ":" + strconv.Itoa(s.port), Addr: ":" + strconv.Itoa(s.port),
Handler: mux, Handler: s.mount(mux),
ReadHeaderTimeout: 10 * time.Second, ReadHeaderTimeout: 10 * time.Second,
} }
@@ -129,14 +139,41 @@ func (s *Server) Stop() {
_ = s.srv.Shutdown(ctx) _ = s.srv.Shutdown(ctx)
} }
// basePathPlaceholder is the token in index.html substituted with the
// configured base path at serve time. Empty base path → "" → root URLs.
const basePathPlaceholder = "__BASE_PATH__"
// mount wraps the root-path mux with the configured base path. When basePath
// is empty the mux is returned unchanged (domain-root serving, 方案A). Otherwise
// the bare prefix redirects to prefix+"/", paths outside the prefix 404, and
// matching paths are stripped before reaching the mux (方案B, subpath serving).
func (s *Server) mount(h http.Handler) http.Handler {
if s.basePath == "" {
return h
}
base := s.basePath // normalized by cmd: leading "/", no trailing "/"
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == base {
http.Redirect(w, r, base+"/", http.StatusMovedPermanently)
return
}
if !strings.HasPrefix(r.URL.Path, base+"/") {
http.NotFound(w, r)
return
}
http.StripPrefix(base, h).ServeHTTP(w, r)
})
}
func (s *Server) handleIndex(w http.ResponseWriter, r *http.Request) { func (s *Server) handleIndex(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/" && r.URL.Path != "/index.html" { if r.URL.Path != "/" && r.URL.Path != "/index.html" {
http.NotFound(w, r) http.NotFound(w, r)
return return
} }
body := bytes.ReplaceAll(IndexHTML, []byte(basePathPlaceholder), []byte(s.basePath))
w.Header().Set("Content-Type", "text/html; charset=utf-8") w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.Header().Set("Cache-Control", "no-cache") w.Header().Set("Cache-Control", "no-cache")
_, _ = w.Write(IndexHTML) _, _ = w.Write(body)
} }
func (s *Server) handleHealth(w http.ResponseWriter, r *http.Request) { func (s *Server) handleHealth(w http.ResponseWriter, r *http.Request) {
@@ -190,21 +227,28 @@ func (s *Server) unauthorized(w http.ResponseWriter) {
} }
// PWA manifest. Referenced by <link rel="manifest"> in index.html. // PWA manifest. Referenced by <link rel="manifest"> in index.html.
// Static JSON, no template needed. // start_url is rooted at the configured base path so an installed PWA
const manifestJSON = `{ // launches into the right location; "/" when served from the domain root.
func (s *Server) manifestJSON() []byte {
start := "/"
if s.basePath != "" {
start = s.basePath + "/"
}
return []byte(fmt.Sprintf(`{
"name": "SimpleRemoter", "name": "SimpleRemoter",
"short_name": "Remoter", "short_name": "Remoter",
"start_url": "/", "start_url": "%s",
"display": "standalone", "display": "standalone",
"orientation": "any", "orientation": "any",
"background_color": "#1a1a2e", "background_color": "#1a1a2e",
"theme_color": "#1a1a2e" "theme_color": "#1a1a2e"
}` }`, start))
}
func (s *Server) handleManifest(w http.ResponseWriter, r *http.Request) { func (s *Server) handleManifest(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/manifest+json") w.Header().Set("Content-Type", "application/manifest+json")
w.Header().Set("Cache-Control", "public, max-age=86400") w.Header().Set("Cache-Control", "public, max-age=86400")
_, _ = w.Write([]byte(manifestJSON)) _, _ = w.Write(s.manifestJSON())
} }
// staticHandler returns an http.HandlerFunc that serves a fixed byte slice // staticHandler returns an http.HandlerFunc that serves a fixed byte slice
+185 -18
View File
@@ -10,9 +10,9 @@
<meta name="apple-mobile-web-app-title" content="Remoter"> <meta name="apple-mobile-web-app-title" content="Remoter">
<meta name="mobile-web-app-capable" content="yes"> <meta name="mobile-web-app-capable" content="yes">
<meta name="theme-color" content="#1a1a2e"> <meta name="theme-color" content="#1a1a2e">
<link rel="manifest" href="/manifest.json"> <link rel="manifest" href="__BASE_PATH__/manifest.json">
<!-- xterm.js (远程终端). Files served by WebService from RC binary resources. --> <!-- xterm.js (远程终端). Files served by WebService from RC binary resources. -->
<link rel="stylesheet" href="/static/xterm.css"> <link rel="stylesheet" href="__BASE_PATH__/static/xterm.css">
<link rel="icon" type="image/png" href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAA0MSURBVFhHNZfnV1TnFof5G7xeoyBKG6p0ELBSRMFoVGyRgBqMscVYEq+aoldIjCVivxq7JsYOYkOadUDq9MZ0GDpDr2qynrvmYD781nvWOh/2s/dv7/2e4/Tu3TtaW9vp6Oihp2eAvr5B+nuHGOhxqJ/e7i76urvp7eqiu6ODzhY7rXVNNFptdLV20GZrwaw10Giqx6o3Y601Y1TXopWrqVVo0EiV1Cq1KKqkSGQ1lFe+QSKXoNGp6R/sw6mttYPc3wt4eu8VWokRk9qKVWOhXluHUW7AIKlFVa5AW6lGVSqj/NEznt1+zIm9h7h3/gYFNx5y8eApdq/dSvbunzm8fQ8Htu9hW9p6vly8kq/SviR9/qfMj01mduwskhPmkJQwmzWfZ2DQ63Hqsvfw+FYJl49cp6ygEr3UhElpQi/Vo6vWoqtQoX2jQFpSjvx5FdX5Yoqv5XJ02z52pn/F9SPnuZh5nM3z0tm6+HO+TV3Pl0nLSJ05n0Uxs5nmF0FCyFRC3PzxHueO25gJjB01ltlxiShkcpy67b1UPpfy6PdCnt9/ja7agFlZh0lhxiAzYKjRYqjWohHLkL+oRl5SQXlOEfeOX+H7zzZxdHsm17JOkr3pR9JjF5Aev5CMhPmkTU9mXngs0/zCiPYKZrJHIKET/AhyEeE+2pkFickoHQD9PYNISxWUF0l49fANilI1BqkZs2IEwijRYazWCqotV6J+KaEy7xn5F+9wZsd+9qVu5syOLH7bmcWOpRksDI/j84RPWBoZR3LgVBIDowh19SHCLYBQV18BZNI4N5JmxKFRqnAa7B9CJ6+l5rWCktxSiu8+Q1WqweiAUJowyfSYarSYanQYqrRoXlVRlfuE/LNXyT3yGyc27uLIF1s4+PlGMj9by6fRs1iTsICMGcksmTyT2YERTPcJIeAjd4LGezPdO4gpoknMm5mARqHEabBvELPWgqZaz4s8MbkX7iN+VIbmTS31KjN1ajP6ajXalzWonouRPS5B8rCQN7fyeHzqIn/8cIAjazbzS/o6MlPXsiZ+ActjZrEhaRFrZiaT4BNMYtBkotz98RszkWjPAAEgZVYyKocFvZ29GJQGLKo6pC8U5F16QNHNYiqevkH3Ro2xSob2RRm1r6qwVCowVirQlStQiWsQ33lE7q+nufr9T/yyeiOZK9ezLimFOYFRrJo1n4yEj1kSMY3EgHBiAyLwGe1KoKtIAFg1b/GIBcN9g9S8rhI6X1dZS9GtEopuFPLy5iPEtx6hL5VhkeqokxuxqkyYFQZUYgk1RWWIbz8gL/s01/bs5/iG7fxnwaesnp7I3JBokoLCWRY9g0VhU5jlF0K0px/+H7nh8S9nwtzcSU2ah1atwWloYAiT0oC0VIa+xkBNSTVPL92lLKcYWXE5unLlSANWqFCXSdGWK5CVlFN29wHPrt3i3uFjnNv8LccyvmRH0idsiJ9L2tR4ZvsGE+8dQJJ/CDM8fIgc706QsxuiUWMJGOfGkoRkNKoPTdhktAnLxwHgCFp6t5BXtwuQFpShEUvRV6qFXaAtk6MplVJdWErR5dvkHDvLjT2HOLJ6E3sWLWNb4jwyps1iWeQM4n2DiHL1ZJqHL1ETvAj+aDwBY1zxGjUW91FjmReXiEqmwKm/d4AmUwONBhsWmRFFUTmywje8uP6QivvPUD2vpqZQzOvcQp79mUfRpZvknbjAxZ1ZHF+7lUNp69k2awGbYz9m44wkloRPISlwMjO9Q4ic6EvERB8i3XwJcvZANMYV0b9dEP17HHPjEkemYKB3QMi+QV+PRaZHViBG8lRMaU4hD87+Se7xc9zOOsKZLd/x6xdb2btkNbs+Xs6mhIWsio5lWUgMSb7hzPYNIdE3mDjHmPmEEiEKJcjVVxi9YHd/Ahyb0NkLXxcv3EY7Mzd+NgqpDKe+7j708lpsOit1Ch3yAjHl94vJP/sHR9bsYE/q1xzadoTv1/3Mnh/Pkrbia5YtXk9UWBJezgF4TwjE08Uf17GejBvlgttH7ni7ByFyCcDPLRg/t0kEi4KZ5BmEl4sINxdPPFw8iZsai0quHLFAL9FiU5mpV+iQFryi8NRlzn1zgN1LNrH7iz2cPVfI/ccyjp4rYOGSbUTFzCcyLBlfjxBBE5x9Ge8iYvzYiYwd5YKvR/DIO/cgQvyiCfIOJdgnjEBRCG4fIBJnzEJaLcFpoKsPY41GkLlKRuWt+9zZc4D9GzLZvmATx/Zd4sGjKuT6Tg6fLWTOnA3Mjl9B7LTFhPjFIJoYiPv4ANxdfPFx9cF19Fg8XdwIEoUIAf09ggkPmEzEpChC/SLwc5+Em7Mnc+KS0Kq0OA1292OQqKmtUWGsqOb5oSzOrtvO1/M38u2izfxx8h5yZT11tj4OHc0jac5XfJayhaQZS5gSlsxHo90ZN06Eq6sPARNE+DlPxG/cBEI9vAny8CNIFEpUyBRiQqYS6h+J38RJeDl7kTI3BblE/gFAqsYs12GWyKi+fJLsz9azbsYKflyyjdzT99DrG2hr7+XK+fssX7iDVQs3snL+KuInf8K4cb6MGeOBs4sv4R5+hLl5EOY6kZk+IqLdvYjw9CM5KoG44CnE+IQT6RFMXFAMKxelotfocRruHsAs01KnMtBiqMMgFvNw/3F+W7ebk+k7uLP7BIrXcuwd3ahlBtYt3cLKxKWsT0rh44AwJrl64zLaFR+3AOIDI5nm7cdMby9SIoKI8xURI/JnUdR0koOmkOg/mZToRJZOSWJL6hdoVRqcBnscY2igQWvGbmmiUWdFIa5CWyGl8tZD7mzZx9PMC1g1Zpqb7Fw/+wcrExayemoC6VHTWBQYSpynF9NFImZ6BzDLN4DF4UF8PjWcEGdn4v1DmBsUybKoeNYmprAhaSlpsUlsTVuFxWRwbMJhbHoLjQYrHfUtdNpaaTHZ6Ghup7PJjuFVGcWZJym7lkdttQqDysLJvYdZGTuP9JiZrI2ewsJJwXzsP4klkeGsiA4nfepkprpNJMbDi7TImWyMn8+ulJXsmL+CrUmL2J6SSvbO77AYDTgNDwzTaKynxdJAZ2MbvS0d9DR3CAC99m562zqpr5JSdv5P5PmlaEsVaKr03D13l00pGSyPmcGi0EiWTY5ieXQ0KeFhxEzwYH5oDD+kruPYpu/JXJ7B3qVpHMzYwPFtuzi5/TuuHsqmucE2chc0m220mEcAuu1dghxfvB0tdrrbu+i1d9GqNiLPKUCWX4FFWYdF24jklZpbF5+wf/cpvlr5DVvStrJ30w+cyzrNjV8vc2nXT2SvWsvpjZu5/ONP3Dh8jOuHjnIt82fuHT9FY50Vp8GBYdqsjQKE3dZCrxCwm562LuzN7SNWtNjpaemgo64JS2kV8vslVN/JR/2imnpDM7VKK8rqWqqe1fD89lNyDp7i+s7/8vuPB7h7+ARF127w4mYOhdducu/oKa5l7ePmkWzaHBUYGhim1dpIo7GOjoZW+uzd9HX0COps7RAg2hta6LC1CupubKfV3IClRonk9iNenDhP8ekrPDlwmrx92Tw+eJLC/12l+M+HvMwp5OW9J4hzn/DyZg75F65x+5dfuLRrF7eys7GZTDgN9Q8L/jebbLTbWoTMBe8/6B8AR3Ucaq9vpsvWSlt988jPiNqAWa5FV6VAXSFDUylHUyFH/kZKeeFrXt64R/G58zw5cYq7mVlc3LaF0+vWc+Gb/9BoseL0buCtANBkqhcq0flP832oRFeLHXtDqxC4ra6JJmMdDQYrtlozNo2JBp2Zeo0Jk8SxztXoq5QoX1UgeVKC+OoNCg8eIm/vHu7s/o7L6zZwdNly9i9M4cLWndSbjCMADXqrsIgaay1CZt1tnUJwB4SjCR090PkBotVkE+yyaR2XVy0WqRZTeQ3Gl2VoCl+gyHlC5fW7lJ65SH7Wz9zZ9i1X1qzlbOoqji/+lGMrVnEsPYOrP+yjvbERp+H+YZp0ZqxVKmxqI23WJqEKjuZzqKe9S1Bns12woM3cIAA06a006izUK/WYqhTUvihH/rCIyus5vD5zheKD2Tz8IYs7jpH7cjO/fbaak5+u5MKm7Vz59jseHz9Da0MDTkN9g9gUtRjfyIWMHCV2VKG1rlEouSO4UIm2TuwO761NgveNeitN+jqadBZsKiOmGhW14irk+c+peVBExc3HiC/d5tnpqzw6fJKHvxzl7n9/Jm9/No+O/o+Xf9yhu90+sogcmVtrNDRpzLQY67HXNdPVbKfHYUVnD32dvfR39tLb1kW3Yz80tNJR10K7tUmQYyqajfU0aIyYa5RYpGoMlUrUpRIUz0tRloiRFbyi5kEhsvxiJI9LUIsr6O/uwenv938z3DvIUM+AcL4bGOb9wFveD73j/eBb/h7+i7/f/sXfw++F57+G3/P+7XveDr4VNDw4zNuBf56HGB4YZKhvQNBgbz9Dff0MdPUKwQR1fTi7e/nr/V/8HzLpSvkUrIc+AAAAAElFTkSuQmCC"> <link rel="icon" type="image/png" href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAA0MSURBVFhHNZfnV1TnFof5G7xeoyBKG6p0ELBSRMFoVGyRgBqMscVYEq+aoldIjCVivxq7JsYOYkOadUDq9MZ0GDpDr2qynrvmYD781nvWOh/2s/dv7/2e4/Tu3TtaW9vp6Oihp2eAvr5B+nuHGOhxqJ/e7i76urvp7eqiu6ODzhY7rXVNNFptdLV20GZrwaw10Giqx6o3Y601Y1TXopWrqVVo0EiV1Cq1KKqkSGQ1lFe+QSKXoNGp6R/sw6mttYPc3wt4eu8VWokRk9qKVWOhXluHUW7AIKlFVa5AW6lGVSqj/NEznt1+zIm9h7h3/gYFNx5y8eApdq/dSvbunzm8fQ8Htu9hW9p6vly8kq/SviR9/qfMj01mduwskhPmkJQwmzWfZ2DQ63Hqsvfw+FYJl49cp6ygEr3UhElpQi/Vo6vWoqtQoX2jQFpSjvx5FdX5Yoqv5XJ02z52pn/F9SPnuZh5nM3z0tm6+HO+TV3Pl0nLSJ05n0Uxs5nmF0FCyFRC3PzxHueO25gJjB01ltlxiShkcpy67b1UPpfy6PdCnt9/ja7agFlZh0lhxiAzYKjRYqjWohHLkL+oRl5SQXlOEfeOX+H7zzZxdHsm17JOkr3pR9JjF5Aev5CMhPmkTU9mXngs0/zCiPYKZrJHIKET/AhyEeE+2pkFickoHQD9PYNISxWUF0l49fANilI1BqkZs2IEwijRYazWCqotV6J+KaEy7xn5F+9wZsd+9qVu5syOLH7bmcWOpRksDI/j84RPWBoZR3LgVBIDowh19SHCLYBQV18BZNI4N5JmxKFRqnAa7B9CJ6+l5rWCktxSiu8+Q1WqweiAUJowyfSYarSYanQYqrRoXlVRlfuE/LNXyT3yGyc27uLIF1s4+PlGMj9by6fRs1iTsICMGcksmTyT2YERTPcJIeAjd4LGezPdO4gpoknMm5mARqHEabBvELPWgqZaz4s8MbkX7iN+VIbmTS31KjN1ajP6ajXalzWonouRPS5B8rCQN7fyeHzqIn/8cIAjazbzS/o6MlPXsiZ+ActjZrEhaRFrZiaT4BNMYtBkotz98RszkWjPAAEgZVYyKocFvZ29GJQGLKo6pC8U5F16QNHNYiqevkH3Ro2xSob2RRm1r6qwVCowVirQlStQiWsQ33lE7q+nufr9T/yyeiOZK9ezLimFOYFRrJo1n4yEj1kSMY3EgHBiAyLwGe1KoKtIAFg1b/GIBcN9g9S8rhI6X1dZS9GtEopuFPLy5iPEtx6hL5VhkeqokxuxqkyYFQZUYgk1RWWIbz8gL/s01/bs5/iG7fxnwaesnp7I3JBokoLCWRY9g0VhU5jlF0K0px/+H7nh8S9nwtzcSU2ah1atwWloYAiT0oC0VIa+xkBNSTVPL92lLKcYWXE5unLlSANWqFCXSdGWK5CVlFN29wHPrt3i3uFjnNv8LccyvmRH0idsiJ9L2tR4ZvsGE+8dQJJ/CDM8fIgc706QsxuiUWMJGOfGkoRkNKoPTdhktAnLxwHgCFp6t5BXtwuQFpShEUvRV6qFXaAtk6MplVJdWErR5dvkHDvLjT2HOLJ6E3sWLWNb4jwyps1iWeQM4n2DiHL1ZJqHL1ETvAj+aDwBY1zxGjUW91FjmReXiEqmwKm/d4AmUwONBhsWmRFFUTmywje8uP6QivvPUD2vpqZQzOvcQp79mUfRpZvknbjAxZ1ZHF+7lUNp69k2awGbYz9m44wkloRPISlwMjO9Q4ic6EvERB8i3XwJcvZANMYV0b9dEP17HHPjEkemYKB3QMi+QV+PRaZHViBG8lRMaU4hD87+Se7xc9zOOsKZLd/x6xdb2btkNbs+Xs6mhIWsio5lWUgMSb7hzPYNIdE3mDjHmPmEEiEKJcjVVxi9YHd/Ahyb0NkLXxcv3EY7Mzd+NgqpDKe+7j708lpsOit1Ch3yAjHl94vJP/sHR9bsYE/q1xzadoTv1/3Mnh/Pkrbia5YtXk9UWBJezgF4TwjE08Uf17GejBvlgttH7ni7ByFyCcDPLRg/t0kEi4KZ5BmEl4sINxdPPFw8iZsai0quHLFAL9FiU5mpV+iQFryi8NRlzn1zgN1LNrH7iz2cPVfI/ccyjp4rYOGSbUTFzCcyLBlfjxBBE5x9Ge8iYvzYiYwd5YKvR/DIO/cgQvyiCfIOJdgnjEBRCG4fIBJnzEJaLcFpoKsPY41GkLlKRuWt+9zZc4D9GzLZvmATx/Zd4sGjKuT6Tg6fLWTOnA3Mjl9B7LTFhPjFIJoYiPv4ANxdfPFx9cF19Fg8XdwIEoUIAf09ggkPmEzEpChC/SLwc5+Em7Mnc+KS0Kq0OA1292OQqKmtUWGsqOb5oSzOrtvO1/M38u2izfxx8h5yZT11tj4OHc0jac5XfJayhaQZS5gSlsxHo90ZN06Eq6sPARNE+DlPxG/cBEI9vAny8CNIFEpUyBRiQqYS6h+J38RJeDl7kTI3BblE/gFAqsYs12GWyKi+fJLsz9azbsYKflyyjdzT99DrG2hr7+XK+fssX7iDVQs3snL+KuInf8K4cb6MGeOBs4sv4R5+hLl5EOY6kZk+IqLdvYjw9CM5KoG44CnE+IQT6RFMXFAMKxelotfocRruHsAs01KnMtBiqMMgFvNw/3F+W7ebk+k7uLP7BIrXcuwd3ahlBtYt3cLKxKWsT0rh44AwJrl64zLaFR+3AOIDI5nm7cdMby9SIoKI8xURI/JnUdR0koOmkOg/mZToRJZOSWJL6hdoVRqcBnscY2igQWvGbmmiUWdFIa5CWyGl8tZD7mzZx9PMC1g1Zpqb7Fw/+wcrExayemoC6VHTWBQYSpynF9NFImZ6BzDLN4DF4UF8PjWcEGdn4v1DmBsUybKoeNYmprAhaSlpsUlsTVuFxWRwbMJhbHoLjQYrHfUtdNpaaTHZ6Ghup7PJjuFVGcWZJym7lkdttQqDysLJvYdZGTuP9JiZrI2ewsJJwXzsP4klkeGsiA4nfepkprpNJMbDi7TImWyMn8+ulJXsmL+CrUmL2J6SSvbO77AYDTgNDwzTaKynxdJAZ2MbvS0d9DR3CAC99m562zqpr5JSdv5P5PmlaEsVaKr03D13l00pGSyPmcGi0EiWTY5ieXQ0KeFhxEzwYH5oDD+kruPYpu/JXJ7B3qVpHMzYwPFtuzi5/TuuHsqmucE2chc0m220mEcAuu1dghxfvB0tdrrbu+i1d9GqNiLPKUCWX4FFWYdF24jklZpbF5+wf/cpvlr5DVvStrJ30w+cyzrNjV8vc2nXT2SvWsvpjZu5/ONP3Dh8jOuHjnIt82fuHT9FY50Vp8GBYdqsjQKE3dZCrxCwm562LuzN7SNWtNjpaemgo64JS2kV8vslVN/JR/2imnpDM7VKK8rqWqqe1fD89lNyDp7i+s7/8vuPB7h7+ARF127w4mYOhdducu/oKa5l7ePmkWzaHBUYGhim1dpIo7GOjoZW+uzd9HX0COps7RAg2hta6LC1CupubKfV3IClRonk9iNenDhP8ekrPDlwmrx92Tw+eJLC/12l+M+HvMwp5OW9J4hzn/DyZg75F65x+5dfuLRrF7eys7GZTDgN9Q8L/jebbLTbWoTMBe8/6B8AR3Ucaq9vpsvWSlt988jPiNqAWa5FV6VAXSFDUylHUyFH/kZKeeFrXt64R/G58zw5cYq7mVlc3LaF0+vWc+Gb/9BoseL0buCtANBkqhcq0flP832oRFeLHXtDqxC4ra6JJmMdDQYrtlozNo2JBp2Zeo0Jk8SxztXoq5QoX1UgeVKC+OoNCg8eIm/vHu7s/o7L6zZwdNly9i9M4cLWndSbjCMADXqrsIgaay1CZt1tnUJwB4SjCR090PkBotVkE+yyaR2XVy0WqRZTeQ3Gl2VoCl+gyHlC5fW7lJ65SH7Wz9zZ9i1X1qzlbOoqji/+lGMrVnEsPYOrP+yjvbERp+H+YZp0ZqxVKmxqI23WJqEKjuZzqKe9S1Bns12woM3cIAA06a006izUK/WYqhTUvihH/rCIyus5vD5zheKD2Tz8IYs7jpH7cjO/fbaak5+u5MKm7Vz59jseHz9Da0MDTkN9g9gUtRjfyIWMHCV2VKG1rlEouSO4UIm2TuwO761NgveNeitN+jqadBZsKiOmGhW14irk+c+peVBExc3HiC/d5tnpqzw6fJKHvxzl7n9/Jm9/No+O/o+Xf9yhu90+sogcmVtrNDRpzLQY67HXNdPVbKfHYUVnD32dvfR39tLb1kW3Yz80tNJR10K7tUmQYyqajfU0aIyYa5RYpGoMlUrUpRIUz0tRloiRFbyi5kEhsvxiJI9LUIsr6O/uwenv938z3DvIUM+AcL4bGOb9wFveD73j/eBb/h7+i7/f/sXfw++F57+G3/P+7XveDr4VNDw4zNuBf56HGB4YZKhvQNBgbz9Dff0MdPUKwQR1fTi7e/nr/V/8HzLpSvkUrIc+AAAAAElFTkSuQmCC">
<style> <style>
* { margin: 0; padding: 0; box-sizing: border-box; } * { margin: 0; padding: 0; box-sizing: border-box; }
@@ -1166,7 +1166,7 @@
<div class="login-footer"> <div class="login-footer">
<div class="security-notice"> <div class="security-notice">
<strong>&#x26A0; Security Notice</strong> <strong>&#x26A0; Security Notice</strong>
SimpleRemoter.com may be flagged as "dangerous" by browsers due to the word "Remote" in its name. This is a false positive. The software is fully open-source and safe to use. This software is for lawful, authorized, and legitimate use only. You are solely responsible for your use and for complying with applicable law. The developer assumes no liability.
</div> </div>
<div class="login-links"> <div class="login-links">
<a href="https://simpleremoter.com/" target="_blank">&#x1F310; Website</a> <a href="https://simpleremoter.com/" target="_blank">&#x1F310; Website</a>
@@ -1349,11 +1349,14 @@
</div> </div>
</div> </div>
<script src="/static/xterm.js"></script> <script src="__BASE_PATH__/static/xterm.js"></script>
<script src="/static/xterm-fit.js"></script> <script src="__BASE_PATH__/static/xterm-fit.js"></script>
<!-- Opus codec for audio decompression --> <!-- Opus WASM decoder (WebAudio fallback for browsers without MSE+WebM/Opus,
<script src="https://cdn.jsdelivr.net/npm/opus.js@0.5.0/dist/opus.js"></script> e.g. iOS Safari/Chrome where WebKit's MediaSource lacks WebM/Opus).
Self-hosted (served from /static/) so audio works without reaching the
internet — a CDN copy is unreliable behind GFW/offline LANs. -->
<script src="__BASE_PATH__/static/opus-decoder.min.js" charset="UTF-8"></script>
<script> <script>
let ws = null, token = null, decoder = null, devices = [], currentDevice = null; let ws = null, token = null, decoder = null, devices = [], currentDevice = null;
@@ -1387,11 +1390,11 @@
// channels:2 sampleRate:4 bitsPerSample:2 blockAlign:2 // channels:2 sampleRate:4 bitsPerSample:2 blockAlign:2
// compression:1 (0=PCM unsupported by web, 1=Opus) reserved:1 // compression:1 (0=PCM unsupported by web, 1=Opus) reserved:1
// MSE + WebM/Opus playback. Raw Opus packets arrive over WS; we wrap // MSE + WebM/Opus playback (desktop Chrome/Firefox/Android). Raw Opus
// each one in a minimal WebM container in JS and feed it to a // packets arrive over WS; we wrap each one in a minimal WebM container
// SourceBuffer attached to a hidden <audio> element. The browser // and feed it to a SourceBuffer attached to a hidden <audio> element.
// decodes Opus natively. Tested on desktop Chrome; mobile playback // Browsers without MSE+WebM/Opus (iOS Safari/Chrome — WebKit has no
// is a known follow-up (see commit notes). // WebM/Opus in MediaSource) fall back to the WebAudio + WASM path below.
let audioFormat = null; // { compression, channels, sampleRate, bitsPerSample, blockAlign } let audioFormat = null; // { compression, channels, sampleRate, bitsPerSample, blockAlign }
let audioEnabled = true; // Audio on/off flag (set by UI) let audioEnabled = true; // Audio on/off flag (set by UI)
let syncDrift = 0; // A/V sync monitoring (milliseconds) let syncDrift = 0; // A/V sync monitoring (milliseconds)
@@ -1405,13 +1408,35 @@
const OPUS_FRAME_MS = 20; // 960 samples @ 48k — matches client encoder const OPUS_FRAME_MS = 20; // 960 samples @ 48k — matches client encoder
const _pendingOpusPackets = []; // packets received before SourceBuffer is ready const _pendingOpusPackets = []; // packets received before SourceBuffer is ready
// ---- WebAudio + WASM Opus fallback path ----
// Used when MSE + WebM/Opus is unsupported (iOS Safari/Chrome — WebKit's
// MediaSource has no WebM/Opus). Raw Opus packets are decoded to PCM
// with a WASM decoder (opus-decoder) and played via AudioContext. The
// decision is made on kernel capability, not UA, so it covers every iOS
// browser at once.
const _mseOpusSupported = !!(window.MediaSource &&
MediaSource.isTypeSupported('audio/webm; codecs="opus"'));
const _useWebAudioPath = !_mseOpusSupported;
let _audioCtx = null; // AudioContext (created/resumed inside a gesture)
let _opusDecoder = null; // WASM OpusDecoder (created when format is known)
let _decoderReady = false; // _opusDecoder.ready resolved
let _waNextTime = null; // next AudioBufferSourceNode start time (ctx clock)
let _waDropLogged = false; // one-shot console.warn for per-frame drop reasons
let _waDecodeFailLogged = false; // one-shot console.error for decode failures
const WA_LOOKAHEAD = 0.15; // seconds of lead time kept in the scheduler
const WA_MAX_LATENCY = 0.60; // drop backlog if we drift this far ahead
const WA_DECODE_SR = 48000; // Opus always decodes to 48 kHz
if (_useWebAudioPath) console.log('[Audio] WebAudio fallback path (no MSE/WebM-Opus)');
// Browser autoplay policies require an HTMLAudioElement to be created // Browser autoplay policies require an HTMLAudioElement to be created
// and .play()'d synchronously inside a user-gesture event handler. // and .play()'d synchronously inside a user-gesture event handler.
// We hook the first click/keydown to spin up the element + MediaSource. // We hook the first click/keydown to spin up the element + MediaSource.
// Subsequent activity (e.g. tab regaining focus) re-issues play(). // Subsequent activity (e.g. tab regaining focus) re-issues play().
function installAudioGestureUnlock() { function installAudioGestureUnlock() {
const onGesture = () => { const onGesture = () => {
if (!_audioElement) { if (_useWebAudioPath) {
_setupWebAudioContext();
} else if (!_audioElement) {
try { try {
_setupAudioElementAndMediaSource(); _setupAudioElementAndMediaSource();
console.log('[MSE] <audio> + MediaSource set up by gesture'); console.log('[MSE] <audio> + MediaSource set up by gesture');
@@ -1423,11 +1448,137 @@
} }
}; };
const opts = { passive: true, capture: true }; const opts = { passive: true, capture: true };
window.addEventListener('click', onGesture, opts); // click/keydown cover desktop; touchend/pointerdown are the most
window.addEventListener('keydown', onGesture, opts); // reliable "user gesture" triggers for AudioContext.resume() on iOS.
window.addEventListener('click', onGesture, opts);
window.addEventListener('keydown', onGesture, opts);
window.addEventListener('touchend', onGesture, opts);
window.addEventListener('pointerdown', onGesture, opts);
// Recover when the app returns to the foreground (iOS may suspend
// the AudioContext while backgrounded).
document.addEventListener('visibilitychange', () => {
if (!document.hidden && _useWebAudioPath) _setupWebAudioContext();
});
} }
installAudioGestureUnlock(); installAudioGestureUnlock();
// ============================================================
// WebAudio + WASM Opus fallback playback.
// The AudioContext is created/resumed inside installAudioGestureUnlock's
// user-gesture handler above (required by iOS autoplay policy). The
// WASM decoder is created lazily when the first AudioFormat arrives
// (handleAudioFrame), so it knows nothing about the container/codec
// support the MSE path depends on.
// ============================================================
function _setupWebAudioContext() {
if (_audioCtx) {
if (_audioCtx.state === 'suspended') {
_audioCtx.resume()
.then(() => console.log('[Audio] resumed → ' + _audioCtx.state))
.catch(e => console.error('[Audio] resume rejected: ' + (e && e.message)));
}
return;
}
const AC = window.AudioContext || window.webkitAudioContext;
if (!AC) { console.error('[Audio] Web Audio API unavailable'); return; }
try {
_audioCtx = new AC();
console.log('[Audio] AudioContext created, resuming…');
_audioCtx.resume()
.then(() => console.log('[Audio] AudioContext ' + _audioCtx.state))
.catch(e => console.error('[Audio] resume rejected: ' + (e && e.message)));
} catch (e) {
console.error('[Audio] AudioContext failed: ' + (e && e.message));
}
}
function _initWebAudioDecoder(fmt) {
if (_opusDecoder) { try { _opusDecoder.free().catch(() => {}); } catch (e) {} }
_opusDecoder = null;
_decoderReady = false;
_waNextTime = null;
_waDropLogged = false;
_waDecodeFailLogged = false;
const OpusDecoder = window['opus-decoder'] && window['opus-decoder'].OpusDecoder;
if (!OpusDecoder) {
console.error('[Audio] opus-decoder not loaded (static script missing?)');
return;
}
try {
_opusDecoder = new OpusDecoder({ sampleRate: WA_DECODE_SR, channels: (fmt && fmt.channels) || 2 });
_opusDecoder.ready.then(() => {
_decoderReady = true;
console.log('[Audio] WebAudio decoder ready ch=' + fmt.channels);
}).catch(e => {
console.error('[Audio] decoder ready failed: ' + (e && e.message));
});
} catch (e) {
console.error('[Audio] decoder construction failed: ' + (e && e.message));
}
}
function _pushOpusWebAudio(opusBytes) {
if (!_opusDecoder || !_decoderReady) return; // WASM still compiling
if (!_audioCtx) {
if (!_waDropLogged) { _waDropLogged = true; console.warn('[Audio] no AudioContext (tap the page first)'); }
return;
}
if (_audioCtx.state !== 'running') {
if (!_waDropLogged) { _waDropLogged = true; console.warn('[Audio] ctx ' + _audioCtx.state + ' — dropping audio'); }
return;
}
// decodeFrame in this bundle is SYNCHRONOUS: it drives the WASM
// decoder directly and returns {channelData, samplesDecoded,
// sampleRate, errors} — not a Promise. Calling .then() on it throws
// a TypeError before any error handling can run. Decode in try/catch.
let res;
try {
res = _opusDecoder.decodeFrame(new Uint8Array(opusBytes));
} catch (e) {
if (!_waDecodeFailLogged) { _waDecodeFailLogged = true; console.error('[Audio] decodeFrame threw: ' + (e && e.message)); }
return;
}
if (!res || !res.samplesDecoded) {
let errStr = 'no samples';
if (res && res.errors && res.errors.length) {
try { errStr = String(res.errors[0].message || res.errors[0]); } catch (e) { errStr = String(res.errors[0]); }
}
if (!_waDecodeFailLogged) { _waDecodeFailLogged = true; console.error('[Audio] decode fail: ' + errStr); }
return;
}
const ch = res.channelData ? res.channelData.length : 0;
if (ch < 1) {
if (!_waDecodeFailLogged) { _waDecodeFailLogged = true; console.error('[Audio] decode ch=0 samples=' + res.samplesDecoded); }
return;
}
if (!_audioCtx || _audioCtx.state !== 'running') {
if (!_waDropLogged) { _waDropLogged = true; console.warn('[Audio] ctx ' + (_audioCtx && _audioCtx.state) + ' at schedule'); }
return;
}
const samples = res.samplesDecoded;
const sr = res.sampleRate || WA_DECODE_SR;
const buffer = _audioCtx.createBuffer(ch, samples, sr);
for (let c = 0; c < ch; c++) {
if (res.channelData[c]) buffer.copyToChannel(res.channelData[c], c);
}
const src = _audioCtx.createBufferSource();
src.buffer = buffer;
src.connect(_audioCtx.destination);
const now = _audioCtx.currentTime;
if (_waNextTime === null || _waNextTime < now) {
_waNextTime = now + WA_LOOKAHEAD;
}
src.start(_waNextTime);
_waNextTime += samples / sr;
if (_waNextTime - now > WA_MAX_LATENCY) {
// Decoder/latency spike put us too far behind; drop the
// backlog and stay live instead of playing stale audio.
_waNextTime = now + WA_LOOKAHEAD;
}
}
// Pagination and filter state // Pagination and filter state
let currentPage = 1; let currentPage = 1;
let viewMode = 'grid'; // 'grid' or 'list' let viewMode = 'grid'; // 'grid' or 'list'
@@ -1524,7 +1675,7 @@
const protocol = location.protocol === 'https:' ? 'wss:' : 'ws:'; const protocol = location.protocol === 'https:' ? 'wss:' : 'ws:';
updateWsStatus('connecting'); updateWsStatus('connecting');
try { try {
ws = new WebSocket(protocol + '//' + location.host + '/ws'); ws = new WebSocket(protocol + '//' + location.host + '__BASE_PATH__/ws');
} catch (e) { } catch (e) {
updateWsStatus('disconnected'); updateWsStatus('disconnected');
scheduleReconnect(); scheduleReconnect();
@@ -2003,6 +2154,10 @@
function pushOpusPacket(opusBytes) { function pushOpusPacket(opusBytes) {
if (!audioFormat || audioFormat.compression !== 1) return; if (!audioFormat || audioFormat.compression !== 1) return;
if (_useWebAudioPath) {
_pushOpusWebAudio(opusBytes);
return;
}
if (_sourceBuffer && _initSegmentSent) { if (_sourceBuffer && _initSegmentSent) {
_enqueueAppend(WebMMuxer.buildCluster(opusBytes, _opusTimestampMs)); _enqueueAppend(WebMMuxer.buildCluster(opusBytes, _opusTimestampMs));
_opusTimestampMs += OPUS_FRAME_MS; _opusTimestampMs += OPUS_FRAME_MS;
@@ -2030,6 +2185,12 @@
_initSegmentSent = false; _initSegmentSent = false;
_opusTimestampMs = 0; _opusTimestampMs = 0;
_pendingOpusPackets.length = 0; _pendingOpusPackets.length = 0;
// Reset the WebAudio fallback (keep the AudioContext alive so we
// don't forfeit the gesture-acquired play permission on reinit).
if (_opusDecoder) { try { _opusDecoder.free().catch(() => {}); } catch (e) {} }
_opusDecoder = null;
_decoderReady = false;
_waNextTime = null;
} }
function handleAudioFrame(data) { function handleAudioFrame(data) {
@@ -2073,10 +2234,12 @@
return; return;
} }
stopAllAudio(); stopAllAudio();
if (_mediaSource && _mediaSource.readyState === 'open') { if (_useWebAudioPath) {
_initWebAudioDecoder(fmt);
} else if (_mediaSource && _mediaSource.readyState === 'open') {
_addSourceBufferAndInit(); _addSourceBufferAndInit();
} }
// else: sourceopen handler will pick up audioFormat when it fires // else (MSE): sourceopen handler will pick up audioFormat when it fires
console.log('[Audio] Format → ch=' + fmt.channels + console.log('[Audio] Format → ch=' + fmt.channels +
' sr=' + fmt.sampleRate + ' compression=' + fmt.compression); ' sr=' + fmt.sampleRate + ' compression=' + fmt.compression);
} }
@@ -3079,7 +3242,11 @@
// authoritative — toggleAudio() does not flip state locally; it only // authoritative — toggleAudio() does not flip state locally; it only
// sends the request and waits for the audio_state broadcast. // sends the request and waits for the audio_state broadcast.
function applyAudioState(enabled) { function applyAudioState(enabled) {
const prev = audioEnabled;
audioEnabled = !!enabled; audioEnabled = !!enabled;
if (prev !== audioEnabled) {
console.log('[Audio] ' + (audioEnabled ? 'enabled' : 'disabled'));
}
const btn = document.getElementById('btn-audio-bar'); const btn = document.getElementById('btn-audio-bar');
if (btn) { if (btn) {
// 0x1F50A speaker / 0x1F507 muted speaker // 0x1F50A speaker / 0x1F507 muted speaker