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