diff options
| author | Simon Rettberg | 2025-09-22 14:18:00 +0200 |
|---|---|---|
| committer | Simon Rettberg | 2025-09-22 14:18:00 +0200 |
| commit | ef6063ba3614b3f1a44179f82fbc0e9bdf47fe99 (patch) | |
| tree | 290c7bf881e5c01ad818351e36ab5f87821ec766 /src | |
| parent | Try to get timeout logic under control (diff) | |
| download | slxgreeter-ef6063ba3614b3f1a44179f82fbc0e9bdf47fe99.tar.gz slxgreeter-ef6063ba3614b3f1a44179f82fbc0e9bdf47fe99.tar.xz slxgreeter-ef6063ba3614b3f1a44179f82fbc0e9bdf47fe99.zip | |
Port from QWebKit to QWebEngine
Port done by clion + GPT-5
Diffstat (limited to 'src')
| -rw-r--r-- | src/global.cpp | 2 | ||||
| -rw-r--r-- | src/webview.cpp | 467 | ||||
| -rw-r--r-- | src/webview.h | 64 |
3 files changed, 336 insertions, 197 deletions
diff --git a/src/global.cpp b/src/global.cpp index ce166c6..f20b4b5 100644 --- a/src/global.cpp +++ b/src/global.cpp @@ -148,7 +148,7 @@ QString Global::getCombinedIdpWhitelist() QFileInfoList fileInfoList = configDir.entryInfoList(QStringList() << "*.idp", QDir::Files); QSet<QString> list; - for (QFileInfo fileInfo : fileInfoList) { + for (const QFileInfo& fileInfo : fileInfoList) { QString filePath = fileInfo.absoluteFilePath(); QFile f(filePath); if (!f.open(QFile::ReadOnly)) diff --git a/src/webview.cpp b/src/webview.cpp index 745d700..1e4a3a4 100644 --- a/src/webview.cpp +++ b/src/webview.cpp @@ -1,196 +1,221 @@ #include "webview.h" -#include "nam.h" + #include "global.h" -#include <QWebFrame> -#include <QNetworkReply> +#include <QAction> #include <QMessageBox> #include <QTimer> #include <QUrlQuery> #include <QCryptographicHash> #include <QCursor> -#include <QWebHistory> -#include <QNetworkCookieJar> -#include <QWebElement> #include <QRegularExpression> -#include <QWebPage> -#include <QWebFrame> +#include <QDateTime> +#include <QDebug> +#include <QMenu> +#include <QContextMenuEvent> -static QRegularExpression urlListToRegExp(const QStringList &list); +#include <QWebEngineHistory> +#include <QWebEngineSettings> +#include <QWebEngineProfile> +#include <QWebEngineCookieStore> +#include <QWebEngineDownloadItem> +#include <QWebEngineUrlRequestInterceptor> +#include <QWebEngineScriptCollection> -// Override user-agent to make it appear mobile -class UaWebPage : public QWebPage +// Add: custom page to catch console messages from JS +class ActivityPage : public QWebEnginePage { public: - static QRegularExpression re; + explicit ActivityPage(QWebEngineProfile *profile, QObject *parent, std::function<void()> onActivity) + : QWebEnginePage(profile, parent), _onActivity(std::move(onActivity)) {} - QString userAgentForUrl(const QUrl &url) const override { - return QWebPage::userAgentForUrl(url).replace(re, "Mobile \\1"); +protected: + void javaScriptConsoleMessage(JavaScriptConsoleMessageLevel level, + const QString &message, int lineNumber, + const QString &sourceID) override + { + Q_UNUSED(level); + Q_UNUSED(lineNumber); + Q_UNUSED(sourceID); + // Recognize our activity marker + if (message == QLatin1String("LOG_USER_ACTIVITY")) { + qDebug() << "User activity detected"; + if (_onActivity) _onActivity(); + } + // fall through to default behavior + QWebEnginePage::javaScriptConsoleMessage(level, message, lineNumber, sourceID); } + +private: + std::function<void()> _onActivity; }; -QRegularExpression UaWebPage::re("(\\S+)$"); +static QRegularExpression urlListToRegExp(const QStringList &list); -WebView::WebView(QWidget* parent) - : QWebView(parent), - _timerAbortMessage(new QTimer(this)), - _abortedDownload(false), - _inErrorState(false), - _timerReset(new QTimer(this)), - _firstLoad(false) -{ - auto p = new UaWebPage; - if (!Global::getCombinedIdpWhitelist().trimmed().isEmpty()) { - QObject::connect(p, &UaWebPage::frameCreated, [this](QWebFrame *frame) { - QObject::connect(frame, &QWebFrame::javaScriptWindowObjectCleared, [this, frame]() { - this->jsInjector(frame); - }); - }); - } - this->setPage(p); - _timerAbortMessage->setSingleShot(true); - _timerReset->setSingleShot(true); - connect(page(), SIGNAL(windowCloseRequested()), this, SLOT(windowCloseRequested())); - auto bl = Global::urlBlacklist(); - auto wl = Global::urlWhitelist(); - page()->setNetworkAccessManager(new SlxNetworkAccessManager(urlListToRegExp(bl), - urlListToRegExp(wl), this)); - page()->setForwardUnsupportedContent(true); - page()->settings()->setAttribute(QWebSettings::LocalStorageEnabled, true); - //page()->settings()->setAttribute(QWebSettings::DeveloperExtrasEnabled, true); - connect(page(), SIGNAL(unsupportedContent(QNetworkReply*)),this,SLOT(unsupportedContent(QNetworkReply*))); - connect(page(), SIGNAL(downloadRequested(QNetworkRequest)),this,SLOT(downloadRequest(QNetworkRequest))); - connect(_timerAbortMessage, &QTimer::timeout, this, &WebView::downloadDeniedMessage); - connect(_timerReset, &QTimer::timeout, this, [this]() { - this->resetBrowserAndTimeout(); - emit triggerReset(tr("Inactivity Timeout")); - }); - connect(this, &QWebView::loadFinished, this, &WebView::onLoadFinished); -} +// URL interceptor for black-/whitelist +class SlxUrlRequestInterceptor : public QWebEngineUrlRequestInterceptor +{ +public: + SlxUrlRequestInterceptor(const QRegularExpression& blackList, const QRegularExpression& whiteList) + : _black(blackList), _white(whiteList) {} -void WebView::resetBrowserAndTimeout() + void interceptRequest(QWebEngineUrlRequestInfo &info) override { + const QString u = info.requestUrl().toString(); + if (_white.isValid() && !_white.match(u).hasMatch()) { + info.block(true); + return; + } + if (_black.isValid() && _black.match(u).hasMatch()) { + info.block(true); + return; + } + } +private: + QRegularExpression _black, _white; +}; + +WebView::WebView(QWidget* parent) + : QWebEngineView(parent), + _timerAbortMessage(new QTimer(this)), + _abortedDownload(false), + _inErrorState(false), + _timerReset(new QTimer(this)), + _firstLoad(false), + _profile(new QWebEngineProfile(this)) { - _timerReset->stop(); - _timerAbortMessage->stop(); - this->stop(); - this->page()->mainFrame()->setContent(""); + // Configure profile, no persistent cookies or cache + _profile->setPersistentCookiesPolicy(QWebEngineProfile::NoPersistentCookies); + _profile->setHttpCacheType(QWebEngineProfile::MemoryHttpCache); + + { + // Make sure we get mobile pages if applicable, since the window is rather small + QString ua = _profile->httpUserAgent(); + ua.replace(QRegularExpression("(\\S+)$"), "Mobile \\1"); + _profile->setHttpUserAgent(ua); + } + + { + // Any url filtering, since many pages allow you to escape through different links + auto bl = Global::urlBlacklist(); + auto wl = Global::urlWhitelist(); + _profile->setRequestInterceptor(new SlxUrlRequestInterceptor(urlListToRegExp(bl), urlListToRegExp(wl))); + } + + // Use our custom page to receive activity signals from injected JS + auto *p = new ActivityPage(_profile, this, [this]() { + this->resetTimeout(); + }); + setPage(p); + + // Settings + page()->settings()->setAttribute(QWebEngineSettings::LocalStorageEnabled, true); + + // Also reset on scrolls (native signal) + connect(page(), &QWebEnginePage::scrollPositionChanged, this, [this](const QPointF &) { + this->resetTimeout(); + }); + + // Block downloads + connect(_profile, &QWebEngineProfile::downloadRequested, this, [this](QWebEngineDownloadItem *item) { + Q_UNUSED(item); + _abortedDownload = true; + _timerAbortMessage->start(1); + item->cancel(); + }); + + // Handle window close requests, simulate back navigation + connect(page(), &QWebEnginePage::windowCloseRequested, this, &WebView::windowCloseRequested); + + _timerAbortMessage->setSingleShot(true); + _timerReset->setSingleShot(true); + + connect(_timerAbortMessage, &QTimer::timeout, this, &WebView::downloadDeniedMessage); + connect(_timerReset, &QTimer::timeout, this, [this]() { + this->resetBrowserAndTimeout(); + emit triggerReset(tr("Inactivity Timeout")); + }); + connect(this, &QWebEngineView::loadFinished, this, &WebView::onLoadFinished); + + // JS-Injection + installJsInjectionScript(); } -void WebView::jsInjector(QWebFrame *frame) +void WebView::resetBrowserAndTimeout() { - QString str = Global::getCombinedIdpWhitelist().replace( - QRegularExpression("[^\\w. /:-]", QRegularExpression::UseUnicodePropertiesOption), - QStringLiteral("")); - frame->evaluateJavaScript(QStringLiteral("var slxIdpFilter ='") + str + QStringLiteral("'")); + _timerReset->stop(); + _timerAbortMessage->stop(); + this->stop(); + this->setHtml(QString()); } void WebView::windowCloseRequested() { - // If we have an old URL stored on the stack, navigate back to it, otherwise we return and nothing happens - if (_urls.empty()) - return; - QUrl url = _urls.pop(); - page()->mainFrame()->load(url); + if (_urls.empty()) + return; + const QUrl url = _urls.pop(); + this->setUrl(url); } -QWebView* WebView::createWindow(QWebPage::WebWindowType) +QWebEngineView* WebView::createWindow(QWebEnginePage::WebWindowType) { - // Remember current URL, then return the current Web View so no new window opens - _urls.push(this->url()); - return this; + // Open in same window and push old URL to stack, so we can navigate back on close requests + _urls.push(this->url()); + return this; } void WebView::mousePressEvent(QMouseEvent* ev) { - QWebView::mousePressEvent(ev); - resetTimeout(); + QWebEngineView::mousePressEvent(ev); + resetTimeout(); } void WebView::keyPressEvent(QKeyEvent* ev) { - QWebView::keyPressEvent(ev); - resetTimeout(); + QWebEngineView::keyPressEvent(ev); + resetTimeout(); } void WebView::wheelEvent(QWheelEvent* ev) { - QWebView::wheelEvent(ev); - resetTimeout(); + QWebEngineView::wheelEvent(ev); + resetTimeout(); } -void WebView::resetTimeout() +void WebView::resetTimeout() const { - if (!_inErrorState) { - _timerReset->start(60000); - } -} - -void WebView::unsupportedContent(QNetworkReply* rep) -{ - _abortedDownload = true; - rep->abort(); - rep->deleteLater(); - _timerAbortMessage->start(1); -} - -void WebView::downloadRequest(QNetworkRequest) -{ - _timerAbortMessage->start(1); + if (!_inErrorState) { + _timerReset->start(60000); + } } void WebView::downloadDeniedMessage() { - QMessageBox::warning(this->parentWidget(), QString::fromUtf8("Denied"), - QString::fromUtf8("The requested action triggered a download, which is not allowed.\n\n" - "Diese Aktion löst einen Download aus, was nicht erlaubt ist.")); + QMessageBox::warning(this->parentWidget(), QString::fromUtf8("Denied"), + QString::fromUtf8("The requested action triggered a download, which is not allowed.\n\n" + "Diese Aktion löst einen Download aus, was nicht erlaubt ist.")); } void WebView::onLoadFinished(bool ok) { - if (_abortedDownload || !ok) { - _abortedDownload = false; - _inErrorState = true; - _timerReset->start(10000); - return; - } - _inErrorState = false; - auto user = this->page()->mainFrame()->documentElement().findFirst("#bwlp-username"); - auto pass = this->page()->mainFrame()->documentElement().findFirst("#bwlp-password"); - auto err = this->page()->mainFrame()->documentElement().findFirst("#bwlp-error"); - auto hash = this->page()->mainFrame()->documentElement().findFirst("#bwlp-hash"); - auto adminToken = this->page()->mainFrame()->documentElement().findFirst("#bwlp-cow-token"); - if (!user.isNull() && !pass.isNull() && !hash.isNull()) { - if (hash.toPlainText() != QCryptographicHash::hash(_token.toLatin1(), QCryptographicHash::Md5).toHex()) { - qDebug() << " *** Invalid security hash ***"; - emit triggerReset("Invalid Hash"); - return; - } - auto ustr = user.toPlainText(); - auto upass = pass.toPlainText(); - if (Global::isValidShibCreds(ustr, upass)) { - QString token = adminToken.toPlainText(); - if (!token.isEmpty()) { - Global::writeCowToken(ustr, token); - } - emit startAuthentication(ustr, "shib=" + _token + upass); - } else { - emit triggerReset("Invalid user or passhash format"); - } - } else if (!err.isNull()) { - this->stop(); - this->page()->mainFrame()->setContent(""); - emit triggerReset(err.toPlainText()); - } else { - _timerReset->start(60000); - } - if (_firstLoad) { - _firstLoad = false; - this->page()->history()->clear(); - this->history()->clear(); - } + if (_abortedDownload || !ok) { + _abortedDownload = false; + _inErrorState = true; + _timerReset->start(10000); + return; + } + _inErrorState = false; + + // Check if this is the result of the auth process + evaluateAuthDom(); + + if (_firstLoad) { + _firstLoad = false; + if (this->history()) + this->history()->clear(); + } } -void WebView::reset(const QString baseUrl) +void WebView::reset(const QString& baseUrl) { QUrl url(baseUrl); QUrlQuery q(url.query()); @@ -203,7 +228,11 @@ void WebView::reset(const QString baseUrl) q.addQueryItem("token", _token); url.setQuery(q); _urls.clear(); - this->page()->networkAccessManager()->setCookieJar(new QNetworkCookieJar); + + // Clear all cookies + if (_profile && _profile->cookieStore()) + _profile->cookieStore()->deleteAllCookies(); + this->setUrl(url); _firstLoad = true; _timerAbortMessage->stop(); @@ -212,33 +241,135 @@ void WebView::reset(const QString baseUrl) static QRegularExpression urlListToRegExp(const QStringList &list) { - if (list.isEmpty()) - return QRegularExpression("(["); // Return an invalid regex, we use .isValid to check if the list is to be used - // We search in the escaped string, so actually look for \*\* and \* - // Capture char before that because it must not be another backslash, as that - // means the star was already escaped in the list. - // Since these are C strings there are some additional backslashes here. - static const QRegularExpression STARSTAR("(^|[^\\\\])\\\\\\*\\\\\\*"); - static const QRegularExpression STAR("(^|[^\\\\])\\\\\\*"); - static const QRegularExpression QUEST("(^|[^\\\\])\\\\\\?"); - static const QString STARSTAR_REP("\\1.*"); - static const QString STAR_REP("\\1[^/]*"); - static const QString QUEST_REP("\\1.?"); - QStringList regexes; // All my regex's live in Regtexas - for (const QString &str : list) { - QString mangled; - if (str.contains(QLatin1String("//"))) { - mangled = str; - } else if (str.contains(QLatin1Char('/')) || str.contains(QLatin1String("**"))) { - mangled = "*//" + str; - } else { - mangled = "*//" + str + "/**"; - } - mangled = QRegularExpression::escape(mangled); - mangled = mangled.replace(STARSTAR, STARSTAR_REP).replace(STAR, STAR_REP) - .replace(QUEST, QUEST_REP); - regexes << mangled; - } - qDebug() << regexes; - return QRegularExpression("^(" + regexes.join('|') + ")$"); + if (list.isEmpty()) + return QRegularExpression("(["); // Return an invalid regex, we use .isValid to check if the list is to be used. + // We search in the escaped string, so actually look for \*\* and \* + // Capture char before that because it must not be another backslash, as that + // means the star was already escaped in the list. + static const QRegularExpression STARSTAR(R"((^|[^\\])\\\*\\\*)"); + static const QRegularExpression STAR(R"((^|[^\\])\\\*)"); + static const QRegularExpression QUEST(R"((^|[^\\])\\\?)"); + static const QString STARSTAR_REP("\\1.*"); + static const QString STAR_REP("\\1[^/]*"); + static const QString QUEST_REP("\\1.?"); + QStringList regexes; + for (const QString &str : list) { + QString mangled; + if (str.contains(QLatin1String("//"))) { + mangled = str; + } else if (str.contains(QLatin1Char('/')) || str.contains(QLatin1String("**"))) { + mangled = "*//" + str; + } else { + mangled = "*//" + str + "/**"; + } + mangled = QRegularExpression::escape(mangled); + mangled = mangled.replace(STARSTAR, STARSTAR_REP).replace(STAR, STAR_REP) + .replace(QUEST, QUEST_REP); + regexes << mangled; + } + qDebug() << regexes; + return QRegularExpression("^(" + regexes.join('|') + ")$"); +} + +void WebView::installJsInjectionScript() +{ + // If we should filter the list of allowed IdPs, inject the list into + // the page as JavaScript + QString str = Global::getCombinedIdpWhitelist().replace( + QRegularExpression("[^\\w. /:-]", QRegularExpression::UseUnicodePropertiesOption), + QString()); + QWebEngineScript script; + script.setName(QStringLiteral("slxIdpFilterInjector")); + script.setInjectionPoint(QWebEngineScript::DocumentCreation); + script.setWorldId(QWebEngineScript::MainWorld); + script.setRunsOnSubFrames(true); + script.setSourceCode(QStringLiteral("var slxIdpFilter ='") + str + QStringLiteral("';")); + page()->scripts().insert(script); + + // Inject activity listeners to signal user interaction back to C++ + QWebEngineScript activityScript; + activityScript.setName(QStringLiteral("slxUserActivity")); + activityScript.setInjectionPoint(QWebEngineScript::DocumentCreation); + activityScript.setWorldId(QWebEngineScript::MainWorld); + activityScript.setRunsOnSubFrames(true); + activityScript.setSourceCode(QStringLiteral( + "(function(){\n" + " var last=0; function ping(){\n" + " var now=Date.now(); if(now-last<2000) return; last=now;\n" + " try{console.debug('LOG_USER_ACTIVITY');}catch(e){}\n" + " }\n" + " var evts=['mousedown','mouseup','click','keydown','keyup','wheel','touchstart','touchend','scroll'];\n" + " evts.forEach(function(ev){\n" + " window.addEventListener(ev, ping, {passive:true, capture:true});\n" + " });\n" + "})();\n" + )); + page()->scripts().insert(activityScript); +} + +void WebView::contextMenuEvent(QContextMenuEvent* ev) +{ + // Build the default menu + QMenu* menu = page()->createStandardContextMenu(); + + // Find and remove/hide the "View source" action + QAction* viewSource = page()->action(QWebEnginePage::ViewSource); + if (viewSource) { + viewSource->setVisible(false); // or: menu->removeAction(viewSource); + viewSource->setEnabled(false); + } + + // Show the customized menu + menu->exec(ev->globalPos()); + menu->deleteLater(); + ev->accept(); +} + +void WebView::evaluateAuthDom() +{ + // See if expected auth fields are in the document. If so, we want to trigger the + // authentication process using those fields. + const QString js = QStringLiteral( + "(function(){" + " function t(sel){var e=document.querySelector(sel);return e?e.textContent:'';}" + " return {" + " user: t('#bwlp-username')," + " pass: t('#bwlp-password')," + " err: t('#bwlp-error')," + " hash: t('#bwlp-hash')," + " adminToken: t('#bwlp-cow-token')" + " };" + "})();" + ); + + page()->runJavaScript(js, [this](const QVariant &v){ + const auto map = v.toMap(); + const QString user = map.value(QStringLiteral("user")).toString().trimmed(); + const QString pass = map.value(QStringLiteral("pass")).toString(); + const QString err = map.value(QStringLiteral("err")).toString(); + const QString hash = map.value(QStringLiteral("hash")).toString(); + const QString adminToken = map.value(QStringLiteral("adminToken")).toString(); + + if (!user.isEmpty() && !pass.isEmpty() && !hash.isEmpty()) { + if (hash != QCryptographicHash::hash(_token.toLatin1(), QCryptographicHash::Md5).toHex()) { + qDebug() << " *** Invalid security hash ***"; + emit triggerReset(QStringLiteral("Invalid Hash")); + return; + } + if (Global::isValidShibCreds(user, pass)) { + if (!adminToken.isEmpty()) { + Global::writeCowToken(user, adminToken); + } + emit startAuthentication(user, QStringLiteral("shib=") + _token + pass); + } else { + emit triggerReset(QStringLiteral("Invalid user or passhash format")); + } + } else if (!err.isEmpty()) { + this->stop(); + this->setHtml(QString()); + emit triggerReset(err); + } else { + _timerReset->start(60000); + } + }); } diff --git a/src/webview.h b/src/webview.h index 48c0ceb..71d9e87 100644 --- a/src/webview.h +++ b/src/webview.h @@ -2,55 +2,63 @@ #define WEBVIEW_H_ #include <QStack> -#include <QWebView> #include <QNetworkRequest> +#include <QRegularExpression> + +#include <QWebEngineView> +#include <QWebEnginePage> +#include <QWebEngineProfile> +#include <QWebEngineScript> -class QNetworkReply; class QTimer; -class QWebFrame; /** * Make sure pages that want to load in a new tab are actually loaded in the same page, * and remember the previous URL in case the "new tab" requests to be closed. */ -class WebView : public QWebView +class WebView : public QWebEngineView { Q_OBJECT public: - WebView(QWidget* parent = NULL); - void reset(const QString baseUrl); + explicit WebView(QWidget* parent = nullptr); + void reset(const QString& baseUrl); - void resetBrowserAndTimeout(); + void resetBrowserAndTimeout(); protected: - QWebView *createWindow(QWebPage::WebWindowType) override; - void mousePressEvent(QMouseEvent*) override; - void keyPressEvent(QKeyEvent*) override; - void wheelEvent(QWheelEvent*) override; - - void resetTimeout(); + QWebEngineView *createWindow(QWebEnginePage::WebWindowType) override; + void mousePressEvent(QMouseEvent*) override; + void keyPressEvent(QKeyEvent*) override; + void wheelEvent(QWheelEvent*) override; - void jsInjector(QWebFrame *frame); + void resetTimeout() const; signals: - void triggerReset(const QString &message); - void startAuthentication(const QString &user, const QString &pass); + void triggerReset(const QString &message); + void startAuthentication(const QString &user, const QString &pass); protected slots: - void windowCloseRequested(); - void unsupportedContent(QNetworkReply*); - void downloadRequest(QNetworkRequest); - void downloadDeniedMessage(); - void onLoadFinished(bool ok); + void windowCloseRequested(); + void downloadDeniedMessage(); + void onLoadFinished(bool ok); private: - QStack<QUrl> _urls; - QTimer *_timerAbortMessage; - bool _abortedDownload; - bool _inErrorState; - QString _token; - QTimer *_timerReset; - bool _firstLoad; + void installJsInjectionScript(); + +void contextMenuEvent(QContextMenuEvent *ev); + +void evaluateAuthDom(); + +private: + QStack<QUrl> _urls; + QTimer *_timerAbortMessage; + bool _abortedDownload; + bool _inErrorState; + QString _token; + QTimer *_timerReset; + bool _firstLoad; + + QWebEngineProfile *_profile; }; #endif |
