#include "qrlogin.h" #include "settings.h" #include "global.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, [=]() { if (svgData.size() > 1024000) { reply->abort(); return; } 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; } 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); timer->stop(); reply->abort(); emit triggerReset(QLatin1String("Timeout: Could not get QR Code")); }); 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_-="; 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; }