#include "qrlogin.h"
#include "settings.h"
#include "global.h"
#include <unistd.h>
#include <QDebug>
#include <QLabel>
#include <QSvgRenderer>
#include <QPainter>
#include <QTimer>
#include <QElapsedTimer>
#include <QNetworkAccessManager>
#include <QNetworkRequest>
#include <QNetworkReply>
#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<quint8>(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;
}