51#include <winpr/assert.h>
55#define SERVICE_NAME L"FreeRDPShadow"
56#define SERVICE_DISPLAY L"FreeRDP shadow server"
57#define SERVICE_ABOUT \
58 L"Shares the console session's screen, logon and lock screens included, over RDP " \
59 L"(freerdp-shadow-cli)"
60#define SHADOW_CLI L"freerdp-shadow-cli.exe"
61#define NO_SESSION 0xFFFFFFFF
63#define LONGEST_PATH 32767
65static SERVICE_STATUS_HANDLE g_statusHandle =
nullptr;
66static SERVICE_STATUS g_status = WINPR_C_ARRAY_INIT;
67static HANDLE g_stopEvent =
nullptr;
68static HANDLE g_sessionEvent =
nullptr;
69static WCHAR* g_logDir =
nullptr;
70static WCHAR* g_options =
nullptr;
76static WCHAR* wcs_vasprintf(
const WCHAR* format, va_list args)
78 va_list copy = WINPR_C_ARRAY_INIT;
83 const int length = _vscwprintf(format, copy);
88 WCHAR* str = calloc((
size_t)length + 1,
sizeof(WCHAR));
93 const int written = _vsnwprintf_s(str, (
size_t)length + 1, _TRUNCATE, format, copy);
95 if (written != length)
105static WCHAR* wcs_asprintf(
const WCHAR* format, ...)
107 va_list args = WINPR_C_ARRAY_INIT;
109 va_start(args, format);
110 WCHAR* str = wcs_vasprintf(format, args);
117static void say(
const WCHAR* format, ...)
120 va_list args = WINPR_C_ARRAY_INIT;
122 WINPR_ASSERT(format);
125 va_start(args, format);
126 WCHAR* message = wcs_vasprintf(format, args);
130 wcs_asprintf(L
"%04u-%02u-%02u %02u:%02u:%02u %ls", now.wYear, now.wMonth, now.wDay,
131 now.wHour, now.wMinute, now.wSecond, message ? message : format);
135 OutputDebugStringW(text);
136 OutputDebugStringW(L
"\n");
138 WCHAR* path = g_logDir ? wcs_asprintf(L
"%ls\\service.log", g_logDir) : nullptr;
140 if (path && (_wfopen_s(&fp, path, L
"a, ccs=UTF-8") == 0) && fp)
142 (void)fwprintf(fp, L
"%ls\n", text);
154static BOOL append_quoted(WCHAR* line,
size_t size,
const WCHAR* arg)
159 size_t len = wcslen(line);
160 const BOOL quote = (arg[0] == L
'\0') || (wcspbrk(arg, L
" \t\"") !=
nullptr);
165 if (len + 1 >= size) \
174 for (
const WCHAR* p = arg; *p; p++)
181 for (
const WCHAR* p = arg;; p++)
183 size_t backslashes = 0;
192 for (
size_t i = 0; i < backslashes * 2; i++)
198 for (
size_t i = 0; i < backslashes * 2 + 1; i++)
204 for (
size_t i = 0; i < backslashes; i++)
218static WCHAR* program_path(
void)
220 for (DWORD size = 256; size <= LONGEST_PATH + 1; size *= 2)
222 WCHAR* path = calloc(size,
sizeof(WCHAR));
225 const DWORD n = GetModuleFileNameW(
nullptr, path, size);
226 if ((n > 0) && (n < size))
237static WCHAR* program_folder(
void)
239 WCHAR* folder = program_path();
242 WCHAR* slash = wcsrchr(folder, L
'\\');
254static WCHAR* full_path(
const WCHAR* name)
258 const DWORD size = GetFullPathNameW(name, 0,
nullptr,
nullptr);
261 WCHAR* path = calloc(size,
sizeof(WCHAR));
264 const DWORD n = GetFullPathNameW(name, size, path,
nullptr);
265 if ((n == 0) || (n >= size))
276static HANDLE start_shadow(DWORD session)
278 HANDLE self =
nullptr;
279 HANDLE token =
nullptr;
280 HANDLE process =
nullptr;
281 WCHAR* folder =
nullptr;
282 WCHAR* exe =
nullptr;
283 static WCHAR line[32768];
287 folder = program_folder();
290 say(L
"the folder of this program: 0x%08lX", GetLastError());
293 exe = wcs_asprintf(L
"%ls%ls", folder, SHADOW_CLI);
297 if (!append_quoted(line, ARRAYSIZE(line), exe))
299 if (g_options && g_options[0])
301 if (wcslen(line) + 1 + wcslen(g_options) + 1 > ARRAYSIZE(line))
303 wcscat_s(line, ARRAYSIZE(line), L
" ");
304 wcscat_s(line, ARRAYSIZE(line), g_options);
309 if (!OpenProcessToken(GetCurrentProcess(),
310 TOKEN_DUPLICATE | TOKEN_QUERY | TOKEN_ASSIGN_PRIMARY |
311 TOKEN_ADJUST_DEFAULT | TOKEN_ADJUST_SESSIONID,
314 say(L
"OpenProcessToken failed: 0x%08lX", GetLastError());
317 if (!DuplicateTokenEx(self, MAXIMUM_ALLOWED,
nullptr, SecurityImpersonation, TokenPrimary,
320 say(L
"DuplicateTokenEx failed: 0x%08lX", GetLastError());
323 if (!SetTokenInformation(token, TokenSessionId, &session,
sizeof(session)))
325 say(L
"SetTokenInformation(TokenSessionId %lu) failed: 0x%08lX", session, GetLastError());
334 WCHAR* name = wcs_asprintf(L
"shadow-%lu-%04u%02u%02u-%02u%02u%02u.log", session, now.wYear,
335 now.wMonth, now.wDay, now.wHour, now.wMinute, now.wSecond);
336 if (!name || !SetEnvironmentVariableW(L
"WLOG_APPENDER", L
"FILE") ||
337 !SetEnvironmentVariableW(L
"WLOG_FILEAPPENDER_OUTPUT_FILE_PATH", g_logDir) ||
338 !SetEnvironmentVariableW(L
"WLOG_FILEAPPENDER_OUTPUT_FILE_NAME", name))
339 say(L
"the shadow server's log file is not set: 0x%08lX", GetLastError());
344 si.lpDesktop = (LPWSTR)L
"winsta0\\default";
345 if (!CreateProcessAsUserW(token, exe, line,
nullptr,
nullptr, FALSE,
346 CREATE_NO_WINDOW | CREATE_UNICODE_ENVIRONMENT,
nullptr, folder, &si,
349 say(L
"CreateProcessAsUserW(%ls) failed: 0x%08lX", exe, GetLastError());
352 (void)CloseHandle(pi.hThread);
353 process = pi.hProcess;
354 say(L
"started %ls on session %lu, process %lu", SHADOW_CLI, session, pi.dwProcessId);
358 (void)CloseHandle(token);
360 (void)CloseHandle(self);
366static void stop_shadow(HANDLE* process)
368 WINPR_ASSERT(process);
372 if (WaitForSingleObject(*process, 0) == WAIT_TIMEOUT)
374 if (!TerminateProcess(*process, 0))
375 say(L
"TerminateProcess(%ls) failed: 0x%08lX", SHADOW_CLI, GetLastError());
376 if (WaitForSingleObject(*process, 5000) != WAIT_OBJECT_0)
377 say(L
"%ls did not end within 5 s", SHADOW_CLI);
379 (void)CloseHandle(*process);
384static void send_sas(
void)
386 typedef VOID(WINAPI * fnSendSAS)(BOOL AsUser);
387 HMODULE sas = LoadLibraryW(L
"sas.dll");
388 fnSendSAS sendSAS = sas ? (fnSendSAS)(
void*)GetProcAddress(sas,
"SendSAS") : nullptr;
392 say(L
"Ctrl+Alt+Del: no SendSAS in sas.dll");
394 (void)FreeLibrary(sas);
398 say(L
"Ctrl+Alt+Del: secure attention sequence sent (Windows ignores it unless "
399 L
"SoftwareSASGeneration allows services: install --allow-sas)");
400 (void)FreeLibrary(sas);
405static void serve(
void)
407 HANDLE process =
nullptr;
408 DWORD session = NO_SESSION;
410 HANDLE sasEvent = CreateEventW(
nullptr, FALSE, FALSE, WIN_SHADOW_SAS_EVENT);
413 say(L
"CreateEvent(%ls) failed: 0x%08lX; Ctrl+Alt+Del will not work", WIN_SHADOW_SAS_EVENT,
418 const DWORD console = WTSGetActiveConsoleSessionId();
420 if (process && (console != session))
422 say(L
"the console moved from session %lu to %lu", session, console);
423 stop_shadow(&process);
425 if (!process && (console != NO_SESSION))
427 process = start_shadow(console);
433 HANDLE events[4] = { g_stopEvent, g_sessionEvent };
436 events[count++] = sasEvent;
437 const DWORD processIndex = count;
439 events[count++] = process;
442 const DWORD rc = WaitForMultipleObjects(count, events, FALSE, 2000);
443 if (rc == WAIT_OBJECT_0)
445 if (sasEvent && (rc == WAIT_OBJECT_0 + 2))
447 if (process && (rc == WAIT_OBJECT_0 + processIndex))
450 if (GetExitCodeProcess(process, &code))
451 say(L
"%ls on session %lu ended (exit code %lu)", SHADOW_CLI, session, code);
453 say(L
"%ls on session %lu ended (exit code unknown: 0x%08lX)", SHADOW_CLI, session,
455 (void)CloseHandle(process);
458 if (WaitForSingleObject(g_stopEvent, restarts > 3 ? 5000 : 1000) == WAIT_OBJECT_0)
463 stop_shadow(&process);
465 (void)CloseHandle(sasEvent);
470static void set_state(DWORD state)
472 g_status.dwServiceType = SERVICE_WIN32_OWN_PROCESS;
473 g_status.dwCurrentState = state;
474 g_status.dwControlsAccepted =
475 (state == SERVICE_RUNNING)
476 ? (SERVICE_ACCEPT_STOP | SERVICE_ACCEPT_SHUTDOWN | SERVICE_ACCEPT_SESSIONCHANGE)
478 g_status.dwWin32ExitCode = NO_ERROR;
479 g_status.dwWaitHint = (state == SERVICE_RUNNING || state == SERVICE_STOPPED) ? 0 : 10000;
480 if (!SetServiceStatus(g_statusHandle, &g_status))
481 say(L
"SetServiceStatus(%lu) failed: 0x%08lX", state, GetLastError());
484static DWORD WINAPI handler(DWORD control, DWORD eventType, LPVOID eventData, LPVOID context)
490 case SERVICE_CONTROL_STOP:
491 case SERVICE_CONTROL_SHUTDOWN:
492 set_state(SERVICE_STOP_PENDING);
493 if (!SetEvent(g_stopEvent))
494 say(L
"SetEvent(stop) failed: 0x%08lX", GetLastError());
496 case SERVICE_CONTROL_SESSIONCHANGE:
499 say(L
"session change %lu", eventType);
500 if (!SetEvent(g_sessionEvent))
501 say(L
"SetEvent(session change) failed: 0x%08lX", GetLastError());
503 case SERVICE_CONTROL_INTERROGATE:
506 return ERROR_CALL_NOT_IMPLEMENTED;
510static VOID WINAPI service_main(DWORD argc, LPWSTR* argv)
514 g_statusHandle = RegisterServiceCtrlHandlerExW(SERVICE_NAME, handler,
nullptr);
517 set_state(SERVICE_START_PENDING);
518 say(L
"service started; options: %ls", g_options ? g_options : L
"");
519 set_state(SERVICE_RUNNING);
521 say(L
"service stopped");
522 set_state(SERVICE_STOPPED);
527#define SAS_POLICY_KEY L"SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Policies\\System"
528#define SAS_POLICY L"SoftwareSASGeneration"
531#define SERVICE_KEY L"SYSTEM\\CurrentControlSet\\Services\\" SERVICE_NAME
532#define SAS_BEFORE L"SoftwareSASGenerationBefore"
533#define SAS_WAS_UNSET 0xFFFFFFFFu
539static BOOL allow_sas(
void)
541 DWORD before = SAS_WAS_UNSET;
542 DWORD size =
sizeof(before);
543 const LSTATUS read = RegGetValueW(HKEY_LOCAL_MACHINE, SAS_POLICY_KEY, SAS_POLICY,
544 RRF_RT_REG_DWORD,
nullptr, &before, &size);
545 if (read != ERROR_SUCCESS)
546 before = SAS_WAS_UNSET;
547 if ((before != SAS_WAS_UNSET) && ((before & 1) != 0))
549 wprintf(L
"SoftwareSASGeneration = %lu already lets services send Ctrl+Alt+Del\n",
550 (
unsigned long)before);
556 if (RegGetValueW(HKEY_LOCAL_MACHINE, SERVICE_KEY, SAS_BEFORE, RRF_RT_REG_DWORD,
nullptr, &kept,
557 &size) != ERROR_SUCCESS)
559 const LSTATUS rc = RegSetKeyValueW(HKEY_LOCAL_MACHINE, SERVICE_KEY, SAS_BEFORE, REG_DWORD,
560 &before,
sizeof(before));
561 if (rc != ERROR_SUCCESS)
563 fwprintf(stderr, L
"%ls: 0x%08lX\n", SAS_BEFORE, (
unsigned long)rc);
569 const DWORD services = (before == SAS_WAS_UNSET) ? 1 : (before | 1);
570 const LSTATUS rc = RegSetKeyValueW(HKEY_LOCAL_MACHINE, SAS_POLICY_KEY, SAS_POLICY, REG_DWORD,
571 &services,
sizeof(services));
572 if (rc != ERROR_SUCCESS)
574 fwprintf(stderr, L
"SoftwareSASGeneration: 0x%08lX\n", (
unsigned long)rc);
578 L
"SoftwareSASGeneration = %lu: services may send Ctrl+Alt+Del (uninstall restores it)\n",
579 (
unsigned long)services);
584static void restore_sas(
void)
587 DWORD size =
sizeof(before);
588 if (RegGetValueW(HKEY_LOCAL_MACHINE, SERVICE_KEY, SAS_BEFORE, RRF_RT_REG_DWORD,
nullptr,
589 &before, &size) != ERROR_SUCCESS)
592 LSTATUS rc = ERROR_SUCCESS;
593 if (before == SAS_WAS_UNSET)
595 rc = RegDeleteKeyValueW(HKEY_LOCAL_MACHINE, SAS_POLICY_KEY, SAS_POLICY);
596 if (rc == ERROR_FILE_NOT_FOUND)
600 rc = RegSetKeyValueW(HKEY_LOCAL_MACHINE, SAS_POLICY_KEY, SAS_POLICY, REG_DWORD, &before,
602 if (rc != ERROR_SUCCESS)
603 fwprintf(stderr, L
"SoftwareSASGeneration: 0x%08lX, not restored\n", (
unsigned long)rc);
604 else if (before == SAS_WAS_UNSET)
605 wprintf(L
"SoftwareSASGeneration removed again (it was not set before install)\n");
607 wprintf(L
"SoftwareSASGeneration = %lu again\n", (
unsigned long)before);
611static int install(BOOL sas)
613 static WCHAR image[32768];
614 SC_HANDLE scm =
nullptr;
615 SC_HANDLE service =
nullptr;
618 WCHAR* self = program_path();
621 fwprintf(stderr, L
"GetModuleFileName: 0x%08lX\n", GetLastError());
625 if (!append_quoted(image, ARRAYSIZE(image), self) ||
626 !append_quoted(image, ARRAYSIZE(image), L
"run"))
628 if (g_logDir && (!append_quoted(image, ARRAYSIZE(image), L
"--log") ||
629 !append_quoted(image, ARRAYSIZE(image), g_logDir)))
631 if (!append_quoted(image, ARRAYSIZE(image), L
"--"))
633 if (g_options && g_options[0])
635 if (wcslen(image) + 1 + wcslen(g_options) + 1 > ARRAYSIZE(image))
637 wcscat_s(image, ARRAYSIZE(image), L
" ");
638 wcscat_s(image, ARRAYSIZE(image), g_options);
641 scm = OpenSCManagerW(
nullptr,
nullptr, SC_MANAGER_CREATE_SERVICE | SC_MANAGER_CONNECT);
644 fwprintf(stderr, L
"OpenSCManager: 0x%08lX (an administrator's prompt is needed)\n",
648 service = CreateServiceW(scm, SERVICE_NAME, SERVICE_DISPLAY, SERVICE_ALL_ACCESS,
649 SERVICE_WIN32_OWN_PROCESS, SERVICE_AUTO_START, SERVICE_ERROR_NORMAL,
650 image,
nullptr,
nullptr,
nullptr,
nullptr,
nullptr);
651 if (!service && (GetLastError() == ERROR_SERVICE_EXISTS))
653 service = OpenServiceW(scm, SERVICE_NAME, SERVICE_ALL_ACCESS);
655 !ChangeServiceConfigW(service, SERVICE_NO_CHANGE, SERVICE_AUTO_START, SERVICE_NO_CHANGE,
656 image,
nullptr,
nullptr,
nullptr,
nullptr,
nullptr,
nullptr))
658 fwprintf(stderr, L
"ChangeServiceConfig: 0x%08lX\n", GetLastError());
664 fwprintf(stderr, L
"CreateService: 0x%08lX\n", GetLastError());
668 if (sas && !allow_sas())
671 SERVICE_DESCRIPTIONW about = { (LPWSTR)SERVICE_ABOUT };
672 if (!ChangeServiceConfig2W(service, SERVICE_CONFIG_DESCRIPTION, &about))
673 fwprintf(stderr, L
"warning: ChangeServiceConfig2(description): 0x%08lX\n", GetLastError());
675 SC_ACTION again[3] = { { SC_ACTION_RESTART, 5000 },
676 { SC_ACTION_RESTART, 5000 },
677 { SC_ACTION_RESTART, 5000 } };
678 SERVICE_FAILURE_ACTIONSW failure = { 86400,
nullptr,
nullptr, ARRAYSIZE(again), again };
679 if (!ChangeServiceConfig2W(service, SERVICE_CONFIG_FAILURE_ACTIONS, &failure))
680 fwprintf(stderr, L
"warning: ChangeServiceConfig2(failure actions): 0x%08lX\n",
683 if (!StartServiceW(service, 0,
nullptr) && (GetLastError() != ERROR_SERVICE_ALREADY_RUNNING))
685 fwprintf(stderr, L
"StartService: 0x%08lX\n", GetLastError());
688 wprintf(L
"%ls installed and started: %ls\n", SERVICE_NAME, image);
693 (void)CloseServiceHandle(service);
695 (void)CloseServiceHandle(scm);
701static int uninstall(
void)
703 SERVICE_STATUS status = WINPR_C_ARRAY_INIT;
704 SC_HANDLE scm = OpenSCManagerW(
nullptr,
nullptr, SC_MANAGER_CONNECT);
707 fwprintf(stderr, L
"OpenSCManager: 0x%08lX\n", GetLastError());
711 OpenServiceW(scm, SERVICE_NAME, SERVICE_STOP | DELETE | SERVICE_QUERY_STATUS);
714 fwprintf(stderr, L
"OpenService: 0x%08lX\n", GetLastError());
715 (void)CloseServiceHandle(scm);
718 if (!ControlService(service, SERVICE_CONTROL_STOP, &status))
720 const DWORD error = GetLastError();
721 if (error != ERROR_SERVICE_NOT_ACTIVE)
722 fwprintf(stderr, L
"warning: ControlService(stop): 0x%08lX\n", error);
725 i < 50 && QueryServiceStatus(service, &status) && status.dwCurrentState != SERVICE_STOPPED;
730 const BOOL deleted = DeleteService(service);
732 fwprintf(stderr, L
"DeleteService: 0x%08lX\n", GetLastError());
734 wprintf(L
"%ls stopped and removed\n", SERVICE_NAME);
735 (void)CloseServiceHandle(service);
736 (void)CloseServiceHandle(scm);
737 return deleted ? 0 : 1;
743static int usage(
void)
745 fwprintf(stderr, L
"usage: freerdp-shadow-service install [--log <dir>] [--allow-sas] -- "
746 L
"<freerdp-shadow-cli options>\n"
747 L
" freerdp-shadow-service uninstall\n"
748 L
" freerdp-shadow-service run [--log <dir>] -- <options>\n");
757 LPWSTR* argv = CommandLineToArgvW(GetCommandLineW(), &argc);
758 static WCHAR options[32768];
760 if (!argv || argc < 2)
766 const WCHAR* command = argv[1];
768 for (; i < argc; i++)
770 if (wcscmp(argv[i], L
"--") == 0)
775 if ((wcscmp(argv[i], L
"--log") == 0) && (i + 1 < argc))
778 g_logDir = full_path(argv[++i]);
784 if (!CreateDirectoryW(g_logDir,
nullptr))
786 const DWORD error = GetLastError();
787 if (error != ERROR_ALREADY_EXISTS)
788 fwprintf(stderr, L
"warning: CreateDirectory(%ls): 0x%08lX\n", g_logDir, error);
791 else if (wcscmp(argv[i], L
"--allow-sas") == 0)
800 for (; i < argc; i++)
802 if (!append_quoted(options, ARRAYSIZE(options), argv[i]))
810 if (wcscmp(command, L
"install") == 0)
812 else if (wcscmp(command, L
"uninstall") == 0)
814 else if (wcscmp(command, L
"run") != 0)
818 g_stopEvent = CreateEventW(
nullptr, TRUE, FALSE,
nullptr);
819 g_sessionEvent = CreateEventW(
nullptr, FALSE, FALSE,
nullptr);
820 if (!g_stopEvent || !g_sessionEvent)
822 SERVICE_TABLE_ENTRYW table[] = { { (LPWSTR)SERVICE_NAME, service_main },
823 {
nullptr,
nullptr } };
824 if (!StartServiceCtrlDispatcherW(table))
826 fwprintf(stderr, L
"run is for the service manager (install starts it): 0x%08lX\n",
837 (void)LocalFree((HLOCAL)argv);