summaryrefslogtreecommitdiffstats
path: root/src/qrlogin.cpp
blob: c562e981892c8feddb6bda01a3ea9ffe03c02f7b (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
#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;
}