#include "webview.h" #include "global.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include // Add: custom page to catch console messages from JS class ActivityPage : public QWebEnginePage { public: explicit ActivityPage(QWebEngineProfile *profile, QObject *parent, std::function onActivity) : QWebEnginePage(profile, parent), _onActivity(std::move(onActivity)) {} 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 _onActivity; }; static QRegularExpression urlListToRegExp(const QStringList &list); // URL interceptor for black-/whitelist class SlxUrlRequestInterceptor : public QWebEngineUrlRequestInterceptor { public: SlxUrlRequestInterceptor(const QRegularExpression& blackList, const QRegularExpression& whiteList) : _black(blackList), _white(whiteList) {} 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)) { // 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::resetBrowserAndTimeout() { _timerReset->stop(); _timerAbortMessage->stop(); this->stop(); this->setHtml(QString()); } void WebView::windowCloseRequested() { if (_urls.empty()) return; const QUrl url = _urls.pop(); this->setUrl(url); } QWebEngineView* WebView::createWindow(QWebEnginePage::WebWindowType) { // 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) { QWebEngineView::mousePressEvent(ev); resetTimeout(); } void WebView::keyPressEvent(QKeyEvent* ev) { QWebEngineView::keyPressEvent(ev); resetTimeout(); } void WebView::wheelEvent(QWheelEvent* ev) { QWebEngineView::wheelEvent(ev); resetTimeout(); } void WebView::resetTimeout() const { 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.")); } void WebView::onLoadFinished(bool ok) { 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) { QUrl url(baseUrl); QUrlQuery q(url.query()); q.addQueryItem("action", "browser"); QByteArray input; input.append((const char*)this, sizeof(*this)); input.append(QString::asprintf("%d %d", QCursor::pos().x(), QCursor::pos().y()).toUtf8()); input.append(QString::number(QDateTime::currentMSecsSinceEpoch()).toUtf8()); _token = QCryptographicHash::hash(input, QCryptographicHash::Md5).left(8).toHex(); q.addQueryItem("token", _token); url.setQuery(q); _urls.clear(); // Clear all cookies if (_profile && _profile->cookieStore()) _profile->cookieStore()->deleteAllCookies(); this->setUrl(url); _firstLoad = true; _timerAbortMessage->stop(); _timerReset->stop(); } 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. 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); } }); }