diff options
-rw-r--r-- | src/global.cpp | 23 | ||||
-rw-r--r-- | src/global.h | 4 | ||||
-rw-r--r-- | src/loginform.cpp | 5 | ||||
-rw-r--r-- | src/qrlogin.cpp | 175 | ||||
-rw-r--r-- | src/qrlogin.h | 7 | ||||
-rw-r--r-- | src/webview.cpp | 12 |
6 files changed, 156 insertions, 70 deletions
diff --git a/src/global.cpp b/src/global.cpp index 4efccad..ab2519b 100644 --- a/src/global.cpp +++ b/src/global.cpp @@ -7,6 +7,8 @@ #include <QDebug> #include <QCoreApplication> #include <QStringList> +#include <QCryptographicHash> +#include <QRegularExpression> bool Global::m_testMode = false; @@ -116,3 +118,24 @@ QStringList Global::urlWhitelist() return QStringList(); return loadUrlList(path); } + +void Global::writeCowToken(const QString &user, const QString &token) +{ + QString userHash = QString::fromLocal8Bit(QCryptographicHash::hash(user.toLocal8Bit(), QCryptographicHash::Md5).toHex()); + QFile file(QLatin1String("/run/openslx/lightdm/") + userHash); + if (file.open(QFile::WriteOnly | QFile::Truncate)) { + file.write(token.toLocal8Bit()); + file.close(); + file.setPermissions(QFileDevice::ReadOwner | QFileDevice::WriteOwner); + } +} + +bool Global::isValidShibCreds(const QString &ustr, const QString &upass) +{ + static QRegularExpression R_USER("^[a-z_A-Z][a-zA-Z0-9_@.-]{1,32}$"); + static QRegularExpression R_PASS("^[a-z0-9]{1,32}$"); + + return ustr.contains('@') + && R_USER.match(ustr).hasMatch() + && R_PASS.match(upass).hasMatch(); +} diff --git a/src/global.h b/src/global.h index bdc79cd..d581c07 100644 --- a/src/global.h +++ b/src/global.h @@ -52,6 +52,10 @@ public: static QStringList urlBlacklist(); + static void writeCowToken(const QString &user, const QString &token); + + static bool isValidShibCreds(const QString &ustr, const QString &upass); + private: static bool m_testMode; static QLightDM::Greeter *m_Greeter; diff --git a/src/loginform.cpp b/src/loginform.cpp index 230f408..3ca0ec4 100644 --- a/src/loginform.cpp +++ b/src/loginform.cpp @@ -409,11 +409,12 @@ void LoginForm::leaveDropDownActivated(int index) void LoginForm::onMessage(QString message, QLightDM::Greeter::MessageType type) { - if (type == QLightDM::Greeter::MessageType::MessageTypeError) { + bool err = type == QLightDM::Greeter::MessageType::MessageTypeError; + if (err) { ui->passwordInput->clear(); } std::cerr << "Message: " << message.toStdString() << std::endl; - showMessage(message, false); + showMessage(QLatin1String("[G] ") + message, err); clearMsg = true; } diff --git a/src/qrlogin.cpp b/src/qrlogin.cpp index 1e44668..c562e98 100644 --- a/src/qrlogin.cpp +++ b/src/qrlogin.cpp @@ -1,5 +1,6 @@ #include "qrlogin.h" #include "settings.h" +#include "global.h" #include <unistd.h> @@ -52,6 +53,10 @@ void QrLogin::loadQrCode(QLabel *dest) QNetworkReply *reply = nam->get(QNetworkRequest(Settings::shibUrl() + QLatin1String("?action=qrgen&token=") + token)); connect(reply, &QNetworkReply::readyRead, [=]() { + if (svgData.size() > 1024000) { + reply->abort(); + return; + } svgData.append(reply->readAll()); }); connect(reply, &QNetworkReply::finished, [reply, this]() { @@ -63,74 +68,17 @@ void QrLogin::loadQrCode(QLabel *dest) emit triggerReset(QLatin1String("QRCode Error: ") + QString::fromUtf8(svgData)); return; } - // Render at 3x size because QSvgRenderer adds ugly artefacts between pixels/modules - QPixmap pm(this->dest->size() * SVG_RENDER_SCALE); - pm.fill(Qt::white); - QPainter painter(&pm); - QRect square = pm.rect(); - square.setSize(pm.size()); - int diff = square.width() - square.height(); - if (diff > 0) { - square.setLeft(diff / 2); - square.setWidth(square.height()); - } else if (diff < 0) { - square.setTop(-diff / 2); - square.setHeight(square.width()); - } - square.adjust(6 * SVG_RENDER_SCALE, 6 * SVG_RENDER_SCALE, -6 * SVG_RENDER_SCALE, -6 * SVG_RENDER_SCALE); - QSvgRenderer(svgData).render(&painter, square); - painter.end(); - this->dest->setPixmap(pm.scaled(pm.size() / SVG_RENDER_SCALE, Qt::KeepAspectRatio, Qt::SmoothTransformation)); - this->dest->update(); - connect(timer, &QTimer::timeout, [=]() { - if (elapsed->hasExpired(300000)) { - QObject::disconnect(timer, nullptr, nullptr, nullptr); - timer->stop(); - emit triggerReset(QLatin1String("Timeout: QR Code expired")); - return; - } - QNetworkReply *pr = - nam->get(QNetworkRequest(Settings::shibUrl() + QLatin1String("?action=qrpoll&token=") + token)); - connect(pr, &QNetworkReply::finished, [=]() { - int code = pr->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); - if (pr->error() != QNetworkReply::NoError) { - QObject::disconnect(timer, nullptr, nullptr, nullptr); - timer->stop(); - emit triggerReset(QString::asprintf("QR Error(%d): ", code) + QString::fromUtf8(pr->readAll())); - return; - } - if (code == 204) { - int rem = (QR_VALID_MS - elapsed->elapsed()) / 1000; - if (rem > 60) { - emit updateStatus(QString::asprintf("Code valid for: ~%d mins", (rem + 30) / 60)); - } else { - if (rem >= 58) { - QPixmap copy = this->dest->pixmap(Qt::ReturnByValue); - QImage img = copy.toImage(); - uchar *b = img.bits(); - for (int i = 0; i < img.sizeInBytes(); ++i) { - if (b[i] < 128) b[i] += 128; - } - this->dest->setPixmap(QPixmap::fromImage(img)); - } - emit updateStatus(QString::asprintf("Code valid for: %d s", rem)); - } - return; - } - QObject::disconnect(timer, nullptr, nullptr, nullptr); - timer->stop(); - auto lines = QString::fromUtf8(pr->readAll()).split('\n'); - if (lines.size() >= 2) { - emit startAuthentication(lines[0], QLatin1String("shib=") + lines[1]); - } else { - emit triggerReset(QLatin1String("QR: Invalid reply received")); - } - }); - }); + + renderReceivedSvg(); + + connect(timer, &QTimer::timeout, this, &QrLogin::pollQrCodeStatus); + elapsed->start(); timer->setSingleShot(false); timer->start(2000); }); + + // Safeguard: 10s timeout in case the NAMs transferTimeout doesn't work connect(timer, &QTimer::timeout, [=]() { QObject::disconnect(reply, nullptr, nullptr, nullptr); QObject::disconnect(timer, nullptr, nullptr, nullptr); @@ -141,6 +89,105 @@ void QrLogin::loadQrCode(QLabel *dest) timer->start(10000); } +/** + * While the QR Login is active, i.e. the code still visible on screen, + * poll the status from the server every 2 seconds. Also, timeout and + * fall back to login screen after 5 minutes. + */ +void QrLogin::pollQrCodeStatus() +{ + if (elapsed->hasExpired(300000)) { + QObject::disconnect(timer, nullptr, nullptr, nullptr); + timer->stop(); + emit triggerReset(QLatin1String("Timeout: QR Code expired")); + return; + } + + QNetworkReply *pr = + nam->get(QNetworkRequest(Settings::shibUrl() + QLatin1String("?action=qrpoll&token=") + token)); + + connect(pr, &QNetworkReply::finished, [=]() { + int code = pr->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); + + if (pr->error() != QNetworkReply::NoError) { + QObject::disconnect(timer, nullptr, nullptr, nullptr); + timer->stop(); + emit triggerReset(QString::asprintf("QR Error(%d): ", code) + QString::fromUtf8(pr->readAll())); + return; + } + if (code == 204) { + handleNoContentReceived(); + return; + } + QObject::disconnect(timer, nullptr, nullptr, nullptr); + timer->stop(); + auto lines = QString::fromUtf8(pr->readAll()).split('\n'); + handleAuthReceived(lines); + }); +} + +void QrLogin::renderReceivedSvg() +{ + // Render at 3x size because QSvgRenderer adds ugly artefacts between pixels/modules + QPixmap pm(this->dest->size() * SVG_RENDER_SCALE); + pm.fill(Qt::white); + QPainter painter(&pm); + QRect square = pm.rect(); + square.setSize(pm.size()); + int diff = square.width() - square.height(); + if (diff > 0) { + square.setLeft(diff / 2); + square.setWidth(square.height()); + } else if (diff < 0) { + square.setTop(-diff / 2); + square.setHeight(square.width()); + } + // Add quiet zone + square.adjust(6 * SVG_RENDER_SCALE, 6 * SVG_RENDER_SCALE, -6 * SVG_RENDER_SCALE, -6 * SVG_RENDER_SCALE); + QSvgRenderer(svgData).render(&painter, square); + painter.end(); + this->dest->setPixmap(pm.scaled(pm.size() / SVG_RENDER_SCALE, Qt::KeepAspectRatio, Qt::SmoothTransformation)); + this->dest->update(); +} + +/** + * Received auth data for QR Code, trigger login. + */ +void QrLogin::handleAuthReceived(const QStringList &lines) +{ + if (lines.size() >= 2) { + if (lines.size() >= 3 && !lines[2].isEmpty()) { + // Admin token for editing VMs + Global::writeCowToken(lines[0], lines[2]); + } + emit startAuthentication(lines[0], QLatin1String("shib=") + lines[1]); + } else { + emit triggerReset(QLatin1String("QR: Invalid reply received")); + } +} + +/** + * Received HTTP 204 No Content, i.e. QR Code is still valid, but nobody logged in. + */ +void QrLogin::handleNoContentReceived() +{ + int rem = (QR_VALID_MS - elapsed->elapsed()) / 1000; + if (rem > 60) { + emit updateStatus(QString::asprintf("Code valid for: ~%d mins", (rem + 30) / 60)); + } else { + if (rem >= 58) { + QPixmap copy = this->dest->pixmap(Qt::ReturnByValue); + QImage img = copy.toImage(); + uchar *b = img.bits(); + for (int i = 0; i < img.sizeInBytes(); ++i) { + if (b[i] < 128) b[i] += 128; + } + this->dest->setPixmap(QPixmap::fromImage(img)); + } + emit updateStatus(QString::asprintf("Code valid for: %d s", rem)); + } +} + static QString toBase32(const QByteArray &data) { static const QString base32Chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567_-="; diff --git a/src/qrlogin.h b/src/qrlogin.h index fa8cc82..3c3c61d 100644 --- a/src/qrlogin.h +++ b/src/qrlogin.h @@ -21,7 +21,14 @@ signals: void startAuthentication(const QString &user, const QString &pass); void updateStatus(const QString &msg); +private slots: + void pollQrCodeStatus(); + private: + void handleAuthReceived(const QStringList &lines); + void handleNoContentReceived(); + void renderReceivedSvg(); + QLabel *dest; QTimer *timer; QNetworkAccessManager *nam; diff --git a/src/webview.cpp b/src/webview.cpp index 54d19eb..9ebc1ba 100644 --- a/src/webview.cpp +++ b/src/webview.cpp @@ -15,9 +15,6 @@ #include <QRegularExpression> #include <QWebPage> -static QRegularExpression R_USER("^[a-z_A-Z][a-zA-Z0-9_@.-]{1,32}$"); -static QRegularExpression R_PASS("^[a-z0-9]{1,32}$"); - static QRegularExpression urlListToRegExp(const QStringList &list); // Override user-agent to make it appear mobile @@ -137,6 +134,7 @@ void WebView::onLoadFinished(bool ok) 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 ***"; @@ -145,8 +143,14 @@ void WebView::onLoadFinished(bool ok) } auto ustr = user.toPlainText(); auto upass = pass.toPlainText(); - if (ustr.contains('@') && R_USER.match(ustr).hasMatch() && R_PASS.match(upass).hasMatch()) { + 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(); |