FreeRDP
Loading...
Searching...
No Matches
win_shadow_cliprdr.c
1
20#include <freerdp/config.h>
21
22#include <winpr/assert.h>
23
24#include "win_shadow_cliprdr.h"
25
26#if defined(CHANNEL_CLIPRDR_SERVER)
27
28/* Include order matters: the NIIF_* macros from shellapi.h collide with the
29 * same-named enum members in rail.h, so all FreeRDP headers must come before
30 * the Windows/Shell headers. */
31#include <winpr/platform.h>
32
33#include <freerdp/log.h>
34#include <freerdp/utils/cliprdr_utils.h>
35
36#include <freerdp/channels/cliprdr.h>
37#include <freerdp/server/cliprdr.h>
38
39#include <winsock2.h>
40#include <ws2tcpip.h>
41#include <iphlpapi.h>
42
43#include <windows.h>
44#include <shellapi.h>
45#include <shlobj.h>
46
47#define TAG SERVER_TAG("shadow.win.cliprdr")
48
49/* Clipboard semantics are a poor fit for bulk transfers, so file pulls are
50 * capped: a single file larger than CLIP_MAX_FILE_BYTES or a total larger
51 * than CLIP_MAX_TOTAL_BYTES is rejected. */
52static const UINT64 CLIP_MAX_FILE_BYTES = 512ull * 1024 * 1024;
53static const UINT64 CLIP_MAX_TOTAL_BYTES = 2ull * 1024 * 1024 * 1024;
54static const UINT32 CLIP_CHUNK = 4u * 1024 * 1024;
55static const UINT32 CLIP_MAX_FILES = 256;
56
57#define FMT_BASE 0xC000 /* base for our self-assigned named format ids */
58
59typedef struct
60{
61 WCHAR path[MAX_PATH]; /* local absolute path (data source for pulls) */
62 WCHAR rel[MAX_PATH]; /* wire cFileName: path relative to drop root */
63 DWORD attrs;
64 UINT64 size;
65} ClipFile;
66
67typedef struct
68{
69 rdpShadowClient* client;
70 CliprdrServerContext* ctx;
71
72 HANDLE thread; /* clipboard watcher thread */
73 HWND hwnd; /* message-only window owned by the thread */
74 HANDLE ready; /* set once hwnd was created */
75 HANDLE stopEvt; /* set on uninit to leave the thread */
76 LONG suppress; /* count of WM_CLIPBOARDUPDATE caused by our own writes */
77
78 UINT32 nextFmt; /* named format id allocator */
79 UINT32 fgdId; /* our announced FileGroupDescriptorW id */
80 UINT32 contentsId; /* FileContents id */
81 UINT32 dropId; /* PreferredDropEffect id */
82
83 ClipFile* files; /* remote clipboard CF_HDROP snapshot (serves client pulls) */
84 UINT32 nFiles;
85
86 /* pull-in: files announced by the client, materialized under stageDir and
87 * then presented as a real CF_HDROP */
88 FILEDESCRIPTORW* inDesc;
89 UINT32 inCount;
90 UINT32 inIdx; /* index of the file currently being pulled */
91 UINT64 inSize; /* total size of the current file */
92 UINT64 inGot; /* bytes received of the current file */
93 HANDLE inFile; /* materialization target of the current file */
94 UINT32 inStream; /* self-incrementing streamId for contents requests */
95 BOOL inAwaitSize; /* TRUE = awaiting SIZE response, FALSE = awaiting RANGE data */
96 UINT64 inTotal; /* accumulated bytes of this pull (total cap) */
97
98 UINT32 reqQ[8]; /* pending ServerFormatDataRequest queue (FIFO) */
99 UINT32 reqHead, reqTail;
100 UINT32 peerFgdId; /* FileGroupDescriptorW id announced by the client */
101
102 WCHAR stageDir[MAX_PATH];
103 CRITICAL_SECTION lock;
104} ShadowClip;
105
106static ShadowClip* clipOf(CliprdrServerContext* ctx)
107{
108 return (ShadowClip*)ctx->custom;
109}
110
111/* The ServerXxx channel send functions are NODISCARD: log send failures
112 * instead of dropping them silently. */
113static void cliprdr_check_send(UINT rc, const char* what)
114{
115 if (rc != CHANNEL_RC_OK)
116 WLog_WARN(TAG, "%s failed with %" PRIu32, what, rc);
117}
118
119/* ---------- remote clipboard I/O (caller: channel thread, synchronous) ---------- */
120
121/* The clipboard is a session-global resource with exclusive open: any window
122 * of the session may hold it for a while (Explorer refreshing after a paste,
123 * other viewers...). OpenClipboard therefore fails transiently under
124 * contention; retry for ~0.5 s in small steps before giving up. */
125WINPR_ATTR_NODISCARD
126static BOOL open_clipboard_retry(void)
127{
128 for (int i = 0; i < 20; ++i)
129 {
130 if (OpenClipboard(nullptr))
131 return TRUE;
132 Sleep(25);
133 }
134 return FALSE;
135}
136
137/* suppress must be incremented before writing the remote clipboard; the
138 * watcher thread skips the resulting WM_CLIPBOARDUPDATE. */
139static void write_remote_text(ShadowClip* clip, const BYTE* utf16, UINT32 len)
140{
141 WINPR_ASSERT(clip);
142 WINPR_ASSERT(utf16);
143
144 HANDLE hMem = nullptr;
145 hMem = GlobalAlloc(GMEM_MOVEABLE, len + 2);
146 if (!hMem)
147 return;
148 void* p = nullptr;
149 p = GlobalLock(hMem);
150 if (p)
151 {
152 CopyMemory(p, utf16, len);
153 ZeroMemory((BYTE*)p + len, 2); /* terminating NUL for CF_UNICODETEXT */
154 GlobalUnlock(hMem);
155 }
156 InterlockedIncrement(&clip->suppress);
157 if (open_clipboard_retry())
158 {
159 EmptyClipboard();
160 if (!SetClipboardData(CF_UNICODETEXT, hMem))
161 {
162 GlobalFree(hMem);
163 InterlockedDecrement(&clip->suppress);
164 }
165 CloseClipboard();
166 }
167 else
168 {
169 GlobalFree(hMem);
170 InterlockedDecrement(&clip->suppress);
171 }
172}
173
174/* Once the pull-in files are materialized under stageDir, present a real
175 * CF_HDROP on the remote clipboard: Ctrl+V in Explorer then performs an
176 * ordinary local file copy. */
177static void write_remote_hdrop(ShadowClip* clip)
178{
179 WINPR_ASSERT(clip);
180
181 UINT32 n = 0;
182 SIZE_T bytes = 0;
183 bytes = sizeof(DROPFILES) + sizeof(WCHAR);
184 WLog_INFO(TAG, "write_remote_hdrop: enter inCount=%u", clip->inCount);
185 EnterCriticalSection(&clip->lock);
186 /* Only top-level entries go into DROPFILES: a top-level directory was
187 * materialized as a whole under stageDir and hands its full tree to
188 * Explorer; nested entries (rel containing '\\') are not listed. */
189 for (UINT32 i = 0; i < clip->inCount; ++i)
190 {
191 if (!wcschr(clip->inDesc[i].cFileName, L'\\'))
192 {
193 WCHAR full[MAX_PATH];
194 _snwprintf_s(full, MAX_PATH, _TRUNCATE, L"%s\\%s", clip->stageDir,
195 clip->inDesc[i].cFileName);
196 bytes += (wcslen(full) + 1) * sizeof(WCHAR);
197 ++n;
198 }
199 }
200 LeaveCriticalSection(&clip->lock);
201 if (!n)
202 return;
203
204 HANDLE hMem = nullptr;
205 hMem = GlobalAlloc(GMEM_MOVEABLE | GMEM_ZEROINIT, bytes);
206 if (!hMem)
207 return;
208 DROPFILES* df = (DROPFILES*)GlobalLock(hMem);
209 if (!df)
210 {
211 GlobalFree(hMem);
212 return;
213 }
214 df->pFiles = sizeof(DROPFILES);
215 df->fWide = TRUE;
216 WCHAR* dst = nullptr;
217 dst = (WCHAR*)((BYTE*)df + sizeof(DROPFILES));
218 SIZE_T remain = bytes; /* includes the trailing NUL; shrinks per item */
219 EnterCriticalSection(&clip->lock);
220 for (UINT32 i = 0; i < clip->inCount; ++i)
221 {
222 if (wcschr(clip->inDesc[i].cFileName, L'\\'))
223 continue;
224 /* The size argument must be the space REMAINING at dst: dst advances
225 * per item, and passing MAX_PATH unconditionally can write past the
226 * allocation once accumulated paths overflow (heap corruption). */
227 _snwprintf_s(dst, remain / sizeof(WCHAR), _TRUNCATE, L"%s\\%s", clip->stageDir,
228 clip->inDesc[i].cFileName);
229 const SIZE_T wrote = (wcslen(dst) + 1) * sizeof(WCHAR);
230 dst += wrote;
231 remain -= wrote;
232 }
233 LeaveCriticalSection(&clip->lock);
234 GlobalUnlock(hMem);
235
236 /* PreferredDropEffect=COPY: Explorer treats the drop as a copy, not a move */
237 UINT cfDrop = 0;
238 cfDrop = RegisterClipboardFormatW(CFSTR_PREFERREDDROPEFFECT);
239 HANDLE hEff = nullptr;
240 hEff = GlobalAlloc(GMEM_MOVEABLE, sizeof(DWORD));
241 if (hEff)
242 {
243 DWORD* e = nullptr;
244 e = (DWORD*)GlobalLock(hEff);
245 if (e)
246 *e = DROPEFFECT_COPY;
247 GlobalUnlock(hEff);
248 }
249
250 InterlockedIncrement(&clip->suppress);
251 if (open_clipboard_retry())
252 {
253 EmptyClipboard();
254 if (SetClipboardData(CF_HDROP, hMem))
255 {
256 if (hEff)
257 SetClipboardData(cfDrop, hEff);
258 }
259 else
260 {
261 GlobalFree(hMem);
262 if (hEff)
263 GlobalFree(hEff);
264 InterlockedDecrement(&clip->suppress);
265 }
266 CloseClipboard();
267 WLog_INFO(TAG, "client files pasted to remote clipboard (%u files)", n);
268 }
269 else
270 {
271 GlobalFree(hMem);
272 if (hEff)
273 GlobalFree(hEff);
274 InterlockedDecrement(&clip->suppress);
275 }
276}
277
278/* ---------- remote clipboard snapshot (taken by the watcher thread) ---------- */
279
280/* Recursive enumeration: directories expand into a flat list with rel storing
281 * the path relative to the drop root ("dir\sub\f"). Directory entries carry
282 * FILE_ATTRIBUTE_DIRECTORY as placeholders so the receiving side can rebuild
283 * the tree. */
284static void clipfiles_push(ClipFile** list, UINT32* n, UINT32* cap, const WCHAR* full,
285 const WCHAR* rel)
286{
287 WINPR_ASSERT(list);
288 WINPR_ASSERT(n);
289 WINPR_ASSERT(cap);
290 WINPR_ASSERT(full);
291 WINPR_ASSERT(rel);
292
293 DWORD a = GetFileAttributesW(full);
294 if (a == INVALID_FILE_ATTRIBUTES || *n >= CLIP_MAX_FILES)
295 return;
296 if (*n >= *cap)
297 {
298 *cap = *cap ? *cap * 2 : 64;
299 ClipFile* nl = (ClipFile*)realloc(*list, *cap * sizeof(ClipFile));
300 if (!nl)
301 {
302 free(*list);
303 *list = nullptr;
304 *n = *cap = 0;
305 return;
306 }
307 *list = nl;
308 }
309 ClipFile* cf = &(*list)[(*n)++];
310 wcscpy_s(cf->path, MAX_PATH, full);
311 wcscpy_s(cf->rel, MAX_PATH, rel);
312 cf->attrs = a;
313 cf->size = 0;
314 if (!(a & FILE_ATTRIBUTE_DIRECTORY))
315 {
316 HANDLE f = CreateFileW(full, GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE, nullptr,
317 OPEN_EXISTING, 0, nullptr);
318 if (f != INVALID_HANDLE_VALUE)
319 {
320 LARGE_INTEGER sz;
321 if (GetFileSizeEx(f, &sz))
322 cf->size = (UINT64)sz.QuadPart;
323 CloseHandle(f);
324 }
325 return;
326 }
327 /* Directory: keep the entry itself, then recurse into its children */
328 WCHAR pat[MAX_PATH];
329 _snwprintf_s(pat, MAX_PATH, _TRUNCATE, L"%s\\*", full);
331 HANDLE h = FindFirstFileW(pat, &fd);
332 if (h == INVALID_HANDLE_VALUE)
333 return;
334 do
335 {
336 if ((fd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) &&
337 (!wcscmp(fd.cFileName, L".") || !wcscmp(fd.cFileName, L"..")))
338 continue;
339 WCHAR sub[MAX_PATH] = { 0 };
340 WCHAR srel[MAX_PATH] = { 0 };
341 _snwprintf_s(sub, MAX_PATH, _TRUNCATE, L"%s\\%s", full, fd.cFileName);
342 _snwprintf_s(srel, MAX_PATH, _TRUNCATE, L"%s\\%s", rel, fd.cFileName);
343 clipfiles_push(list, n, cap, sub, srel);
344 } while (FindNextFileW(h, &fd) && *n < CLIP_MAX_FILES);
345 FindClose(h);
346}
347
348static void snapshot_remote_files(ShadowClip* clip)
349{
350 WINPR_ASSERT(clip);
351
352 HANDLE h = nullptr; /* OpenClipboard already held by caller */
353 h = GetClipboardData(CF_HDROP);
354 if (!h)
355 return;
356 HDROP drop = (HDROP)h;
357 UINT cnt = 0;
358 cnt = DragQueryFileW(drop, 0xFFFFFFFF, nullptr, 0);
359 ClipFile* list = nullptr;
360 UINT32 real = 0;
361 UINT32 cap = 0;
362 for (UINT i = 0; i < cnt && real < CLIP_MAX_FILES; ++i)
363 {
364 WCHAR path[MAX_PATH];
365 if (!DragQueryFileW(drop, i, path, MAX_PATH))
366 continue;
367 const WCHAR* base = wcsrchr(path, L'\\');
368 clipfiles_push(&list, &real, &cap, path, base ? base + 1 : path);
369 }
370 EnterCriticalSection(&clip->lock);
371 free(clip->files);
372 clip->files = list;
373 clip->nFiles = real;
374 LeaveCriticalSection(&clip->lock);
375}
376
377/* ---------- announce remote clipboard to the client (watcher thread) ---------- */
378
379static void announce_local(ShadowClip* clip)
380{
381 WINPR_ASSERT(clip);
382
383 if (!open_clipboard_retry())
384 return;
385 BOOL hasText = FALSE;
386 BOOL hasFiles = FALSE;
387 hasText = IsClipboardFormatAvailable(CF_UNICODETEXT) || IsClipboardFormatAvailable(CF_TEXT);
388 hasFiles = IsClipboardFormatAvailable(CF_HDROP);
389 if (hasFiles)
390 snapshot_remote_files(clip);
391 CloseClipboard();
392
393 CLIPRDR_FORMAT fmts[4] = WINPR_C_ARRAY_INIT;
394 UINT32 n = 0;
395 if (hasText)
396 {
397 fmts[n].formatId = CF_UNICODETEXT;
398 fmts[n].formatName = nullptr;
399 ++n;
400 }
401 if (hasFiles && clip->nFiles)
402 {
403 fmts[n].formatId = clip->fgdId;
404 fmts[n].formatName = _strdup("FileGroupDescriptorW");
405 ++n;
406 fmts[n].formatId = clip->contentsId;
407 fmts[n].formatName = _strdup("FileContents");
408 ++n;
409 fmts[n].formatId = clip->dropId;
410 fmts[n].formatName = _strdup("PreferredDropEffect");
411 ++n;
412 }
413 if (!n)
414 return;
415 const CLIPRDR_FORMAT_LIST list = {
416 .common = { .msgType = CB_FORMAT_LIST, .msgFlags = 0, .dataLen = 0 },
417 .numFormats = n,
418 .formats = fmts
419 };
420 cliprdr_check_send(clip->ctx->ServerFormatList(clip->ctx, &list), "ServerFormatList");
421 for (UINT32 i = 0; i < n; ++i)
422 free(fmts[i].formatName);
423}
424
425/* ---------- pull-in: client files materialized remotely (channel thread) ---------- */
426
427static void pull_abort(ShadowClip* clip)
428{
429 WINPR_ASSERT(clip);
430
431 if (clip->inFile != INVALID_HANDLE_VALUE && clip->inFile)
432 CloseHandle(clip->inFile);
433 clip->inFile = nullptr;
434 free(clip->inDesc);
435 clip->inDesc = nullptr;
436 clip->inCount = clip->inIdx = 0;
437 clip->inSize = clip->inGot = clip->inTotal = 0;
438 clip->inAwaitSize = FALSE;
439}
440
441/* Pull complete: hand the materialized files to the remote clipboard as a
442 * real CF_HDROP. */
443static void finish_pull(ShadowClip* clip)
444{
445 write_remote_hdrop(clip);
446}
447
448static void rm_tree(const WCHAR* dir)
449{
450 WCHAR pat[MAX_PATH];
451 _snwprintf_s(pat, MAX_PATH, _TRUNCATE, L"%s\\*", dir);
453 HANDLE h = FindFirstFileW(pat, &fd);
454 if (h == INVALID_HANDLE_VALUE)
455 return;
456 do
457 {
458 if (!wcscmp(fd.cFileName, L".") || !wcscmp(fd.cFileName, L".."))
459 continue;
460 WCHAR full[MAX_PATH];
461 _snwprintf_s(full, MAX_PATH, _TRUNCATE, L"%s\\%s", dir, fd.cFileName);
462 if (fd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)
463 {
464 rm_tree(full);
465 RemoveDirectoryW(full);
466 }
467 else
468 {
469 DeleteFileW(full);
470 }
471 } while (FindNextFileW(h, &fd));
472 FindClose(h);
473}
474
475static void wipe_stage_dir(ShadowClip* clip)
476{
477 CreateDirectoryW(clip->stageDir, nullptr);
478 rm_tree(clip->stageDir);
479}
480
481/* Creates the directory chain under stageDir matching rel ("dir\sub\f");
482 * with includeLast=FALSE the last segment is treated as a file name and only
483 * the parent directories are created. Shared by directory descriptors and
484 * file materialization. */
485static void mkdirs_under(ShadowClip* clip, const WCHAR* rel, BOOL includeLast)
486{
487 WINPR_ASSERT(clip);
488 WINPR_ASSERT(rel);
489
490 WCHAR acc[MAX_PATH];
491 wcscpy_s(acc, MAX_PATH, clip->stageDir);
492 const WCHAR* p = rel;
493 while (*p)
494 {
495 const WCHAR* bs = wcschr(p, L'\\');
496 const size_t n = bs ? (size_t)(bs - p) : wcslen(p);
497 if (!bs && !includeLast)
498 break;
499 const size_t l = wcslen(acc);
500 if (l + 1 + n >= MAX_PATH)
501 break;
502 acc[l] = L'\\';
503 wcsncpy_s(acc + l + 1, MAX_PATH - l - 1, p, n);
504 acc[l + 1 + n] = 0;
505 CreateDirectoryW(acc, nullptr);
506 if (!bs)
507 break;
508 p = bs + 1;
509 }
510}
511
512/* Advances the pull cursor: directory descriptors create their directory and
513 * are skipped; the cursor stops at the next file descriptor. */
514static void pull_skip_dirs(ShadowClip* clip)
515{
516 while (clip->inIdx < clip->inCount)
517 {
518 if (!(clip->inDesc[clip->inIdx].dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY))
519 break;
520 mkdirs_under(clip, clip->inDesc[clip->inIdx].cFileName, TRUE);
521 clip->inIdx++;
522 }
523}
524
525/* Sends the next FileContentsRequest; FALSE means the pull is finished. */
526WINPR_ATTR_NODISCARD
527static BOOL pull_step(ShadowClip* clip)
528{
529 WINPR_ASSERT(clip);
530
531 if (clip->inIdx >= clip->inCount)
532 return FALSE;
533 CLIPRDR_FILE_CONTENTS_REQUEST req = { .common = { .msgType = CB_FILECONTENTS_REQUEST,
534 .msgFlags = 0 },
535 .streamId = ++clip->inStream,
536 .listIndex = clip->inIdx,
537 .dwFlags = 0,
538 .nPositionLow = 0,
539 .nPositionHigh = 0,
540 .cbRequested = 0,
541 .haveClipDataId = FALSE,
542 .clipDataId = 0 };
543 if (clip->inGot == 0)
544 {
545 req.dwFlags = FILECONTENTS_SIZE;
546 req.cbRequested = 8;
547 clip->inAwaitSize = TRUE;
548 }
549 else
550 {
551 req.dwFlags = FILECONTENTS_RANGE;
552 const UINT64 left = clip->inSize - clip->inGot;
553 const UINT64 want = left < CLIP_CHUNK ? left : CLIP_CHUNK;
554 req.nPositionLow = (UINT32)(clip->inGot & 0xFFFFFFFFull);
555 req.nPositionHigh = (UINT32)(clip->inGot >> 32);
556 req.cbRequested = (UINT32)want;
557 clip->inAwaitSize = FALSE;
558 }
559 cliprdr_check_send(clip->ctx->ServerFileContentsRequest(clip->ctx, &req),
560 "ServerFileContentsRequest");
561 return TRUE;
562}
563
564/* Current file complete -> next file; all done -> place the CF_HDROP. */
565static void pull_advance(ShadowClip* clip)
566{
567 WINPR_ASSERT(clip);
568
569 if (clip->inFile && clip->inFile != INVALID_HANDLE_VALUE)
570 CloseHandle(clip->inFile);
571 clip->inFile = nullptr;
572 clip->inIdx++;
573 clip->inSize = clip->inGot = 0;
574 pull_skip_dirs(clip);
575 if (clip->inIdx >= clip->inCount)
576 {
577 finish_pull(clip);
578 pull_abort(clip); /* release state, desc is consumed */
579 return;
580 }
581 (void)pull_step(clip);
582}
583
584static void on_client_file_contents(ShadowClip* clip, const CLIPRDR_FILE_CONTENTS_RESPONSE* r)
585{
586 WINPR_ASSERT(clip);
587 WINPR_ASSERT(r);
588
589 if (!clip->inDesc || (r->common.msgFlags & CB_RESPONSE_FAIL))
590 {
591 WLog_WARN(TAG, "filecontents response fail/idle (flags=0x%x)", r->common.msgFlags);
592 pull_abort(clip);
593 return;
594 }
595 if (clip->inAwaitSize)
596 {
597 clip->inAwaitSize = FALSE; /* must clear before any early return below:
598 otherwise a pending SIZE response would be
599 re-parsed as RANGE data */
600 if (r->common.dataLen < 8 || !r->requestedData)
601 {
602 pull_abort(clip);
603 return;
604 }
605 clip->inSize = *(const UINT64*)r->requestedData;
606 WLog_INFO(TAG, "pull: SIZE idx=%u size=%llu dataLen=%u", clip->inIdx,
607 (unsigned long long)clip->inSize, r->common.dataLen);
608 if (clip->inSize > CLIP_MAX_FILE_BYTES ||
609 clip->inTotal + clip->inSize > CLIP_MAX_TOTAL_BYTES)
610 {
611 WLog_WARN(TAG, "file too large for clipboard transfer (%llu bytes)",
612 (unsigned long long)clip->inSize);
613 pull_abort(clip);
614 return;
615 }
616 mkdirs_under(clip, clip->inDesc[clip->inIdx].cFileName, FALSE);
617 WCHAR full[MAX_PATH];
618 _snwprintf_s(full, MAX_PATH, _TRUNCATE, L"%s\\%s", clip->stageDir,
619 clip->inDesc[clip->inIdx].cFileName);
620 clip->inFile = CreateFileW(full, GENERIC_WRITE, 0, nullptr, CREATE_ALWAYS,
621 FILE_ATTRIBUTE_NORMAL, nullptr);
622 if (clip->inFile == INVALID_HANDLE_VALUE)
623 {
624 WLog_WARN(TAG, "stage create failed err=%lu path=%ls", GetLastError(), full);
625 pull_abort(clip);
626 return;
627 }
628 if (clip->inSize == 0)
629 {
630 pull_advance(clip);
631 return;
632 }
633 clip->inGot = 0;
634 /* SIZE is in hand -> send the RANGE request right away instead of
635 * waiting for another response cycle. */
637 .common = { .msgType = CB_FILECONTENTS_REQUEST, .msgFlags = 0 },
638 .streamId = ++clip->inStream,
639 .listIndex = clip->inIdx,
640 .dwFlags = FILECONTENTS_RANGE,
641 .nPositionLow = 0,
642 .nPositionHigh = 0,
643 .cbRequested = (UINT32)(clip->inSize < CLIP_CHUNK ? clip->inSize : CLIP_CHUNK),
644 .haveClipDataId = FALSE,
645 .clipDataId = 0
646 };
647 cliprdr_check_send(clip->ctx->ServerFileContentsRequest(clip->ctx, &req),
648 "ServerFileContentsRequest");
649 return;
650 }
651 /* RANGE data chunk */
652 if (clip->inFile && clip->inFile != INVALID_HANDLE_VALUE && r->requestedData)
653 {
654 DWORD wr = 0;
655 LARGE_INTEGER off;
656 off.QuadPart = (LONGLONG)clip->inGot;
657 SetFilePointerEx(clip->inFile, off, nullptr, FILE_BEGIN);
658 WriteFile(clip->inFile, r->requestedData, r->common.dataLen, &wr, nullptr);
659 clip->inGot += wr;
660 clip->inTotal += wr;
661 }
662 if (clip->inGot >= clip->inSize)
663 pull_advance(clip);
664 else
665 (void)pull_step(clip);
666 WLog_INFO(TAG, "pull: RANGE idx=%u got=%u/%u", clip->inIdx, (unsigned)clip->inGot,
667 (unsigned)clip->inSize);
668}
669
670/* ---------- cliprdr server callbacks (cliprdr channel thread) ---------- */
671
672static UINT srv_client_format_list(CliprdrServerContext* ctx, const CLIPRDR_FORMAT_LIST* list)
673{
674 WINPR_ASSERT(ctx);
675 WINPR_ASSERT(list);
676
677 ShadowClip* clip = clipOf(ctx);
678 UINT32 textId = 0;
679 UINT32 fgdId = 0;
680 WLog_INFO(TAG, "format_list: %u formats", list->numFormats);
681 for (UINT32 i = 0; i < list->numFormats; ++i)
682 {
683 const CLIPRDR_FORMAT* f = &list->formats[i];
684 if (!f->formatName && (f->formatId == CF_UNICODETEXT || f->formatId == CF_TEXT))
685 textId = f->formatId;
686 else if (f->formatName && strcmp(f->formatName, "FileGroupDescriptorW") == 0)
687 fgdId = f->formatId;
688 }
689 clip->peerFgdId = fgdId;
690 /* Requests are queued: responses carry no formatId, so they are matched
691 * against the queue in send order. */
692 if (textId)
693 {
694 clip->reqQ[clip->reqTail++ % 8] = textId;
695 const CLIPRDR_FORMAT_DATA_REQUEST req = { .common = { .msgType = CB_FORMAT_DATA_REQUEST,
696 .msgFlags = 0 },
697 .requestedFormatId = textId };
698 cliprdr_check_send(ctx->ServerFormatDataRequest(ctx, &req), "ServerFormatDataRequest");
699 }
700 if (fgdId && ctx->streamFileClipEnabled)
701 {
702 clip->reqQ[clip->reqTail++ % 8] = fgdId;
703 const CLIPRDR_FORMAT_DATA_REQUEST req = { .common = { .msgType = CB_FORMAT_DATA_REQUEST,
704 .msgFlags = 0 },
705 .requestedFormatId = fgdId };
706 cliprdr_check_send(ctx->ServerFormatDataRequest(ctx, &req), "ServerFormatDataRequest");
707 }
708 const CLIPRDR_FORMAT_LIST_RESPONSE resp = { .common = { .msgType = CB_FORMAT_LIST_RESPONSE,
709 .msgFlags = CB_RESPONSE_OK } };
710 cliprdr_check_send(ctx->ServerFormatListResponse(ctx, &resp), "ServerFormatListResponse");
711 return CHANNEL_RC_OK;
712}
713
714static UINT srv_client_format_data_response(CliprdrServerContext* ctx,
716{
717 WINPR_ASSERT(ctx);
718 WINPR_ASSERT(r);
719
720 ShadowClip* clip = clipOf(ctx);
721 UINT32 id = 0;
722 if (clip->reqHead != clip->reqTail)
723 id = clip->reqQ[clip->reqHead++ % 8];
724 if (!(r->common.msgFlags & CB_RESPONSE_OK) || !r->requestedFormatData)
725 return CHANNEL_RC_OK;
726 if (id == CF_UNICODETEXT || id == CF_TEXT)
727 {
728 write_remote_text(clip, r->requestedFormatData, r->common.dataLen);
729 return CHANNEL_RC_OK;
730 }
731 if (id == clip->peerFgdId && id != 0)
732 {
733 /* client file list -> start the materialization state machine */
734 WLog_INFO(TAG, "pull: fgd response, start");
735 pull_abort(clip);
736 wipe_stage_dir(clip);
737 if (cliprdr_parse_file_list(r->requestedFormatData, r->common.dataLen, &clip->inDesc,
738 &clip->inCount) != NO_ERROR)
739 {
740 /* the parser keeps the partially filled array allocated on
741 * failure -- release it here */
742 free(clip->inDesc);
743 clip->inDesc = nullptr;
744 clip->inCount = 0;
745 return CHANNEL_RC_OK;
746 }
747 if (clip->inCount > CLIP_MAX_FILES)
748 clip->inCount = CLIP_MAX_FILES;
749 clip->inIdx = 0;
750 clip->inSize = clip->inGot = clip->inTotal = 0;
751 clip->inFile = nullptr;
752 pull_skip_dirs(clip);
753 if (clip->inIdx < clip->inCount)
754 (void)pull_step(clip);
755 else
756 {
757 /* only directory descriptors (e.g. empty folders): nothing to
758 * stream, still place the materialized tree on the clipboard */
759 finish_pull(clip);
760 pull_abort(clip);
761 }
762 return CHANNEL_RC_OK;
763 }
764 return CHANNEL_RC_OK;
765}
766
767/* The client pulls data we announced (text / file descriptor list / drop
768 * effect). */
769static UINT srv_client_format_data_request(CliprdrServerContext* ctx,
771{
772 WINPR_ASSERT(ctx);
773 WINPR_ASSERT(req);
774
775 ShadowClip* clip = clipOf(ctx);
776 CLIPRDR_FORMAT_DATA_RESPONSE resp = { .common = { .msgType = CB_FORMAT_DATA_RESPONSE,
777 .msgFlags = CB_RESPONSE_FAIL,
778 .dataLen = 0 },
779 .requestedFormatData = nullptr };
780 BYTE* buf = nullptr;
781 if (req->requestedFormatId == CF_UNICODETEXT || req->requestedFormatId == CF_TEXT)
782 {
783 if (open_clipboard_retry())
784 {
785 HANDLE h = nullptr;
786 h = GetClipboardData(CF_UNICODETEXT);
787 if (h)
788 {
789 const WCHAR* s = nullptr;
790 s = (const WCHAR*)GlobalLock(h);
791 if (s)
792 {
793 const SIZE_T len = (wcslen(s) + 1) * sizeof(WCHAR);
794 buf = (BYTE*)malloc(len);
795 if (buf)
796 CopyMemory(buf, s, len);
797 }
798 GlobalUnlock(h);
799 }
800 CloseClipboard();
801 if (buf)
802 {
803 resp.common.msgFlags = CB_RESPONSE_OK;
804 resp.common.dataLen = (UINT32)((wcslen((const WCHAR*)buf) + 1) * sizeof(WCHAR));
805 resp.requestedFormatData = buf;
806 }
807 }
808 }
809 else if (req->requestedFormatId == clip->fgdId)
810 {
811 UINT32 len = 0;
812 BYTE* data = nullptr;
813 EnterCriticalSection(&clip->lock);
814 FILEDESCRIPTORW* desc = (FILEDESCRIPTORW*)calloc(clip->nFiles, sizeof(FILEDESCRIPTORW));
815 if (desc)
816 {
817 for (UINT32 i = 0; i < clip->nFiles; ++i)
818 {
819 const BOOL isDir = (clip->files[i].attrs & FILE_ATTRIBUTE_DIRECTORY) != 0;
820 desc[i].dwFlags = FD_ATTRIBUTES | FD_PROGRESSUI | (isDir ? 0 : FD_FILESIZE);
821 desc[i].dwFileAttributes = clip->files[i].attrs;
822 desc[i].nFileSizeHigh = (DWORD)(clip->files[i].size >> 32);
823 desc[i].nFileSizeLow = (DWORD)(clip->files[i].size & 0xFFFFFFFFu);
824 wcscpy_s(desc[i].cFileName, sizeof(desc[i].cFileName) / sizeof(WCHAR),
825 clip->files[i].rel);
826 }
827 if (cliprdr_serialize_file_list(desc, clip->nFiles, &data, &len) == NO_ERROR)
828 {
829 buf = data;
830 resp.common.msgFlags = CB_RESPONSE_OK;
831 resp.common.dataLen = len;
832 resp.requestedFormatData = buf;
833 }
834 free(desc);
835 }
836 LeaveCriticalSection(&clip->lock);
837 }
838 else if (req->requestedFormatId == clip->dropId)
839 {
840 buf = (BYTE*)malloc(sizeof(DWORD));
841 if (buf)
842 {
843 *(DWORD*)buf = DROPEFFECT_COPY;
844 resp.common.msgFlags = CB_RESPONSE_OK;
845 resp.common.dataLen = sizeof(DWORD);
846 resp.requestedFormatData = buf;
847 }
848 }
849 cliprdr_check_send(ctx->ServerFormatDataResponse(ctx, &resp), "ServerFormatDataResponse");
850 free(buf);
851 return CHANNEL_RC_OK;
852}
853
854/* The client pulls SIZE/RANGE chunks of the files we announced. */
855static UINT srv_client_file_contents_request(CliprdrServerContext* ctx,
857{
858 WINPR_ASSERT(ctx);
859 WINPR_ASSERT(req);
860
861 ShadowClip* clip = clipOf(ctx);
862 CLIPRDR_FILE_CONTENTS_RESPONSE resp = { .common = { .msgType = CB_FILECONTENTS_RESPONSE,
863 .msgFlags = CB_RESPONSE_FAIL,
864 .dataLen = 0 },
865 .streamId = req->streamId,
866 .cbRequested = 0,
867 .requestedData = nullptr };
868 BYTE* buf = nullptr;
869 EnterCriticalSection(&clip->lock);
870 if (req->listIndex < clip->nFiles && clip->files)
871 {
872 ClipFile* cf = &clip->files[req->listIndex];
873 if (req->dwFlags & FILECONTENTS_SIZE)
874 {
875 buf = (BYTE*)malloc(8);
876 if (buf)
877 {
878 *(UINT64*)buf = cf->size;
879 resp.common.msgFlags = CB_RESPONSE_OK;
880 resp.common.dataLen = 8;
881 resp.cbRequested = 8;
882 resp.requestedData = buf;
883 }
884 }
885 else if (req->dwFlags & FILECONTENTS_RANGE)
886 {
887 HANDLE f = CreateFileW(cf->path, GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE,
888 nullptr, OPEN_EXISTING, 0, nullptr);
889 if (f != INVALID_HANDLE_VALUE)
890 {
891 LARGE_INTEGER off;
892 off.QuadPart = ((LONGLONG)req->nPositionHigh << 32) | req->nPositionLow;
893 SetFilePointerEx(f, off, nullptr, FILE_BEGIN);
894 buf = (BYTE*)malloc(req->cbRequested);
895 DWORD rd = 0;
896 if (buf && ReadFile(f, buf, req->cbRequested, &rd, nullptr) && rd)
897 {
898 resp.common.msgFlags = CB_RESPONSE_OK;
899 resp.common.dataLen = rd;
900 resp.cbRequested = rd;
901 resp.requestedData = buf;
902 }
903 CloseHandle(f);
904 }
905 }
906 }
907 LeaveCriticalSection(&clip->lock);
908 cliprdr_check_send(ctx->ServerFileContentsResponse(ctx, &resp), "ServerFileContentsResponse");
909 free(buf);
910 return CHANNEL_RC_OK;
911}
912
913static UINT srv_client_file_contents_response(CliprdrServerContext* ctx,
915{
916 WINPR_ASSERT(ctx);
917 WINPR_ASSERT(r);
918
919 on_client_file_contents(clipOf(ctx), r);
920 return CHANNEL_RC_OK;
921}
922
923static UINT srv_client_capabilities(CliprdrServerContext* ctx, const CLIPRDR_CAPABILITIES* caps)
924{
925 WLog_INFO(TAG, "client cliprdr caps received (%u sets)", caps ? caps->cCapabilitiesSets : 0);
926 return CHANNEL_RC_OK;
927}
928
929static UINT srv_monitor_ready(CliprdrServerContext* ctx, const CLIPRDR_MONITOR_READY* ready)
930{
931 WINPR_ASSERT(ctx);
932
933 ShadowClip* clip = clipOf(ctx);
934 WLog_INFO(TAG, "cliprdr ready -- announcing current remote clipboard");
935 announce_local(clip);
936 return CHANNEL_RC_OK;
937}
938
939static UINT srv_temp_directory(CliprdrServerContext* ctx, const CLIPRDR_TEMP_DIRECTORY* td)
940{
941 WINPR_ASSERT(ctx);
942 WINPR_ASSERT(td);
943
944 WLog_DBG(TAG, "client temp dir: %s", td->szTempDir);
945 return CHANNEL_RC_OK;
946}
947
948/* ---------- clipboard watcher thread (owns the message-only HWND) ---------- */
949
950static LRESULT CALLBACK clip_wndproc(HWND h, UINT m, WPARAM w, LPARAM l)
951{
952 if (m == WM_CLIPBOARDUPDATE)
953 {
954 ShadowClip* clip = nullptr;
955 clip = (ShadowClip*)GetWindowLongPtrW(h, GWLP_USERDATA);
956 if (clip)
957 {
958 if (InterlockedDecrement(&clip->suppress) >= 0)
959 return 0; /* our own write, skip to prevent an echo */
960 InterlockedExchange(&clip->suppress, 0);
961 if (WaitForSingleObject(clip->stopEvt, 0) != WAIT_OBJECT_0)
962 announce_local(clip);
963 }
964 return 0;
965 }
966 return DefWindowProcW(h, m, w, l);
967}
968
969static DWORD WINAPI clip_watch_thread(LPVOID arg)
970{
971 ShadowClip* clip = (ShadowClip*)arg;
972 WINPR_ASSERT(clip);
973
974 WNDCLASSW wc = WINPR_C_ARRAY_INIT;
975 wc.lpfnWndProc = clip_wndproc;
976 wc.hInstance = GetModuleHandleW(nullptr);
977 wc.lpszClassName = L"FreeRDPShadowClip";
978 RegisterClassW(&wc);
979 clip->hwnd = CreateWindowExW(0, wc.lpszClassName, L"", 0, 0, 0, 0, 0, HWND_MESSAGE, nullptr,
980 wc.hInstance, nullptr);
981 if (clip->hwnd)
982 {
983 SetWindowLongPtrW(clip->hwnd, GWLP_USERDATA, (LONG_PTR)clip);
984 AddClipboardFormatListener(clip->hwnd);
985 }
986 SetEvent(clip->ready);
987 MSG msg = WINPR_C_ARRAY_INIT;
988 while (GetMessageW(&msg, nullptr, 0, 0) > 0)
989 {
990 if (WaitForSingleObject(clip->stopEvt, 0) == WAIT_OBJECT_0)
991 break;
992 DispatchMessageW(&msg);
993 }
994 if (clip->hwnd)
995 {
996 RemoveClipboardFormatListener(clip->hwnd);
997 DestroyWindow(clip->hwnd);
998 clip->hwnd = nullptr;
999 }
1000 return 0;
1001}
1002
1003/* ---------- lifecycle ---------- */
1004
1005/* TRUE when the client connects from this machine: its RDP clipboard is then
1006 * the very clipboard this bridge writes to, so announcing and pulling
1007 * degenerates into a ping-pong loop between the two copies of the clipboard
1008 * (a full loop was observed closing in ~250 ms). Such sessions get the
1009 * clipboard channel deactivated (per review discussion on #13955): there is
1010 * nothing to sync that the local clipboard does not already have. */
1011static BOOL clip_client_is_local(rdpShadowClient* client)
1012{
1013 WINPR_ASSERT(client);
1014 const rdpSettings* settings = client->context.settings;
1015 WINPR_ASSERT(settings);
1016
1017 const char* addr = freerdp_settings_get_string(settings, FreeRDP_ClientAddress);
1018 if (!addr)
1019 return FALSE;
1020
1021 if (_stricmp(addr, "localhost") == 0 || strncmp(addr, "127.", 4) == 0 ||
1022 strcmp(addr, "::1") == 0)
1023 return TRUE;
1024
1025 /* Literal addresses only: a hostname client is assumed remote. */
1026 union
1027 {
1028 struct sockaddr_in v4;
1029 struct sockaddr_in6 v6;
1030 } peer = { 0 };
1031 size_t peerLen = 0;
1032 if (strchr(addr, ':'))
1033 {
1034 if (inet_pton(AF_INET6, addr, &peer.v6.sin6_addr) == 1)
1035 peerLen = sizeof(struct sockaddr_in6);
1036 }
1037 else if (inet_pton(AF_INET, addr, &peer.v4.sin_addr) == 1)
1038 peerLen = sizeof(struct sockaddr_in);
1039
1040 if (peerLen == 0)
1041 return FALSE;
1042
1043 /* Walk the local interface addresses; a match means the client sits on
1044 * the same host. */
1045 ULONG size = 15 * 1024;
1046 PIP_ADAPTER_ADDRESSES adapters = nullptr;
1047 for (int attempt = 0; attempt < 3; ++attempt)
1048 {
1049 adapters = (PIP_ADAPTER_ADDRESSES)malloc(size);
1050 if (!adapters)
1051 return FALSE;
1052 ULONG rc = 0;
1053 rc = GetAdaptersAddresses(
1054 AF_UNSPEC, GAA_FLAG_SKIP_ANYCAST | GAA_FLAG_SKIP_MULTICAST | GAA_FLAG_SKIP_DNS_SERVER,
1055 nullptr, adapters, &size);
1056 if (rc == ERROR_SUCCESS)
1057 break;
1058 free(adapters);
1059 adapters = nullptr;
1060 if (rc != ERROR_BUFFER_OVERFLOW)
1061 return FALSE;
1062 }
1063 if (!adapters)
1064 return FALSE;
1065
1066 BOOL local = FALSE;
1067 for (PIP_ADAPTER_ADDRESSES cur = adapters; cur && !local; cur = cur->Next)
1068 {
1069 for (PIP_ADAPTER_UNICAST_ADDRESS u = cur->FirstUnicastAddress; u && !local; u = u->Next)
1070 {
1071 const struct sockaddr* sa = u->Address.lpSockaddr;
1072 if (!sa)
1073 continue;
1074 if ((sa->sa_family == AF_INET) && (peerLen == sizeof(struct sockaddr_in)))
1075 {
1076 const struct sockaddr_in* s4 = (const struct sockaddr_in*)sa;
1077 if (memcmp(&s4->sin_addr, &peer.v4.sin_addr, sizeof(struct in_addr)) == 0)
1078 local = TRUE;
1079 }
1080 else if ((sa->sa_family == AF_INET6) && (peerLen == sizeof(struct sockaddr_in6)))
1081 {
1082 const struct sockaddr_in6* s6 = (const struct sockaddr_in6*)sa;
1083 if (memcmp(&s6->sin6_addr, &peer.v6.sin6_addr, sizeof(struct in6_addr)) == 0)
1084 local = TRUE;
1085 }
1086 }
1087 }
1088 free(adapters);
1089 return local;
1090}
1091
1092int win_shadow_cliprdr_init(rdpShadowClient* client)
1093{
1094 WINPR_ASSERT(client);
1095
1096 /* Same-machine session: deactivate the clipboard channel instead of
1097 * bridging a clipboard onto itself. Connecting without the cliprdr
1098 * channel still works (the join check below stays in place). */
1099 if (clip_client_is_local(client))
1100 {
1101 WLog_INFO(TAG, "client %s is local: clipboard bridge deactivated",
1102 freerdp_settings_get_string(client->context.settings, FreeRDP_ClientAddress));
1103 return 0;
1104 }
1105
1106 ShadowClip* clip = (ShadowClip*)calloc(1, sizeof(ShadowClip));
1107 if (!clip)
1108 return -1;
1109 clip->client = client;
1110 clip->ctx = cliprdr_server_context_new(client->vcm);
1111 if (!clip->ctx)
1112 {
1113 free(clip);
1114 return -1;
1115 }
1116 clip->ctx->custom = clip;
1117 clip->ctx->rdpcontext = &client->context;
1118 clip->ctx->useLongFormatNames = TRUE;
1119 clip->ctx->streamFileClipEnabled = TRUE;
1120 clip->ctx->hasHugeFileSupport = TRUE;
1121 clip->ctx->ClientCapabilities = srv_client_capabilities;
1122 clip->ctx->MonitorReady = srv_monitor_ready;
1123 clip->ctx->TempDirectory = srv_temp_directory;
1124 clip->ctx->ClientFormatList = srv_client_format_list;
1125 clip->ctx->ClientFormatDataRequest = srv_client_format_data_request;
1126 clip->ctx->ClientFormatDataResponse = srv_client_format_data_response;
1127 clip->ctx->ClientFileContentsRequest = srv_client_file_contents_request;
1128 clip->ctx->ClientFileContentsResponse = srv_client_file_contents_response;
1129
1130 InitializeCriticalSection(&clip->lock);
1131 clip->nextFmt = FMT_BASE;
1132 clip->fgdId = clip->nextFmt++;
1133 clip->contentsId = clip->nextFmt++;
1134 clip->dropId = clip->nextFmt++;
1135 clip->inFile = nullptr;
1136 clip->ready = CreateEventW(nullptr, TRUE, FALSE, nullptr);
1137 clip->stopEvt = CreateEventW(nullptr, TRUE, FALSE, nullptr);
1138
1139 /* stageDir: %TEMP%\freerdp-shadow-clip<pid> -- both levels must be
1140 * created explicitly, CreateDirectoryW does not fill in intermediates.
1141 * The directory is per-process: with several concurrent viewers pulls
1142 * race for it (deliberate last-writer-wins, see #13891). */
1143 DWORD tl = 0;
1144 tl = GetTempPathW(MAX_PATH, clip->stageDir);
1145 if (tl && tl < MAX_PATH - 32)
1146 {
1147 WCHAR parent[MAX_PATH];
1148 _snwprintf_s(parent, MAX_PATH, _TRUNCATE, L"%sfreerdp-shadow-clip", clip->stageDir);
1149 CreateDirectoryW(parent, nullptr);
1150 _snwprintf_s(clip->stageDir + wcslen(clip->stageDir), MAX_PATH - wcslen(clip->stageDir),
1151 _TRUNCATE, L"freerdp-shadow-clip\\%lu", GetCurrentProcessId());
1152 CreateDirectoryW(clip->stageDir, nullptr);
1153 }
1154
1155 client->cliprdr = clip->ctx;
1156
1157 const UINT rc = clip->ctx->Start(clip->ctx);
1158 if (rc != CHANNEL_RC_OK)
1159 {
1160 win_shadow_cliprdr_uninit(client);
1161 return -1;
1162 }
1163 clip->thread = CreateThread(nullptr, 0, clip_watch_thread, clip, 0, nullptr);
1164 WLog_INFO(TAG, "cliprdr channel mounted (text+file sync enabled)");
1165 return 1;
1166}
1167
1168void win_shadow_cliprdr_uninit(rdpShadowClient* client)
1169{
1170 WINPR_ASSERT(client);
1171 ShadowClip* clip = client->cliprdr ? clipOf(client->cliprdr) : nullptr;
1172 if (clip)
1173 {
1174 if (clip->stopEvt)
1175 SetEvent(clip->stopEvt);
1176 if (clip->hwnd)
1177 PostMessageW(clip->hwnd, WM_QUIT, 0, 0);
1178 if (clip->thread)
1179 {
1180 WaitForSingleObject(clip->thread, 3000);
1181 CloseHandle(clip->thread);
1182 }
1183 pull_abort(clip);
1184 free(clip->files);
1185 if (clip->ready)
1186 CloseHandle(clip->ready);
1187 if (clip->stopEvt)
1188 CloseHandle(clip->stopEvt);
1189 DeleteCriticalSection(&clip->lock);
1190 free(clip);
1191 }
1192 if (client->cliprdr)
1193 {
1194 client->cliprdr->Stop(client->cliprdr);
1195 cliprdr_server_context_free(client->cliprdr);
1196 client->cliprdr = nullptr;
1197 }
1198}
1199
1200#else /* !CHANNEL_CLIPRDR_SERVER */
1201
1202/* The cliprdr server channel is not built: no clipboard support. The call
1203 * sites in win_shadow.c are guarded by the same define, so these stubs are
1204 * not reached in practice. */
1205int win_shadow_cliprdr_init(rdpShadowClient* client)
1206{
1207 WINPR_ASSERT(client);
1208 return 0;
1209}
1210
1211void win_shadow_cliprdr_uninit(rdpShadowClient* client)
1212{
1213 WINPR_ASSERT(client);
1214}
1215
1216#endif /* CHANNEL_CLIPRDR_SERVER */
WINPR_ATTR_NODISCARD FREERDP_API const char * freerdp_settings_get_string(const rdpSettings *settings, FreeRDP_Settings_Keys_String id)
Returns a immutable string settings value.