summaryrefslogblamecommitdiffstats
path: root/src/qrlogin.cpp
blob: c562e981892c8feddb6bda01a3ea9ffe03c02f7b (plain) (tree)
1
2
3

                     
                   



















































                                                                                                                 



                                       










                                                                                            




                                                                           



                                    

                                                                           









                                                                           


































































                                                                                                                  
                                                       
                                          
                                                      




























                                                                                          























                                                                             
#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;
}