FreeRDP
Loading...
Searching...
No Matches
qt-aad-helper/main.cpp
1
26#include <QApplication>
27#include <QCloseEvent>
28#include <QJsonDocument>
29#include <QJsonObject>
30#include <QJsonValue>
31#include <QMetaObject>
32#include <QString>
33#include <QUrl>
34#include <QWebEngineNavigationRequest>
35#include <QWebEnginePage>
36#include <QWebEngineProfile>
37#include <QWebEngineUrlRequestJob>
38#include <QWebEngineUrlScheme>
39#include <QWebEngineUrlSchemeHandler>
40#include <QWebEngineView>
41
42#include <chrono>
43#include <condition_variable>
44#include <cstdint>
45#include <iostream>
46#include <mutex>
47#include <string>
48#include <thread>
49
50#include <winpr/file.h>
51#include <winpr/handle.h>
52
53#include "redirect_watcher.hpp"
54
55constexpr uint32_t kDefaultTimeoutMs = 180000;
56
75constexpr const char* kBrokerScheme = "ms-appx-web";
76
77struct PendingResult
78{
79 std::mutex mtx;
80 std::condition_variable cv;
81 bool done = false;
82 bool ok = false;
83 std::string redirectUrl;
84 std::string errorMessage;
85};
86
87class Session;
88
101class AuthWindow : public QWebEngineView
102{
103 Q_OBJECT
104 public:
105 explicit AuthWindow(Session& owner);
106
107 protected:
108 void closeEvent(QCloseEvent* event) override;
109
110 private slots:
111 void onNavigationRequested(QWebEngineNavigationRequest& request);
112 void onLoadFinished(bool ok);
113
114 private:
115 Session& session;
116};
117
121class Session
122{
123 public:
124 Session() : nativeUserAgent(QWebEngineProfile::defaultProfile()->httpUserAgent().toStdString())
125 {
126 }
127
128 bool navigate(const std::string& title, const std::string& url, const std::string& redirectUri,
129 uint32_t timeoutMs, const std::string& userAgent, std::string& redirectUrl,
130 std::string& error)
131 {
132 PendingResult pending;
133 {
134 std::lock_guard<std::mutex> lock(stateMtx);
135 if (current)
136 {
137 error = "navigate_already_in_progress";
138 return false;
139 }
140 current = &pending;
141 watcher = RedirectWatcher(redirectUri);
142 }
143
144 const QString qtitle = QString::fromStdString(title);
145 const QUrl qurl(QString::fromStdString(url));
146 QMetaObject::invokeMethod(
147 qApp,
148 [this, qtitle, qurl, userAgent]()
149 {
150 if (!window)
151 window = new AuthWindow(*this);
152 window->setWindowTitle(qtitle);
153 window->resize(800, 600);
154 window->show();
155 window->raise();
156 window->activateWindow();
157 /* QtWebEngine keeps one profile for the process' life, so an override would
158 * otherwise persist after it is dropped: always set either the override or the
159 * native UA captured at construction. */
160 const QString ua =
161 QString::fromStdString(userAgent.empty() ? nativeUserAgent : userAgent);
162 QWebEngineProfile::defaultProfile()->setHttpUserAgent(ua);
163 window->setUrl(qurl);
164 },
165 Qt::QueuedConnection);
166
167 const uint32_t timeout = timeoutMs ? timeoutMs : kDefaultTimeoutMs;
168 bool signalled = false;
169 {
170 std::unique_lock<std::mutex> lock(pending.mtx);
171 signalled = pending.cv.wait_for(lock, std::chrono::milliseconds(timeout),
172 [&] { return pending.done; });
173 }
174
175 {
176 std::lock_guard<std::mutex> lock(stateMtx);
177 /* still ours: nobody finished it (timeout) - detach the now-invalid pointer to the
178 * stack-local `pending` before it goes out of scope */
179 if (current == &pending)
180 current = nullptr;
181 }
182
183 /* the popup has served its purpose once this navigate() is done, whatever the outcome -
184 * hide it now rather than leaving it sitting on screen until the next navigate() (if any)
185 * reuses and re-shows it. */
186 QMetaObject::invokeMethod(
187 qApp,
188 [this]()
189 {
190 if (window)
191 window->hide();
192 },
193 Qt::QueuedConnection);
194
195 if (!signalled)
196 {
197 error = "timeout";
198 return false;
199 }
200
201 if (!pending.ok)
202 {
203 error = pending.errorMessage.empty() ? "user_cancelled" : pending.errorMessage;
204 return false;
205 }
206
207 redirectUrl = pending.redirectUrl;
208 return true;
209 }
210
211 void cancel()
212 {
213 std::lock_guard<std::mutex> lock(stateMtx);
214 finishCurrentLocked(false, "", "user_cancelled");
215 }
216
217 void quit()
218 {
219 {
220 std::lock_guard<std::mutex> lock(stateMtx);
221 finishCurrentLocked(false, "", "shutting_down");
222 }
223 /* the window (and the QWebEnginePage/profile reference it holds) must be gone before
224 * QApplication's destructor tears down the default QWebEngineProfile, or QtWebEngine
225 * warns "Release of profile requested but WebEnginePage still not deleted." Deleting it
226 * here, still inside the running event loop, is what QtWebEngine expects. */
227 QMetaObject::invokeMethod(
228 qApp,
229 [this]()
230 {
231 delete window;
232 window = nullptr;
233 qApp->quit();
234 },
235 Qt::QueuedConnection);
236 }
237
238 /* called on the UI thread by AuthWindow::onNavigationRequested. Returns true if `uri` matched
239 * the pending redirect_uri (in which case the pending navigate() has been resolved and the
240 * caller must reject the navigation rather than let QtWebEngine try to load it). */
241 bool onNavigate(const std::string& uri)
242 {
243 std::lock_guard<std::mutex> lock(stateMtx);
244 if (!current || !watcher.matches(uri))
245 return false;
246
247 std::string err;
248 if (watcher.hasError(uri, err))
249 finishCurrentLocked(false, "", err);
250 else
251 finishCurrentLocked(true, uri, "");
252 return true;
253 }
254
255 /* called on the UI thread by AuthWindow::closeEvent, e.g. the user closed the popup */
256 void onWindowClosed()
257 {
258 std::lock_guard<std::mutex> lock(stateMtx);
259 finishCurrentLocked(false, "", "user_cancelled");
260 }
261
262 private:
263 /* caller must hold stateMtx */
264 void finishCurrentLocked(bool ok, const std::string& redirectUrl, const std::string& err)
265 {
266 if (!current)
267 return;
268 {
269 std::lock_guard<std::mutex> lock(current->mtx);
270 current->done = true;
271 current->ok = ok;
272 current->redirectUrl = redirectUrl;
273 current->errorMessage = err;
274 }
275 current->cv.notify_all();
276 current = nullptr;
277 }
278
279 std::mutex stateMtx;
280 PendingResult* current = nullptr;
281 RedirectWatcher watcher{ std::string() };
282 AuthWindow* window = nullptr; /* UI thread only */
283 std::string nativeUserAgent;
284};
285
286AuthWindow::AuthWindow(Session& owner) : session(owner)
287{
288 connect(page(), &QWebEnginePage::navigationRequested, this, &AuthWindow::onNavigationRequested);
289 connect(this, &QWebEngineView::loadFinished, this, &AuthWindow::onLoadFinished);
290}
291
292void AuthWindow::onNavigationRequested(QWebEngineNavigationRequest& request)
293{
294 if (session.onNavigate(request.url().toString().toStdString()))
295 request.reject();
296}
297
298void AuthWindow::onLoadFinished(bool ok)
299{
300 std::cout << "[qt-aad-helper] loaded " << url().toString().toStdString()
301 << (ok ? "" : " (failed)") << std::endl;
302}
303
304void AuthWindow::closeEvent(QCloseEvent* event)
305{
306 session.onWindowClosed();
307 QWebEngineView::closeEvent(event);
308}
309
315class BrokerSchemeHandler : public QWebEngineUrlSchemeHandler
316{
317 public:
318 explicit BrokerSchemeHandler(Session& owner) : session(owner)
319 {
320 }
321
322 void requestStarted(QWebEngineUrlRequestJob* job) override
323 {
324 session.onNavigate(job->requestUrl().toString().toStdString());
325 job->fail(QWebEngineUrlRequestJob::RequestAborted);
326 }
327
328 private:
329 Session& session;
330};
331
332namespace
333{
334
335 /* the JSON-RPC channel: handles imported from the --cmdInFd=/--cmdOutFd= command line arguments
336 * (see main()), not stdin/stdout - so this helper's own stdio stays free for its normal
337 * diagnostic output (Qt/Chromium warnings, WLog, etc.) instead of colliding with the protocol.
338 */
339 HANDLE g_cmdIn = nullptr;
340 HANDLE g_cmdOut = nullptr;
341
342 bool writeLine(const std::string& line)
343 {
344 std::string data = line;
345 data += "\n";
346
347 size_t written = 0;
348 while (written < data.size())
349 {
350 DWORD dwWritten = 0;
351 if (!WriteFile(g_cmdOut, data.data() + written,
352 static_cast<DWORD>(data.size() - written), &dwWritten, nullptr) ||
353 (dwWritten == 0))
354 return false;
355 written += dwWritten;
356 }
357 return true;
358 }
359
360 /* extracts one '\n'-terminated line already buffered in `buf`, if any */
361 bool extractLine(std::string& buf, std::string& line)
362 {
363 const auto pos = buf.find('\n');
364 if (pos == std::string::npos)
365 return false;
366 line = buf.substr(0, pos);
367 buf.erase(0, pos + 1);
368 return true;
369 }
370
371 bool readLine(std::string& buf, std::string& line)
372 {
373 if (extractLine(buf, line))
374 return true;
375
376 while (true)
377 {
378 char chunk[4096];
379 DWORD dwRead = 0;
380 if (!ReadFile(g_cmdIn, chunk, sizeof(chunk), &dwRead, nullptr) || (dwRead == 0))
381 return false;
382 buf.append(chunk, dwRead);
383 if (extractLine(buf, line))
384 return true;
385 }
386 }
387
388 void sendLine(const std::string& line)
389 {
390 (void)writeLine(line);
391 }
392
393 void sendJson(const QJsonObject& obj)
394 {
395 const QJsonDocument doc(obj);
396 sendLine(doc.toJson(QJsonDocument::Compact).toStdString());
397 }
398
399 void sendHelloResult(int64_t id)
400 {
401 QJsonObject result;
402 result["protocol_version"] = 1;
403 result["helper"] = QStringLiteral("freerdp-qt-aad-helper/1.0");
404
405 QJsonObject obj;
406 obj["jsonrpc"] = QStringLiteral("2.0");
407 obj["id"] = static_cast<double>(id);
408 obj["result"] = result;
409 sendJson(obj);
410 }
411
412 void sendNavigateResult(int64_t id, const std::string& redirectUrl)
413 {
414 QJsonObject result;
415 result["status"] = QStringLiteral("ok");
416 result["redirect_url"] = QString::fromStdString(redirectUrl);
417
418 QJsonObject obj;
419 obj["jsonrpc"] = QStringLiteral("2.0");
420 obj["id"] = static_cast<double>(id);
421 obj["result"] = result;
422 sendJson(obj);
423 }
424
425 void sendNullResult(int64_t id)
426 {
427 QJsonObject obj;
428 obj["jsonrpc"] = QStringLiteral("2.0");
429 obj["id"] = static_cast<double>(id);
430 obj["result"] = QJsonValue();
431 sendJson(obj);
432 }
433
434 void sendError(int64_t id, int code, const std::string& message)
435 {
436 QJsonObject error;
437 error["code"] = code;
438 error["message"] = QString::fromStdString(message);
439
440 QJsonObject obj;
441 obj["jsonrpc"] = QStringLiteral("2.0");
442 obj["id"] = static_cast<double>(id);
443 obj["error"] = error;
444 sendJson(obj);
445 }
446
447 std::string getStringField(const QJsonObject& obj, const char* name)
448 {
449 const QJsonValue v = obj.value(QLatin1String(name));
450 return v.isString() ? v.toString().toStdString() : std::string();
451 }
452
453 uint32_t getUintField(const QJsonObject& obj, const char* name, uint32_t def)
454 {
455 const QJsonValue v = obj.value(QLatin1String(name));
456 return v.isDouble() ? static_cast<uint32_t>(v.toDouble()) : def;
457 }
458
462 void readerLoop(Session& session)
463 {
464 std::string buf;
465 std::string line;
466 while (readLine(buf, line))
467 {
468 if (line.empty())
469 continue;
470
471 const QJsonDocument doc = QJsonDocument::fromJson(QByteArray::fromStdString(line));
472 if (!doc.isObject())
473 continue;
474 const QJsonObject msg = doc.object();
475
476 const QJsonValue idValue = msg.value(QLatin1String("id"));
477 const bool hasId = idValue.isDouble();
478 const int64_t id = hasId ? static_cast<int64_t>(idValue.toDouble()) : 0;
479 const std::string method = getStringField(msg, "method");
480 const QJsonObject params = msg.value(QLatin1String("params")).toObject();
481
482 if (method == "hello")
483 {
484 sendHelloResult(id);
485 }
486 else if (method == "navigate")
487 {
488 const std::string title = getStringField(params, "title");
489 const std::string url = getStringField(params, "url");
490 const std::string redirectUri = getStringField(params, "redirect_uri");
491 const std::string userAgent = getStringField(params, "user_agent");
492 const uint32_t timeoutMs = getUintField(params, "timeout_ms", kDefaultTimeoutMs);
493
494 std::string redirectUrl;
495 std::string error;
496 if (session.navigate(title, url, redirectUri, timeoutMs, userAgent, redirectUrl,
497 error))
498 sendNavigateResult(id, redirectUrl);
499 else
500 sendError(id, 1, error);
501 }
502 else if (method == "cancel")
503 {
504 session.cancel();
505 }
506 else if (method == "shutdown")
507 {
508 sendNullResult(id);
509 }
510 else if (method == "exit")
511 {
512 break;
513 }
514 }
515
516 session.quit();
517 }
518
519} // namespace
520
521int main(int argc, char* argv[])
522{
523 std::string cmdInArg;
524 std::string cmdOutArg;
525 for (int i = 1; i < argc; i++)
526 {
527 const std::string arg = argv[i];
528 if (arg.rfind("--cmdInFd=", 0) == 0)
529 cmdInArg = arg;
530 else if (arg.rfind("--cmdOutFd=", 0) == 0)
531 cmdOutArg = arg;
532 }
533
534 if (cmdInArg.empty() || cmdOutArg.empty())
535 {
536 std::cerr << "usage: " << (argc > 0 ? argv[0] : "freerdp-qt-aad-helper")
537 << " --cmdInFd=<handle> --cmdOutFd=<handle>" << std::endl;
538 return 1;
539 }
540
541 g_cmdIn = winpr_importHandleFromString(cmdInArg.c_str(), "--cmdInFd={}");
542 g_cmdOut = winpr_importHandleFromString(cmdOutArg.c_str(), "--cmdOutFd={}");
543 if (!g_cmdIn || (g_cmdIn == INVALID_HANDLE_VALUE) || !g_cmdOut ||
544 (g_cmdOut == INVALID_HANDLE_VALUE))
545 {
546 std::cerr << "failed to import the cmdIn/cmdOut channel handles" << std::endl;
547 return 1;
548 }
549
550 /* must be registered before QApplication is constructed. CorsEnabled adds the scheme to
551 * Chromium's CORS-enabled/"web-safe" scheme list, which its network stack also consults to
552 * decide whether an https: page is allowed to redirect into it - without this, Chromium
553 * rejects the AAD redirect with net::ERR_UNSAFE_REDIRECT before we ever see it. LocalScheme
554 * (file:-like privilege) is deliberately NOT set: that made things worse, since Chromium
555 * specifically disallows network content redirecting into a local-privilege scheme. */
556 QWebEngineUrlScheme brokerScheme(kBrokerScheme);
557 brokerScheme.setSyntax(QWebEngineUrlScheme::Syntax::Host);
558 brokerScheme.setFlags(QWebEngineUrlScheme::CorsEnabled);
559 QWebEngineUrlScheme::registerScheme(brokerScheme);
560
561 QApplication app(argc, argv);
562 /* QApplication defaults to quitting as soon as its last top-level window (here, the popup)
563 * is closed - which would call qApp->quit() straight from closeEvent(), bypassing
564 * Session::quit() (and the `delete window` it does before quitting) entirely: the app would
565 * exit with the AuthWindow/QWebEnginePage still alive, triggering QtWebEngine's "Release of
566 * profile requested but WebEnginePage still not deleted" warning at teardown. The window is
567 * deliberately kept alive (just hidden) across navigate() calls anyway - see Session's class
568 * comment - so only our own explicit quit(), driven by the "exit" JSON-RPC notification,
569 * should end the process. */
570 app.setQuitOnLastWindowClosed(false);
571
572 Session session;
573 BrokerSchemeHandler brokerHandler(session);
574 QWebEngineProfile::defaultProfile()->installUrlSchemeHandler(QByteArray(kBrokerScheme),
575 &brokerHandler);
576
577 std::thread reader(readerLoop, std::ref(session));
578
579 const int rc = QApplication::exec();
580 reader.join();
581
582 (void)CloseHandle(g_cmdIn);
583 (void)CloseHandle(g_cmdOut);
584 return rc;
585}
586
587#include "main.moc"