From 80917a8b79b0052d07060143fff346eddd189786 Mon Sep 17 00:00:00 2001 From: Simon Rettberg Date: Mon, 25 Mar 2024 17:27:15 +0100 Subject: Add support for QRCode login --- src/loginform.cpp | 60 +++++++++++++++++++- src/loginform.h | 6 ++ src/loginform.ui | 37 +++++++++++- src/qrlogin.cpp | 167 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ src/qrlogin.h | 33 +++++++++++ src/settings.h | 2 + src/snake.cpp | 65 +++++++++++++++++++-- src/snake.h | 1 + src/webview.cpp | 2 +- 9 files changed, 364 insertions(+), 9 deletions(-) create mode 100644 src/qrlogin.cpp create mode 100644 src/qrlogin.h diff --git a/src/loginform.cpp b/src/loginform.cpp index bb4fdb9..230f408 100644 --- a/src/loginform.cpp +++ b/src/loginform.cpp @@ -17,6 +17,7 @@ #include "global.h" #include "namereplace.h" #include "loginrpc.h" +#include "qrlogin.h" #undef KeyPress #undef KeyRelease #undef FocusIn @@ -44,7 +45,9 @@ LoginForm::LoginForm(QWidget *parent) : browser(nullptr), clearMsg(false), capsOn(-1), - pageCount(0) + pageCount(0), + qrcode(nullptr), + qrlogin(nullptr) { ui->setupUi(this); origSize = shibSize = sizeHint(); @@ -160,6 +163,8 @@ void LoginForm::initialize() // timer to reset the form to its original state if (Settings::resetForm() > 0) { connect(&resetFormTimer, &QTimer::timeout, [this]() { + if (ui->loginChooser->currentWidget() == ui->qrPage) + return; int idleTime = static_cast(getIdleTime(QX11Info::display())); int remaining = Settings::resetForm() * 1000 - idleTime; if (remaining <= 0) { @@ -210,6 +215,18 @@ void LoginForm::initialize() ui->shibButton->hide(); } + if (Settings::qrSessionEnabled()) { + pageCount += 2; // Fake this so we always return on timeout; + // otherwise, the qr code could expire after some time, breaking + // the login process. + if (!Settings::qrSessionButtonText().isEmpty()) { + ui->qrButton->setText(Settings::qrSessionButtonText()); + } + connect(ui->qrButton, &QAbstractButton::released, this, &LoginForm::showQrWindow); + } else { + ui->qrButton->hide(); + } + if (Settings::userSessionEnabled()) { pageCount++; } else { @@ -296,6 +313,31 @@ void LoginForm::showShibWindow() { setBrowserSize(); } +void LoginForm::showQrWindow() { + if (qrcode == nullptr) { + qrcode = new QLabel(this); + ui->vlQrCode->addWidget(qrcode); + qrcode->setBackgroundRole(QPalette::Shadow); + //connect(browser, &WebView::startAuthentication, this, &LoginForm::startAuthAs); + } + if (qrlogin != nullptr) { + disconnect(qrlogin); + qrlogin->deleteLater(); + } + qrlogin = new QrLogin(this); + connect(qrlogin, &QrLogin::startAuthentication, this, &LoginForm::startAuthAs); + connect(qrlogin, &QrLogin::updateStatus, this, &LoginForm::showLowPrioMessage); + connect(qrlogin, &QrLogin::triggerReset, [this](const QString &message) { + this->showMessage(message, true); + ui->loginChooser->setCurrentWidget(ui->welcomePage); + }); + QPixmap pm(20, 20); + pm.fill(Qt::red); + qrcode->setPixmap(pm); + qrlogin->loadQrCode(qrcode); + ui->loginChooser->setCurrentWidget(ui->qrPage); +} + void LoginForm::checkCaps() { unsigned int mask = getKeyMask(QX11Info::display()); @@ -407,6 +449,9 @@ void LoginForm::cancelLogin() std::cerr << "Was in authentication" << std::endl; Global::greeter()->cancelAuthentication(); } + if (ui->loginChooser->currentWidget() != ui->loginPage && pageCount > 1) { + ui->loginChooser->setCurrentWidget(ui->welcomePage); + } cancelLoginTimer.stop(); ui->passwordInput->clear(); enableInputs(true); @@ -425,11 +470,20 @@ void LoginForm::enableInputs(bool enable) ui->passwordInput->setEnabled(enable); ui->backButton->setEnabled(enable); ui->guestButton->setEnabled(enable); + ui->shibButton->setEnabled(enable); + ui->qrButton->setEnabled(enable); if (browser != nullptr) { browser->setEnabled(enable); } } +void LoginForm::showLowPrioMessage(QString message) +{ + if (!ui->messageLabel->text().isEmpty() && !ui->messageLabel->styleSheet().isEmpty()) + return; + showMessage(message, false); +} + void LoginForm::showMessage(QString message, bool error) { hideMessageTimer.stop(); @@ -478,6 +532,10 @@ void LoginForm::keyPressEvent(QKeyEvent *event) void LoginForm::resetForm() { std::cerr << "PageCount: " << pageCount << std::endl; + if (ui->loginChooser->currentWidget() == ui->qrPage && qrlogin != nullptr) { + qrlogin->abort(); + hideMessage(); + } if (pageCount > 1) { ui->loginChooser->setCurrentWidget(ui->welcomePage); } diff --git a/src/loginform.h b/src/loginform.h index 10e1d2b..6a05ea9 100644 --- a/src/loginform.h +++ b/src/loginform.h @@ -27,6 +27,8 @@ class LoginForm; } class WebView; +class QLabel; +class QrLogin; class LoginForm : public QWidget { @@ -50,6 +52,8 @@ public slots: private slots: void setBrowserSize(); void showShibWindow(); + void showQrWindow(); + void showLowPrioMessage(QString message); signals: void resized(); @@ -79,6 +83,8 @@ private: QSize shibSize, origSize; WebView *browser; + QLabel *qrcode; + QrLogin *qrlogin; bool clearMsg; int capsOn; diff --git a/src/loginform.ui b/src/loginform.ui index 6a0fa63..66e47fa 100644 --- a/src/loginform.ui +++ b/src/loginform.ui @@ -290,7 +290,7 @@ QComboBox QAbstractItemView::item { - + Qt::Vertical @@ -303,6 +303,25 @@ QComboBox QAbstractItemView::item { + + + + + 0 + 50 + + + + + 10 + true + + + + QRCode-Login + + + @@ -531,6 +550,22 @@ QComboBox QAbstractItemView::item { + + + + 0 + + + 0 + + + 0 + + + 0 + + + diff --git a/src/qrlogin.cpp b/src/qrlogin.cpp new file mode 100644 index 0000000..1e44668 --- /dev/null +++ b/src/qrlogin.cpp @@ -0,0 +1,167 @@ +#include "qrlogin.h" +#include "settings.h" + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define SVG_RENDER_SCALE 3 +#define QR_VALID_MS 300000 + +static QString toBase32(const QByteArray &data); + +QrLogin::QrLogin(QObject *parent) + : QObject(parent) + , dest(nullptr) + , timer(new QTimer(this)) + , nam(new QNetworkAccessManager(this)) + , elapsed(new QElapsedTimer()) +{ + QByteArray input; + input.append((const char*)this, sizeof(*this)); + input.append(QString::asprintf("%d %d %d ", QCursor::pos().x(), QCursor::pos().y(), (int)getpid()).toUtf8()); + input.append(QString::number(QDateTime::currentMSecsSinceEpoch()).toUtf8()); + token = toBase32(QCryptographicHash::hash(input, QCryptographicHash::Md5).left(10)).left(16); + nam->setAutoDeleteReplies(true); + nam->setTransferTimeout(5000); +} + +void QrLogin::abort() +{ + QObject::disconnect(timer, nullptr, nullptr, nullptr); + timer->stop(); + timer->blockSignals(true); + nam->blockSignals(true); +} + +void QrLogin::loadQrCode(QLabel *dest) +{ + this->dest = dest; + QObject::disconnect(timer, nullptr, nullptr, nullptr); + timer->stop(); + timer->setSingleShot(true); + svgData.clear(); + QNetworkReply *reply = + nam->get(QNetworkRequest(Settings::shibUrl() + QLatin1String("?action=qrgen&token=") + token)); + connect(reply, &QNetworkReply::readyRead, [=]() { + svgData.append(reply->readAll()); + }); + connect(reply, &QNetworkReply::finished, [reply, this]() { + QObject::disconnect(timer, nullptr, nullptr, nullptr); + timer->stop(); + auto err = reply->error(); + if (err != QNetworkReply::NoError) { + qDebug() << err << svgData; + 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")); + } + }); + }); + elapsed->start(); + timer->setSingleShot(false); + timer->start(2000); + }); + connect(timer, &QTimer::timeout, [=]() { + QObject::disconnect(reply, nullptr, nullptr, nullptr); + QObject::disconnect(timer, nullptr, nullptr, nullptr); + timer->stop(); + reply->abort(); + emit triggerReset(QLatin1String("Timeout: Could not get QR Code")); + }); + timer->start(10000); +} + +static QString toBase32(const QByteArray &data) +{ + static const QString base32Chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567_-="; + QString result; + int numBits = 0; + quint16 buffer = 0; + + for (int i = 0; i < data.size(); ++i) { + buffer = (buffer << 8) | static_cast(data[i]); + numBits += 8; + + while (numBits >= 5) { + numBits -= 5; + result += base32Chars[(buffer >> numBits) & 0x1F]; + } + } + + if (numBits > 0) { + buffer <<= (5 - numBits); + result += base32Chars[buffer & 0x1F]; + } + + return result; +} diff --git a/src/qrlogin.h b/src/qrlogin.h new file mode 100644 index 0000000..fa8cc82 --- /dev/null +++ b/src/qrlogin.h @@ -0,0 +1,33 @@ +#ifndef _QRLOGIN_H_ +#define _QRLOGIN_H_ + +#include + +class QLabel; +class QTimer; +class QElapsedTimer; +class QNetworkAccessManager; + +class QrLogin : public QObject +{ + Q_OBJECT +public: + explicit QrLogin(QObject *parent); + void loadQrCode(QLabel *dest); + void abort(); + +signals: + void triggerReset(const QString &message); + void startAuthentication(const QString &user, const QString &pass); + void updateStatus(const QString &msg); + +private: + QLabel *dest; + QTimer *timer; + QNetworkAccessManager *nam; + QString token; + QByteArray svgData; + QElapsedTimer *elapsed; +}; + +#endif diff --git a/src/settings.h b/src/settings.h index b00bce6..5bda9b0 100644 --- a/src/settings.h +++ b/src/settings.h @@ -48,10 +48,12 @@ public: static bool userSessionEnabled() { return s_settings->value("user-session-enabled", "1").toBool(); } static bool guestSessionEnabled() { return s_settings->value("guest-session-enabled").toBool(); } static bool shibSessionEnabled() { return s_settings->value("shib-session-enabled").toBool(); } + static bool qrSessionEnabled() { return s_settings->value("qr-session-enabled").toBool(); } static QString shibUrl() { return s_settings->value("shib-url").toString(); } static QString urlWhitelistFile() { return s_settings->value("shib-url-whitelist").toString(); } static QString urlBlacklistFile() { return s_settings->value("shib-url-blacklist").toString(); } static QString shibSessionButtonText() { return s_settings->value("shib-session-button-text").toString(); } + static QString qrSessionButtonText() { return s_settings->value("qr-session-button-text").toString(); } static QString userSessionButtonText() { return s_settings->value("user-session-button-text").toString(); } static QString guestSessionButtonText() { return s_settings->value("guest-session-button-text").toString(); } static QString guestSessionStartText() { return s_settings->value("guest-session-start-text").toString(); } diff --git a/src/snake.cpp b/src/snake.cpp index e855250..8d45dd9 100644 --- a/src/snake.cpp +++ b/src/snake.cpp @@ -22,11 +22,13 @@ #define CELL_BREAKOUT_BRICK 4 #define CELL_SNAKEBRICK 5 #define CELL_PADDLE 6 +#define CELL_GOL 7 #define AXIS_X 0 #define AXIS_Y 1 #define FIELD(x,y) _field[(x) + ((y) * _width)] +#define FIELD2(x,y) _field2[(x) + ((y) * _width)] struct Paddle; @@ -38,15 +40,17 @@ struct Cell Cell(int t, QColor c) : type(t), color(QBrush(c)), player(nullptr) {} bool isFood() const { return type == CELL_FOOD; } bool willKill() const { return type == CELL_PADDLE_BACKING | type == CELL_PADDLE || type == CELL_SNAKE || type == CELL_SNAKEBRICK; } - bool isFree() const { return type == CELL_FREE; } - bool isPaddleFree() const { return type == CELL_FREE || type == CELL_SNAKEBRICK || type == CELL_BREAKOUT_BRICK || type == CELL_FOOD; } + bool isFree() const { return type == CELL_FREE || type == CELL_GOL; } + bool isPaddleFree() const { return type == CELL_FREE || type == CELL_SNAKEBRICK || type == CELL_BREAKOUT_BRICK || type == CELL_FOOD || type == CELL_GOL; } bool isBrick() const { return type == CELL_BREAKOUT_BRICK || type == CELL_SNAKEBRICK; } - bool canSpawnFood() const { return type == CELL_SNAKE || type == CELL_FREE || type == CELL_BREAKOUT_BRICK; } - bool ballWillDestroy() const { return isFood() || isBrick(); } + bool canSpawnFood() const { return type == CELL_SNAKE || type == CELL_FREE || type == CELL_BREAKOUT_BRICK || type == CELL_GOL; } + bool ballWillDestroy() const { return isFood() || isBrick() || type == CELL_GOL; } + bool isAlive() const { return type == CELL_FOOD || type == CELL_BREAKOUT_BRICK || type == CELL_GOL; } }; static const Cell empty(CELL_FREE, QColor()); static const Cell snakebrick(CELL_SNAKEBRICK, QColor::fromRgb(200, 200, 200)); +static const Cell golBrick(CELL_GOL, QColor::fromRgb(180, 200, 220)); static Cell breakoutCenterBrick(CELL_BREAKOUT_BRICK, QColor::fromRgb(255, 255, 255)); static const Cell food[] = { Cell(CELL_FOOD, QColor::fromRgb(0, 255, 0)), @@ -133,6 +137,8 @@ public: }; void GameCore::setField(int x, int y, const Cell *val) { + if (_field[x + _width * y] == val) + return; _field[x + _width * y] = val; _widget->update(x * SCALING, y * SCALING, SCALING, SCALING); } @@ -151,6 +157,7 @@ GameCore::GameCore(QWidget *widget) return; int cellCount = _width * _height; _field = (const Cell**)calloc(cellCount, sizeof(*_field)); + _field2 = (const Cell**)calloc(cellCount, sizeof(*_field)); for (int i = 0; i < cellCount; ++i) { _field[i] = ∅ } @@ -462,7 +469,53 @@ GameCore::GameCore(QWidget *widget) } } // Done with movement logic } // End loop over paddles - } + } // End paddles + // Game of life + if (tick % 13 == 0) { + auto **tmp = _field; + _field = _field2; + _field2 = tmp; + memcpy(_field, _field2, _width * _height * sizeof(void*)); + bool killall = ((tick / 4000) % 2); + int killed = 0; + for (int y = 0; y < _height; ++y) { + for (int x = 0; x < _width; ++x) { + if (FIELD2(x,y) != &golBrick && FIELD2(x,y) != &empty) { + FIELD(x,y) = FIELD2(x,y); + continue; + } + if (killall) { + if (FIELD2(x,y) == &golBrick) { + setField(x,y, &empty); + if (++killed > 5) { + goto endgol; + } + } + continue; + } + int alive = 0; + // Direct + if (x > 0 && FIELD2(x-1,y)->isAlive()) alive++; + if (y > 0 && FIELD2(x,y-1)->isAlive()) alive++; + if (x+1 < _width && FIELD2(x+1,y)->isAlive()) alive++; + if (y+1 < _height && FIELD2(x,y+1)->isAlive()) alive++; + // Diagonal + if (x > 0 && y > 0 && FIELD2(x-1,y-1)->isAlive()) alive++; + if (x > 0 && y+1 < _height && FIELD2(x-1,y+1)->isAlive()) alive++; + if (y > 0 && x+1 < _width && FIELD2(x+1,y-1)->isAlive()) alive++; + if (x+1 < _width && y+1 < _height && FIELD2(x+1,y+1)->isAlive()) alive++; + // Check + if (alive < 2 || alive > 3) { + setField(x,y, &empty); + } else if (alive == 3) { + setField(x,y, &golBrick); + } else { + FIELD(x,y) = FIELD2(x,y); + } + } + } +endgol:; + } // End GOL }); } @@ -623,7 +676,7 @@ void GameCore::paint(QPaintEvent *event) if (x < 0) continue; const Cell *c = FIELD(x, y); - if (c->isFree()) + if (c == &empty) continue; p.setBrush(c->color); p.drawRect(x * SCALING, y * SCALING, SCALING-1, SCALING-1); diff --git a/src/snake.h b/src/snake.h index 538a944..b3b8433 100644 --- a/src/snake.h +++ b/src/snake.h @@ -27,6 +27,7 @@ private: qint64 _lastMeal; qint64 _lastPaddle; const Cell **_field; + const Cell **_field2; public: const Cell *field(int x, int y) const { diff --git a/src/webview.cpp b/src/webview.cpp index b178d73..54d19eb 100644 --- a/src/webview.cpp +++ b/src/webview.cpp @@ -171,7 +171,7 @@ void WebView::reset(const QString baseUrl) input.append((const char*)this, sizeof(*this)); input.append(QString().sprintf("%d %d", QCursor::pos().x(), QCursor::pos().y())); input.append(QString::number(QDateTime::currentMSecsSinceEpoch())); - _token = QCryptographicHash::hash(input, QCryptographicHash::Md5).chopped(8).toHex(); + _token = QCryptographicHash::hash(input, QCryptographicHash::Md5).left(8).toHex(); q.addQueryItem("token", _token); url.setQuery(q); _urls.clear(); -- cgit v1.2.3-55-g7522