FreeRDP
Loading...
Searching...
No Matches
aad_helper.c
1
20#include <stdlib.h>
21#include <string.h>
22#include <stdarg.h>
23
24#include <winpr/assert.h>
25#include <winpr/file.h>
26#include <winpr/handle.h>
27#include <winpr/json.h>
28#include <winpr/pipe.h>
29#include <winpr/string.h>
30#include <winpr/synch.h>
31#include <winpr/thread.h>
32#include <winpr/library.h>
33#include <winpr/path.h>
34
35#include <freerdp/build-config.h>
36#include <freerdp/utils/helpers.h>
37#include <freerdp/log.h>
38#include <freerdp/client/aad_helper.h>
39
40#define TAG CLIENT_TAG("common.aadauth")
41
42struct AadAuthHelper
43{
44 rdpClientContext* context;
45 HANDLE hCmdOutRead; /* parent's read end: helper -> FreeRDP responses/notifications */
46 HANDLE hCmdInWrite; /* parent's write end: FreeRDP -> helper requests */
47 HANDLE hProcess;
48 UINT32 nextId;
49
50 BYTE* buf;
51 size_t bufLen;
52
53 char* exe;
54 char* userAgent;
55};
56
57WINPR_ATTR_MALLOC(free, 1)
58static char* aad_auth_helper_detect_helper(void);
59
60/* ---- wire format helpers ------------------------------------------------------------- */
61WINPR_ATTR_MALLOC(free, 1)
62static char* build_hello_request(UINT32 id)
63{
64 WINPR_JSON* obj = WINPR_JSON_CreateObject();
65 if (!obj)
66 return nullptr;
67
68 BOOL ok = WINPR_JSON_AddStringToObject(obj, "jsonrpc", "2.0") &&
69 WINPR_JSON_AddIntegerToObject(obj, "id", id) &&
70 WINPR_JSON_AddStringToObject(obj, "method", "hello");
71
72 WINPR_JSON* params = ok ? WINPR_JSON_AddObjectToObject(obj, "params") : nullptr;
73 ok = ok && params && WINPR_JSON_AddIntegerToObject(params, "protocol_version", 1) &&
74 WINPR_JSON_AddStringToObject(params, "client", "freerdp");
75
76 char* str = ok ? WINPR_JSON_PrintUnformatted(obj) : nullptr;
78 return str;
79}
80
81WINPR_ATTR_MALLOC(free, 1)
82static char* build_navigate_request(UINT32 id, const char* title, const char* url,
83 const char* redirect_uri, const char* user_agent,
84 UINT32 timeout_ms)
85{
86 WINPR_JSON* obj = WINPR_JSON_CreateObject();
87 if (!obj)
88 return nullptr;
89
90 BOOL ok = WINPR_JSON_AddStringToObject(obj, "jsonrpc", "2.0") &&
91 WINPR_JSON_AddIntegerToObject(obj, "id", id) &&
92 WINPR_JSON_AddStringToObject(obj, "method", "navigate");
93
94 WINPR_JSON* params = ok ? WINPR_JSON_AddObjectToObject(obj, "params") : nullptr;
95 ok = ok && params && WINPR_JSON_AddStringToObject(params, "title", title) &&
96 WINPR_JSON_AddStringToObject(params, "url", url) &&
97 WINPR_JSON_AddStringToObject(params, "redirect_uri", redirect_uri) &&
98 WINPR_JSON_AddIntegerToObject(params, "timeout_ms", timeout_ms);
99 if (ok && user_agent && (user_agent[0] != '\0'))
100 ok = ok && WINPR_JSON_AddStringToObject(params, "user_agent", user_agent);
101
102 char* str = ok ? WINPR_JSON_PrintUnformatted(obj) : nullptr;
104 return str;
105}
106
107WINPR_ATTR_MALLOC(free, 1)
108static char* build_shutdown_request(UINT32 id)
109{
110 WINPR_JSON* obj = WINPR_JSON_CreateObject();
111 if (!obj)
112 return nullptr;
113
114 BOOL ok = WINPR_JSON_AddStringToObject(obj, "jsonrpc", "2.0") &&
115 WINPR_JSON_AddIntegerToObject(obj, "id", id) &&
116 WINPR_JSON_AddStringToObject(obj, "method", "shutdown");
117
118 char* str = ok ? WINPR_JSON_PrintUnformatted(obj) : nullptr;
120 return str;
121}
122
123WINPR_ATTR_MALLOC(free, 1)
124static char* build_exit_notification(void)
125{
126 WINPR_JSON* obj = WINPR_JSON_CreateObject();
127 if (!obj)
128 return nullptr;
129
130 BOOL ok = WINPR_JSON_AddStringToObject(obj, "jsonrpc", "2.0") &&
131 WINPR_JSON_AddStringToObject(obj, "method", "exit");
132
133 char* str = ok ? WINPR_JSON_PrintUnformatted(obj) : nullptr;
135 return str;
136}
137
138/* ---- transport: newline-delimited JSON over the helper's cmdIn/cmdOut pipes ----------- */
139WINPR_ATTR_NODISCARD
140static BOOL helper_write_line(AadAuthHelper* helper, const char* json)
141{
142 WINPR_ASSERT(helper);
143 WINPR_ASSERT(json);
144
145 const size_t len = strlen(json);
146 const size_t total = len + 1; /* trailing '\n' */
147 char* line = malloc(total);
148 if (!line)
149 return FALSE;
150 /* terminated with '\n', not '\0' - written as-is over the wire, never treated as a C string */
151 // NOLINTNEXTLINE(bugprone-not-null-terminated-result)
152 memcpy(line, json, len);
153 line[len] = '\n';
154
155 BOOL rc = TRUE;
156 size_t written = 0;
157 while (written < total)
158 {
159 DWORD dwWritten = 0;
160 if (!WriteFile(helper->hCmdInWrite, line + written, (DWORD)(total - written), &dwWritten,
161 nullptr) ||
162 (dwWritten == 0))
163 {
164 WLog_ERR(TAG, "aad-auth-helper: failed writing to helper");
165 rc = FALSE;
166 break;
167 }
168 written += dwWritten;
169 }
170
171 free(line);
172 return rc;
173}
174
176WINPR_ATTR_MALLOC(free, 1)
177static char* linebuf_extract(AadAuthHelper* helper)
178{
179 if (!helper->buf || !helper->bufLen)
180 return nullptr;
181
182 const BYTE* nl = memchr(helper->buf, '\n', helper->bufLen);
183 if (!nl)
184 return nullptr;
185
186 const size_t lineLen = (size_t)(nl - helper->buf);
187 char* line = malloc(lineLen + 1);
188 if (!line)
189 return nullptr;
190 memcpy(line, helper->buf, lineLen);
191 line[lineLen] = '\0';
192
193 const size_t consumed = lineLen + 1;
194 const size_t remaining = helper->bufLen - consumed;
195 memmove(helper->buf, helper->buf + consumed, remaining);
196 helper->bufLen = remaining;
197 return line;
198}
199
200WINPR_ATTR_MALLOC(free, 1)
201static char* helper_read_line(AadAuthHelper* helper)
202{
203 WINPR_ASSERT(helper);
204
205 char* line = linebuf_extract(helper);
206 if (line)
207 return line;
208
209 while (TRUE)
210 {
211 BYTE chunk[4096];
212 DWORD dwRead = 0;
213 if (!ReadFile(helper->hCmdOutRead, chunk, sizeof(chunk), &dwRead, nullptr) || (dwRead == 0))
214 {
215 WLog_ERR(TAG, "aad-auth-helper: helper pipe closed or read error");
216 return nullptr;
217 }
218
219 BYTE* nbuf = realloc(helper->buf, helper->bufLen + dwRead);
220 if (!nbuf)
221 return nullptr;
222 helper->buf = nbuf;
223 memcpy(helper->buf + helper->bufLen, chunk, dwRead);
224 helper->bufLen += dwRead;
225
226 line = linebuf_extract(helper);
227 if (line)
228 return line;
229 }
230}
231
235WINPR_ATTR_MALLOC(WINPR_JSON_Delete, 1)
236static WINPR_JSON* wait_for_response(AadAuthHelper* helper, UINT32 expectedId)
237{
238 while (TRUE)
239 {
240 char* line = helper_read_line(helper);
241 if (!line)
242 return nullptr;
243
244 WINPR_JSON* msg = WINPR_JSON_Parse(line);
245 free(line);
246 if (!msg)
247 {
248 WLog_WARN(TAG, "aad-auth-helper: ignoring malformed line from helper");
249 continue;
250 }
251
252 WINPR_JSON* method = WINPR_JSON_GetObjectItemCaseSensitive(msg, "method");
253 if (method && WINPR_JSON_IsString(method))
254 {
255 const char* m = WINPR_JSON_GetStringValue(method);
256 if (m && (strcmp(m, "log") == 0))
257 {
258 WINPR_JSON* params = WINPR_JSON_GetObjectItemCaseSensitive(msg, "params");
259 WINPR_JSON* message =
260 params ? WINPR_JSON_GetObjectItemCaseSensitive(params, "message") : nullptr;
261 const char* text = (message && WINPR_JSON_IsString(message))
263 : "";
264 WLog_INFO(TAG, "[helper] %s", text);
265 }
267 continue;
268 }
269
270 WINPR_JSON* id = WINPR_JSON_GetObjectItemCaseSensitive(msg, "id");
271 const double idValue = (id && WINPR_JSON_IsNumber(id)) ? WINPR_JSON_GetNumberValue(id) : 0;
272 if (!id || !WINPR_JSON_IsNumber(id) || ((UINT32)idValue != expectedId))
273 {
274 WLog_WARN(TAG, "aad-auth-helper: dropping response with unexpected id");
276 continue;
277 }
278
279 return msg;
280 }
281}
282
283static void updateBoolFromConfig(WINPR_JSON* obj, const char* what, BOOL* pVal)
284{
285 WINPR_ASSERT(obj);
286 WINPR_ASSERT(what);
287 WINPR_ASSERT(pVal);
288 WINPR_JSON* val = WINPR_JSON_GetObjectItemCaseSensitive(obj, what);
289 if (!val)
290 return;
291 if (!WINPR_JSON_IsBool(val))
292 return;
293 *pVal = WINPR_JSON_IsTrue(val);
294}
295
296static void updateStringFromConfig(WINPR_JSON* obj, const char* what, char** pVal)
297{
298 WINPR_ASSERT(obj);
299 WINPR_ASSERT(what);
300 WINPR_ASSERT(pVal);
301 WINPR_JSON* val = WINPR_JSON_GetObjectItemCaseSensitive(obj, what);
302 if (!val)
303 return;
304 if (!WINPR_JSON_IsString(val))
305 return;
306 free(*pVal);
307 *pVal = _strdup(WINPR_JSON_GetStringValue(val));
308}
309
310WINPR_ATTR_NODISCARD
311static BOOL aad_auth_helper_config(AadAuthHelper* helper)
312{
313 WINPR_ASSERT(helper);
314
315 BOOL useArg = TRUE;
316 BOOL useDetect = TRUE;
317 BOOL useUserConfig = TRUE;
318
319 const char config[] = "freerdp-client-aad.json";
320 WINPR_JSON* sys = freerdp_GetJSONConfigFile(TRUE, config);
321 if (sys)
322 {
323 updateBoolFromConfig(sys, "allow-commandline", &useArg);
324 updateBoolFromConfig(sys, "allow-autodetect", &useDetect);
325 updateBoolFromConfig(sys, "allow-user-config", &useUserConfig);
326 updateStringFromConfig(sys, "helper-binary", &helper->exe);
327 updateStringFromConfig(sys, "user-agent", &helper->userAgent);
329 }
330 if (useUserConfig)
331 {
332 WINPR_JSON* user = freerdp_GetJSONConfigFile(FALSE, config);
333 if (user)
334 {
335 updateBoolFromConfig(user, "allow-commandline", &useArg);
336 updateBoolFromConfig(user, "allow-autodetect", &useDetect);
337 updateStringFromConfig(user, "helper-binary", &helper->exe);
338 updateStringFromConfig(user, "user-agent", &helper->userAgent);
339 }
340 WINPR_JSON_Delete(user);
341 }
342
343 if (useArg)
344 {
345 const char* args =
346 freerdp_settings_get_string(helper->context->context.settings, FreeRDP_AadAuthHelper);
347 if (args && (strcmp("autodetect", args) == 0))
348 {
349 free(helper->exe);
350 helper->exe = aad_auth_helper_detect_helper();
351 if (!helper->exe)
352 return FALSE;
353 }
354 else if (args)
355 {
356 free(helper->exe);
357 helper->exe = _strdup(args);
358 if (!helper->exe)
359 return FALSE;
360 }
361 }
362
363 if (useArg)
364 {
365 const char* args = freerdp_settings_get_string(helper->context->context.settings,
366 FreeRDP_AadAuthUserAgent);
367 if (args)
368 {
369 free(helper->userAgent);
370 helper->userAgent = _strdup(args);
371 if (!helper->userAgent)
372 return FALSE;
373 }
374 }
375
376 if (useDetect && !helper->exe)
377 helper->exe = aad_auth_helper_detect_helper();
378
379 if (!helper->exe)
380 {
381 WLog_ERR(TAG, "aad-auth-helper: no helper application detected, aborting");
382 return FALSE;
383 }
384 return TRUE;
385}
386
387/* ---- public API ------------------------------------------------------------------------ */
388
389AadAuthHelper* aad_auth_helper_start(rdpClientContext* context)
390{
391 WINPR_ASSERT(context);
392
393 AadAuthHelper* helper = calloc(1, sizeof(AadAuthHelper));
394 if (!helper)
395 return nullptr;
396 helper->context = context;
397
398 PROCESS_INFORMATION procInfo = WINPR_C_ARRAY_INIT;
399 LPPROC_THREAD_ATTRIBUTE_LIST attrList = nullptr;
400 HANDLE hCmdInRead = nullptr; /* child's end, handed away via --cmdInFd= */
401 HANDLE hCmdOutWrite = nullptr; /* child's end, handed away via --cmdOutFd= */
402 char* cmdline = nullptr;
403 BOOL created = FALSE;
404
405 SECURITY_ATTRIBUTES saAttr = { .nLength = sizeof(SECURITY_ATTRIBUTES),
406 .bInheritHandle = TRUE,
407 .lpSecurityDescriptor = nullptr };
408
409 STARTUPINFOEXA siStartInfoEx = {
410 .StartupInfo.cb = sizeof(siStartInfoEx),
411 /* the JSON-RPC channel travels over two dedicated pipes handed to the helper via
412 * --cmdInFd=/--cmdOutFd= command line arguments (see winpr_exportHandleToString()
413 * below), not stdin/stdout - so the helper's stdio is left as a plain passthrough of
414 * this process' own, the same way hStdError already was. This keeps the protocol immune
415 * to anything the helper (or a library it links, e.g. Chromium/Qt) happens to print to
416 * stdout/stderr for its own diagnostics, and lets that output reach the user's terminal
417 * normally. */
418 .StartupInfo.hStdInput = GetStdHandle(STD_INPUT_HANDLE),
419 .StartupInfo.hStdOutput = GetStdHandle(STD_OUTPUT_HANDLE),
420 .StartupInfo.hStdError = GetStdHandle(STD_ERROR_HANDLE),
421 .StartupInfo.dwFlags = STARTF_USESTDHANDLES
422 };
423
424 if (!aad_auth_helper_config(helper))
425 goto cleanup;
426
427 if (!CreatePipe(&helper->hCmdOutRead, &hCmdOutWrite, &saAttr, 0))
428 {
429 WLog_ERR(TAG, "aad-auth-helper: cmdOut CreatePipe failed");
430 goto cleanup;
431 }
432 if (!SetHandleInformation(helper->hCmdOutRead, HANDLE_FLAG_INHERIT, 0))
433 {
434 WLog_ERR(TAG, "aad-auth-helper: cmdOut SetHandleInformation failed");
435 goto cleanup;
436 }
437
438 if (!CreatePipe(&hCmdInRead, &helper->hCmdInWrite, &saAttr, 0))
439 {
440 WLog_ERR(TAG, "aad-auth-helper: cmdIn CreatePipe failed");
441 goto cleanup;
442 }
443 if (!SetHandleInformation(helper->hCmdInWrite, HANDLE_FLAG_INHERIT, 0))
444 {
445 WLog_ERR(TAG, "aad-auth-helper: cmdIn SetHandleInformation failed");
446 goto cleanup;
447 }
448
449 char cmdInArg[64] = WINPR_C_ARRAY_INIT;
450 char cmdOutArg[64] = WINPR_C_ARRAY_INIT;
451 if (!winpr_exportHandleToString(hCmdInRead, "--cmdInFd={}", cmdInArg, sizeof(cmdInArg)))
452 {
453 WLog_ERR(TAG, "aad-auth-helper: failed to export the cmdIn handle");
454 goto cleanup;
455 }
456 if (!winpr_exportHandleToString(hCmdOutWrite, "--cmdOutFd={}", cmdOutArg, sizeof(cmdOutArg)))
457 {
458 WLog_ERR(TAG, "aad-auth-helper: failed to export the cmdOut handle");
459 goto cleanup;
460 }
461
462 /* explicit allowlist: only these handles are inherited by the spawned helper, regardless of
463 * anything else in this process that happens to also be marked inheritable (e.g. by another
464 * component linked into the same client). See winpr's CreateProcess /
465 * PROC_THREAD_ATTRIBUTE_HANDLE_LIST support - without this, WinPR's CreateProcess already
466 * defaults to closing everything not wired via STARTUPINFO, so this list exists to make
467 * that contract explicit and portable to real Windows builds of this same file.
468 *
469 * UpdateProcThreadAttribute() only stores a pointer to `handles`, it does not copy it
470 * (matches real Windows - see winpr's own DeleteProcThreadAttributeList() comment) - so
471 * `handles` must stay alive until the CreateProcessA() call below returns. Deliberately
472 * kept in the same block as that call rather than a narrower nested scope: a variable can
473 * be read as use-after-scope by AddressSanitizer once its enclosing block ends even while
474 * its storage is technically still on the stack, and even though CreateProcessA() only
475 * reads it through this still-live block, an earlier version of this function that closed
476 * `handles`' scope before calling CreateProcessA() (relying only on
477 * `attrList`/`siStartInfoEx` still being valid) tripped exactly that. */
478 HANDLE handles[5] = { siStartInfoEx.StartupInfo.hStdOutput, siStartInfoEx.StartupInfo.hStdInput,
479 siStartInfoEx.StartupInfo.hStdError, hCmdInRead, hCmdOutWrite };
480 {
481 SIZE_T size = 0;
482
483 if (InitializeProcThreadAttributeList(nullptr, 1, 0, &size) || (size == 0))
484 {
485 WLog_ERR(TAG, "aad-auth-helper: unexpected attribute list sizing result");
486 goto cleanup;
487 }
488
489 attrList = (LPPROC_THREAD_ATTRIBUTE_LIST)malloc(size);
490 if (!attrList || !InitializeProcThreadAttributeList(attrList, 1, 0, &size))
491 {
492 WLog_ERR(TAG, "aad-auth-helper: InitializeProcThreadAttributeList failed");
493 goto cleanup;
494 }
495
496 if (!UpdateProcThreadAttribute(attrList, 0, PROC_THREAD_ATTRIBUTE_HANDLE_LIST,
497 (PVOID)handles, sizeof(handles), nullptr, nullptr))
498 {
499 WLog_ERR(TAG, "aad-auth-helper: UpdateProcThreadAttribute failed");
500 goto cleanup;
501 }
502
503 siStartInfoEx.lpAttributeList = attrList;
504 }
505
506 {
507 size_t cmdlineLen = 0;
508 const int rc =
509 winpr_asprintf(&cmdline, &cmdlineLen, "\"%s\" %s %s", helper->exe, cmdInArg, cmdOutArg);
510 if (rc < 0)
511 goto cleanup;
512
513 created =
514 CreateProcessA(nullptr, cmdline, nullptr, nullptr, TRUE, EXTENDED_STARTUPINFO_PRESENT,
515 nullptr, nullptr, (LPSTARTUPINFOA)&siStartInfoEx, &procInfo);
516 }
517
518 if (!created)
519 WLog_ERR(TAG, "aad-auth-helper: failed to spawn '%s'", helper->exe);
520
521cleanup:
522 free(cmdline);
523 if (attrList)
524 {
525 DeleteProcThreadAttributeList(attrList);
526 free(attrList);
527 }
528 (void)CloseHandle(procInfo.hThread);
529 if (hCmdInRead)
530 (void)CloseHandle(hCmdInRead);
531 if (hCmdOutWrite)
532 (void)CloseHandle(hCmdOutWrite);
533
534 if (!created)
535 {
536 aad_auth_helper_stop(helper);
537 return nullptr;
538 }
539
540 helper->hProcess = procInfo.hProcess;
541
542 {
543 const UINT32 id = ++helper->nextId;
544 char* req = build_hello_request(id);
545 BOOL ok = req && helper_write_line(helper, req);
546 free(req);
547
548 if (ok)
549 {
550 WINPR_JSON* resp = wait_for_response(helper, id);
551 ok = resp && WINPR_JSON_HasObjectItem(resp, "result");
552 if (resp)
553 WINPR_JSON_Delete(resp);
554 }
555
556 if (!ok)
557 {
558 WLog_ERR(TAG, "aad-auth-helper: hello handshake failed");
559 aad_auth_helper_stop(helper);
560 return nullptr;
561 }
562 }
563
564 return helper;
565}
566
567static AadAuthHelperNavigateStatus
568aad_auth_helper_navigate(AadAuthHelper* helper, const char* title, const char* url,
569 const char* redirect_uri, const char* user_agent, UINT32 timeout_ms,
570 char** redirect_url, size_t* redirect_url_len)
571{
572 WINPR_ASSERT(helper);
573 WINPR_ASSERT(url);
574 WINPR_ASSERT(redirect_uri);
575 WINPR_ASSERT(redirect_url);
576 WINPR_ASSERT(redirect_url_len);
577
578 *redirect_url = nullptr;
579 *redirect_url_len = 0;
580
581 const UINT32 id = ++helper->nextId;
582 char* req =
583 build_navigate_request(id, title ? title : "", url, redirect_uri, user_agent, timeout_ms);
584 if (!req)
585 return AAD_AUTH_HELPER_NAVIGATE_ERROR;
586
587 BOOL ok = helper_write_line(helper, req);
588 free(req);
589 if (!ok)
590 return AAD_AUTH_HELPER_NAVIGATE_ERROR;
591
592 WINPR_JSON* resp = wait_for_response(helper, id);
593 if (!resp)
594 return AAD_AUTH_HELPER_NAVIGATE_ERROR;
595
596 WINPR_JSON* error = WINPR_JSON_GetObjectItemCaseSensitive(resp, "error");
597 if (error)
598 {
599 WINPR_JSON* message = WINPR_JSON_GetObjectItemCaseSensitive(error, "message");
600 const char* msg = (message && WINPR_JSON_IsString(message))
602 : "unknown error";
603 WLog_WARN(TAG, "aad-auth-helper: navigate failed: %s", msg);
604
605 AadAuthHelperNavigateStatus status = AAD_AUTH_HELPER_NAVIGATE_ERROR;
606 if (strcmp(msg, "user_cancelled") == 0)
607 status = AAD_AUTH_HELPER_NAVIGATE_CANCELLED;
608 else if (strcmp(msg, "timeout") == 0)
609 status = AAD_AUTH_HELPER_NAVIGATE_TIMEOUT;
610
611 WINPR_JSON_Delete(resp);
612 return status;
613 }
614
615 WINPR_JSON* result = WINPR_JSON_GetObjectItemCaseSensitive(resp, "result");
616 WINPR_JSON* urlItem =
617 result ? WINPR_JSON_GetObjectItemCaseSensitive(result, "redirect_url") : nullptr;
618 const char* value =
619 (urlItem && WINPR_JSON_IsString(urlItem)) ? WINPR_JSON_GetStringValue(urlItem) : nullptr;
620
621 if (!value)
622 {
623 WLog_ERR(TAG, "aad-auth-helper: malformed navigate result");
624 WINPR_JSON_Delete(resp);
625 return AAD_AUTH_HELPER_NAVIGATE_ERROR;
626 }
627
628 *redirect_url = _strdup(value);
629 if (*redirect_url)
630 *redirect_url_len = strlen(*redirect_url);
631 WINPR_JSON_Delete(resp);
632 return (*redirect_url != nullptr) ? AAD_AUTH_HELPER_NAVIGATE_OK
633 : AAD_AUTH_HELPER_NAVIGATE_ERROR;
634}
635
636BOOL aad_auth_helper_is_running(const AadAuthHelper* helper)
637{
638 if (!helper || !helper->hProcess)
639 return FALSE;
640 return WaitForSingleObject(helper->hProcess, 0) == WAIT_TIMEOUT;
641}
642
643void aad_auth_helper_stop(AadAuthHelper* helper)
644{
645 if (!helper)
646 return;
647
648 /* no shutdown handshake with a helper that already exited: nobody reads the pipe anymore
649 * (writing there would raise SIGPIPE) */
650 if (aad_auth_helper_is_running(helper))
651 {
652 const UINT32 id = ++helper->nextId;
653 char* req = build_shutdown_request(id);
654 if (req && helper_write_line(helper, req))
655 {
656 WINPR_JSON* resp = wait_for_response(helper, id);
657 if (resp)
658 WINPR_JSON_Delete(resp);
659 }
660 free(req);
661
662 char* notif = build_exit_notification();
663 if (notif)
664 (void)helper_write_line(helper, notif);
665 free(notif);
666
667 if (WaitForSingleObject(helper->hProcess, 3000) != WAIT_OBJECT_0)
668 {
669 WLog_WARN(TAG, "aad-auth-helper: did not exit in time, terminating");
670 (void)TerminateProcess(helper->hProcess, 0);
671 }
672 }
673
674 if (helper->hProcess)
675 (void)CloseHandle(helper->hProcess);
676
677 if (helper->hCmdInWrite)
678 (void)CloseHandle(helper->hCmdInWrite);
679 if (helper->hCmdOutRead)
680 (void)CloseHandle(helper->hCmdOutRead);
681
682 free(helper->buf);
683 free(helper->exe);
684 free(helper->userAgent);
685 free(helper);
686}
687
688WINPR_ATTR_MALLOC(winpr_zfree, 1)
689static char* aad_auth_helper_extract_query_param(const char* url, const char* name)
690{
691 if (!url || !name)
692 return nullptr;
693
694 const char* start = strchr(url, '?');
695 if (!start)
696 return nullptr;
697
698 const char* param = strstr(start, name);
699 if (!param)
700 return nullptr;
701
702 const size_t len = strlen(name);
703 if (param[len] != '=')
704 return nullptr;
705
706 char* str = _strdup(&param[len + 1]);
707 if (!str)
708 return nullptr;
709
710 char* end = strchr(str, '&');
711 if (end)
712 *end = '\0';
713 const size_t slen = strlen(str);
714 char* decoded = winpr_str_url_decode(str, slen);
715 winpr_zfree(str);
716 return decoded;
717}
718
726WINPR_ATTR_NODISCARD
727static AadAuthHelperNavigateStatus aad_helper_navigate(AadAuthHelper* helper, const char* title,
728 const char* url, char** pRedirectUrl,
729 size_t* pRedirectUrlLen)
730{
731 WINPR_ASSERT(helper);
732 WINPR_ASSERT(title);
733 WINPR_ASSERT(url);
734 WINPR_ASSERT(pRedirectUrl);
735 WINPR_ASSERT(pRedirectUrlLen);
736
737 *pRedirectUrl = nullptr;
738 *pRedirectUrlLen = 0;
739
740 char* redirectUri = aad_auth_helper_extract_query_param(url, "redirect_uri");
741 if (!redirectUri)
742 {
743 WLog_ERR(TAG, "[aad-auth] url %s has no redirect_uri parameter", url);
744 return AAD_AUTH_HELPER_NAVIGATE_ERROR;
745 }
746
747 char* out = nullptr;
748 size_t outLen = 0;
749 const char* user_agent = helper->userAgent;
750 const AadAuthHelperNavigateStatus status = aad_auth_helper_navigate(
751 helper, title, url, redirectUri, user_agent, 180000, &out, &outLen);
752 winpr_zfree(redirectUri);
753 if (status != AAD_AUTH_HELPER_NAVIGATE_OK)
754 {
755 free(out);
756 return status;
757 }
758
759 *pRedirectUrl = out;
760 *pRedirectUrlLen = outLen;
761 return AAD_AUTH_HELPER_NAVIGATE_OK;
762}
763
764WINPR_ATTR_NODISCARD
765static BOOL aad_auth_helper_get_rdsaad_access_token(AadAuthHelper* helper,
766 freerdp_client_aad_type requestType,
767 freerdp_client_aad_type tokenType,
768 const char* scope, const char* req_cnf,
769 char** token)
770{
771 WINPR_ASSERT(helper);
772 WINPR_ASSERT(scope);
773 WINPR_ASSERT(req_cnf);
774 WINPR_ASSERT(token);
775
776 rdpClientContext* cctx = helper->context;
777 WINPR_ASSERT(cctx);
778
779 const char* title = "FreeRDP WebView - AAD access token";
780 if (requestType == FREERDP_CLIENT_AAD_AVD_AUTH_REQUEST)
781 title = "FreeRDP WebView - AVD access token";
782
783 char* request = freerdp_client_get_aad_url(cctx, requestType, scope);
784 if (!request)
785 {
786 WLog_ERR(TAG, "[aad-auth] authentication failed, could not construct request");
787 return FALSE;
788 }
789
790 char* redirectUrl = nullptr;
791 size_t redirectUrlLen = 0;
792 const AadAuthHelperNavigateStatus status =
793 aad_helper_navigate(helper, title, request, &redirectUrl, &redirectUrlLen);
794 winpr_zfree(request);
795
796 if (status == AAD_AUTH_HELPER_NAVIGATE_CANCELLED)
797 {
798 winpr_znfree(redirectUrl, redirectUrlLen);
799 WLog_INFO(TAG, "[aad-auth] user cancelled the authentication");
800 return FALSE;
801 }
802 if (status == AAD_AUTH_HELPER_NAVIGATE_TIMEOUT)
803 {
804 winpr_znfree(redirectUrl, redirectUrlLen);
805 WLog_ERR(TAG, "[aad-auth] authentication timed out");
806 return FALSE;
807 }
808 if (status != AAD_AUTH_HELPER_NAVIGATE_OK)
809 {
810 winpr_znfree(redirectUrl, redirectUrlLen);
811 WLog_ERR(TAG, "[aad-auth] authentication failed");
812 return FALSE;
813 }
814
815 char* code = freerdp_client_extract_aad_code(cctx, redirectUrl, redirectUrlLen);
816 winpr_znfree(redirectUrl, redirectUrlLen);
817
818 if (!code)
819 {
820 WLog_ERR(TAG, "[aad-auth] authentication failed, could not find code parameter");
821 return FALSE;
822 }
823
824 char* token_request = nullptr;
825 if (tokenType == FREERDP_CLIENT_AAD_TOKEN_REQUEST)
826 token_request = freerdp_client_get_aad_url(cctx, tokenType, scope, code, req_cnf);
827 else
828 token_request = freerdp_client_get_aad_url(cctx, tokenType, code);
829 winpr_zfree(code);
830 if (!token_request)
831 {
832 WLog_ERR(TAG, "[aad-auth] authentication failed, could not get token");
833 return FALSE;
834 }
835
836 const BOOL rc = client_common_get_access_token(cctx->context.instance, token_request, token);
837 winpr_zfree(token_request);
838 return rc;
839}
840
841BOOL aad_auth_helper_get_access_token_v(AadAuthHelper* helper, AccessTokenType tokenType,
842 char** token, size_t count, va_list args)
843{
844 WINPR_ASSERT(token);
845 switch (tokenType)
846 {
847 case ACCESS_TOKEN_TYPE_AAD:
848 {
849 if (count < 2)
850 {
851 WLog_ERR(TAG,
852 "ACCESS_TOKEN_TYPE_AAD expected 2 additional arguments, but got %" PRIuz
853 ", aborting",
854 count);
855 return FALSE;
856 }
857 else if (count > 2)
858 WLog_WARN(TAG,
859 "ACCESS_TOKEN_TYPE_AAD expected 2 additional arguments, but got %" PRIuz
860 ", ignoring",
861 count);
862 const char* scope = va_arg(args, const char*);
863 const char* req_cnf = va_arg(args, const char*);
864 return aad_auth_helper_get_rdsaad_access_token(helper, FREERDP_CLIENT_AAD_AUTH_REQUEST,
865 FREERDP_CLIENT_AAD_TOKEN_REQUEST, scope,
866 req_cnf, token);
867 }
868 case ACCESS_TOKEN_TYPE_AVD:
869 if (count != 0)
870 WLog_WARN(TAG,
871 "ACCESS_TOKEN_TYPE_AVD expected 0 additional arguments, but got %" PRIuz
872 ", ignoring",
873 count);
874 return aad_auth_helper_get_rdsaad_access_token(
875 helper, FREERDP_CLIENT_AAD_AVD_AUTH_REQUEST, FREERDP_CLIENT_AAD_AVD_TOKEN_REQUEST,
876 "", "", token);
877 default:
878 WLog_ERR(TAG, "Unexpected value for AccessTokenType [%" PRIu32 "], aborting",
879 tokenType);
880 return FALSE;
881 }
882}
883
884BOOL aad_auth_helper_get_access_token(AadAuthHelper* helper, AccessTokenType tokenType,
885 char** token, size_t count, ...)
886{
887 va_list ap = WINPR_C_ARRAY_INIT;
888 va_start(ap, count);
889 const BOOL rc = aad_auth_helper_get_access_token_v(helper, tokenType, token, count, ap);
890 va_end(ap);
891 return rc;
892}
893
894/* whether any auto-detectable helper was enabled at build time at all - see
895 * WITH_XDG_AAD_AUTH_HELPER / WITH_WEBVIEW_AAD_AUTH_HELPER / WITH_QT_AAD_AUTH_HELPER in
896 * client/common/CMakeLists.txt, propagated here as compile definitions by
897 * client/SDL/common/CMakeLists.txt. Guards kHelperCandidates below: with none of the three
898 * defined there's nothing to list, and a zero-size array isn't valid standard C++. */
899
900/* auto-pick order for /azure:auth-helper:autodetect (or the option omitted entirely): xdg-open
901 * first (drives the user's actual default browser, so it inherits whatever SSO session/cookies
902 * are already there instead of prompting again), then the embedded webview (lighter, native OS
903 * look), then Qt. */
904static const char* kHelperCandidates[] = { "freerdp-xdg-aad-helper", "freerdp-qt-aad-helper",
905 "freerdp-webview-aad-helper" };
906
907static void helper_binary_dirs_free(char** dirs, size_t count)
908{
909 if (!dirs)
910 return;
911 for (size_t x = 0; x < count; x++)
912 {
913 char* dir = dirs[x];
914 free(dir);
915 }
916 free((void*)dirs);
917}
918
919WINPR_ATTR_MALLOC(free, 1)
920static char* aad_auth_helper_get_binary_dir(void)
921{
922 DWORD len = 4096;
923 char* path = nullptr;
924 do
925 {
926 char* tmp = realloc(path, len);
927 if (!tmp)
928 {
929 WLog_ERR(TAG, "[aad-auth] GetModuleFileNameA failed");
930 free(path);
931 return nullptr;
932 }
933 path = tmp;
934
935 const DWORD rc = GetModuleFileNameA(nullptr, path, len);
936 if (rc == 0)
937 {
938 WLog_ERR(TAG, "[aad-auth] GetModuleFileNameA failed");
939 free(path);
940 return nullptr;
941 }
942
943 if (rc == len)
944 {
945 if (GetLastError() == ERROR_INSUFFICIENT_BUFFER)
946 {
947 len += 4096;
948 continue;
949 }
950 WLog_ERR(TAG, "[aad-auth] GetModuleFileNameA failed");
951 free(path);
952 return nullptr;
953 }
954 else
955 break;
956 } while (TRUE);
957
958 char* sep = strrchr(path, '/');
959#ifdef _WIN32
960 char* sepWin = strrchr(path, '\\');
961 if (!sep || (sepWin && (sepWin > sep)))
962 sep = sepWin;
963#endif
964 if (!sep)
965 {
966 free(path);
967 return nullptr;
968 }
969 *sep = '\0';
970 return path;
971}
972
973/* directory this client binary itself lives in - where an installed (or freshly built) helper
974 * binary is expected to sit alongside it. */
975WINPR_ATTR_MALLOC(helper_binary_dirs_free, 1)
976static char** aad_auth_helper_binary_dirs(size_t* count)
977{
978 WINPR_ASSERT(count);
979
980 *count = 0;
981 char** dirs = (char**)calloc(32, sizeof(char*));
982 if (!dirs)
983 return nullptr;
984 {
985 char* libexec = GetCombinedPath(FREERDP_INSTALL_PREFIX, FREERDP_LIBEXEC_PATH);
986 if (libexec)
987 dirs[(*count)++] = libexec;
988 }
989
990 char* app = aad_auth_helper_get_binary_dir();
991 if (app)
992 {
993 dirs[(*count)++] = app;
994
995 char* libexec = GetCombinedPath(app, FREERDP_LIBEXEC_REL_PATH);
996 if (libexec)
997 dirs[(*count)++] = libexec;
998 }
999 return dirs;
1000}
1001
1002WINPR_ATTR_MALLOC(free, 1)
1003static char* aad_auth_helper_path_for_binary(const char* dir, const char* binaryName)
1004{
1005 const char extension[] = CMAKE_EXECUTABLE_SUFFIX;
1006
1007 char* path = nullptr;
1008 size_t plen = 0;
1009 winpr_asprintf(&path, &plen, "%s/%s%s", dir, binaryName, extension);
1010 return path;
1011}
1012
1013/* /azure:auth-helper:autodetect (or the option omitted entirely): probe the well-known binaries
1014 * in kHelperCandidates order and use whichever is actually present. */
1015WINPR_ATTR_MALLOC(free, 1)
1016static char* aad_auth_helper_auto_locate(void)
1017{
1018 size_t dirscount = 0;
1019 char** dirs = aad_auth_helper_binary_dirs(&dirscount);
1020 if (!dirs)
1021 return nullptr;
1022
1023 char* path = nullptr;
1024 for (size_t i = 0; i < dirscount; i++)
1025 {
1026 char* dir = dirs[i];
1027 if (!dir)
1028 continue;
1029 for (size_t x = 0; x < ARRAYSIZE(kHelperCandidates); x++)
1030 {
1031 const char* binaryName = kHelperCandidates[x];
1032 char* cpath = aad_auth_helper_path_for_binary(dir, binaryName);
1033 if (winpr_PathFileExists(cpath))
1034 {
1035 path = cpath;
1036 break;
1037 }
1038 free(cpath);
1039 }
1040 if (path)
1041 break;
1042 }
1043
1044 helper_binary_dirs_free(dirs, dirscount);
1045 return path;
1046}
1047
1048/* @p helper is the caller's own per-connection storage slot (e.g. a member of its SdlContext) -
1049 * this file never stores anything itself, so it stays usable as one binary shared between the
1050 * SDL2 and SDL3 clients regardless of their (different) concrete SdlContext type. */
1051char* aad_auth_helper_detect_helper(void)
1052{
1053 char* path = aad_auth_helper_auto_locate();
1054
1055 if (!path)
1056 {
1057 WLog_ERR(TAG, "[aad-auth] could not determine expected helper binary location");
1058 return nullptr;
1059 }
1060
1061 if (!winpr_PathFileExists(path))
1062 {
1063 WLog_ERR(TAG, "[aad-auth] helper binary not found at '%s'", path);
1064 free(path);
1065 return nullptr;
1066 }
1067
1068 WLog_DBG(TAG, "[aad-auth] auto-detected helper %s", path);
1069 return path;
1070}
WINPR_ATTR_NODISCARD WINPR_API WINPR_JSON * WINPR_JSON_AddObjectToObject(WINPR_JSON *object, const char *name)
WINPR_JSON_AddObjectToObject.
Definition c-json.c:274
WINPR_ATTR_NODISCARD WINPR_API WINPR_JSON * WINPR_JSON_CreateObject(void)
WINPR_JSON_CreateObject.
Definition c-json.c:232
WINPR_ATTR_NODISCARD WINPR_API BOOL WINPR_JSON_HasObjectItem(const WINPR_JSON *object, const char *string)
Check if JSON has an object matching the name.
Definition c-json.c:132
WINPR_ATTR_NODISCARD WINPR_API BOOL WINPR_JSON_IsBool(const WINPR_JSON *item)
Check if JSON item is of type BOOL.
Definition c-json.c:167
WINPR_ATTR_NODISCARD WINPR_API BOOL WINPR_JSON_IsNumber(const WINPR_JSON *item)
Check if JSON item is of type Number.
Definition c-json.c:177
WINPR_ATTR_NODISCARD WINPR_API WINPR_JSON * WINPR_JSON_AddIntegerToObject(WINPR_JSON *object, const char *name, int64_t number)
WINPR_JSON_AddIntegerToObject.
Definition c-json.c:262
WINPR_ATTR_NODISCARD WINPR_API BOOL WINPR_JSON_IsTrue(const WINPR_JSON *item)
Check if JSON item is BOOL value True.
Definition c-json.c:162
WINPR_ATTR_NODISCARD WINPR_API WINPR_JSON * WINPR_JSON_GetObjectItemCaseSensitive(const WINPR_JSON *object, const char *string)
Same as WINPR_JSON_GetObjectItem but with case sensitive matching.
Definition c-json.c:127
WINPR_ATTR_NODISCARD WINPR_API BOOL WINPR_JSON_IsString(const WINPR_JSON *item)
Check if JSON item is of type String.
Definition c-json.c:182
WINPR_API char * WINPR_JSON_PrintUnformatted(WINPR_JSON *item)
Serialize a JSON instance to string without formatting for human readable formatted output see WINPR_...
Definition c-json.c:311
WINPR_ATTR_NODISCARD WINPR_API double WINPR_JSON_GetNumberValue(const WINPR_JSON *item)
Return the Number value of a JSON item.
Definition c-json.c:147
WINPR_ATTR_NODISCARD WINPR_API WINPR_JSON * WINPR_JSON_AddStringToObject(WINPR_JSON *object, const char *name, const char *string)
WINPR_JSON_AddStringToObject.
Definition c-json.c:269
WINPR_API void WINPR_JSON_Delete(WINPR_JSON *item)
Delete a WinPR JSON wrapper object.
Definition c-json.c:103
WINPR_ATTR_NODISCARD WINPR_API const char * WINPR_JSON_GetStringValue(WINPR_JSON *item)
Return the String value of a JSON item.
Definition c-json.c:142
WINPR_API WINPR_JSON * WINPR_JSON_Parse(const char *value)
Parse a '\0' terminated JSON string.
Definition c-json.c:93
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.