219 lines
8.3 KiB
C++
219 lines
8.3 KiB
C++
#include <QDir>
|
|
#include <QFile>
|
|
#include <QJsonArray>
|
|
#include <QJsonDocument>
|
|
#include <QLabel>
|
|
#include <QProcess>
|
|
#include <QSslCertificate>
|
|
#include <QSslKey>
|
|
#include <QSslSocket>
|
|
#include <QStackedWidget>
|
|
#include <QTcpServer>
|
|
#include <QTemporaryDir>
|
|
#include <QtTest>
|
|
|
|
#include "desktop_list_page.h"
|
|
#include "main_window.h"
|
|
|
|
namespace vdi {
|
|
|
|
// Real HTTPS requests exercise the device adapter without production identities.
|
|
class TestPlatform : public QTcpServer {
|
|
public:
|
|
QSslCertificate certificate;
|
|
QSslKey key;
|
|
bool deny = false;
|
|
int delayMs = 0;
|
|
int authRequests = 0;
|
|
int listRequests = 0;
|
|
|
|
protected:
|
|
void incomingConnection(qintptr descriptor) override {
|
|
auto* socket = new QSslSocket(this);
|
|
socket->setLocalCertificate(certificate);
|
|
socket->setPrivateKey(key);
|
|
socket->setPeerVerifyMode(QSslSocket::VerifyNone);
|
|
if (!socket->setSocketDescriptor(descriptor)) {
|
|
socket->deleteLater();
|
|
return;
|
|
}
|
|
auto buffer = std::make_shared<QByteArray>();
|
|
connect(socket, &QSslSocket::readyRead, this, [this, socket, buffer] {
|
|
*buffer += socket->readAll();
|
|
if (!buffer->contains("\r\n\r\n") || socket->property("answered").toBool()) return;
|
|
socket->setProperty("answered", true);
|
|
const QByteArray path = buffer->split(' ').value(1);
|
|
QJsonObject body;
|
|
int status = 200;
|
|
if (path == "/api/v1/terminal/config") {
|
|
++authRequests;
|
|
if (deny) {
|
|
status = 403;
|
|
body["message"] = QStringLiteral("设备授权不可用");
|
|
}
|
|
} else if (path.startsWith("/api/v1/terminal/desktops")) {
|
|
++listRequests;
|
|
body["items"] = QJsonArray{QJsonObject{
|
|
{"id", "authorized-desktop"}, {"name", "Authorized Windows"},
|
|
{"os", "Windows"}, {"powerState", "running"},
|
|
{"endpoint", "127.0.0.1"}, {"app", "Desktop"}}};
|
|
} else {
|
|
body["items"] = QJsonArray{};
|
|
}
|
|
const QByteArray data = QJsonDocument(body).toJson(QJsonDocument::Compact);
|
|
const QByteArray response = "HTTP/1.1 " + QByteArray::number(status) +
|
|
" Result\r\nContent-Type: application/json\r\nContent-Length: " +
|
|
QByteArray::number(data.size()) + "\r\nConnection: close\r\n\r\n" + data;
|
|
QTimer::singleShot(delayMs, socket, [socket, response] {
|
|
socket->write(response);
|
|
socket->disconnectFromHost();
|
|
});
|
|
});
|
|
connect(socket, &QSslSocket::disconnected, socket, &QObject::deleteLater);
|
|
socket->startServerEncryption();
|
|
}
|
|
};
|
|
|
|
class MainWindowTest : public QObject {
|
|
Q_OBJECT
|
|
private:
|
|
QTemporaryDir certificates_;
|
|
std::unique_ptr<QTemporaryDir> homeDirectory_;
|
|
QByteArray originalHome_;
|
|
QString certificatePath_;
|
|
QString keyPath_;
|
|
|
|
void configure(TestPlatform& server, quint16 port, bool invalidCA = false) {
|
|
const QString directory = homeDirectory_->path() + "/.config/vdi-terminal";
|
|
QVERIFY(QDir().mkpath(directory));
|
|
QFile certificateFile(certificatePath_);
|
|
QVERIFY(certificateFile.open(QIODevice::ReadOnly));
|
|
server.certificate = QSslCertificate(certificateFile.readAll());
|
|
QFile keyFile(keyPath_);
|
|
QVERIFY(keyFile.open(QIODevice::ReadOnly));
|
|
server.key = QSslKey(keyFile.readAll(), QSsl::Rsa);
|
|
QFile config(directory + "/platform.json");
|
|
QVERIFY(config.open(QIODevice::WriteOnly));
|
|
config.write(QJsonDocument(QJsonObject{
|
|
{"url", QString("https://127.0.0.1:%1").arg(port)},
|
|
{"deviceToken", "isolated-test-identity"},
|
|
{"caFile", invalidCA ? directory + "/missing.pem" : certificatePath_}
|
|
}).toJson());
|
|
config.close();
|
|
// A previously saved local desktop must not bypass device authentication.
|
|
QFile local(directory + "/desktops.json");
|
|
QVERIFY(local.open(QIODevice::WriteOnly));
|
|
local.write("[{\"id\":\"local-only\",\"name\":\"Unauthorized Local\","
|
|
"\"os\":\"Windows\",\"endpoint\":\"127.0.0.2\"}]");
|
|
}
|
|
|
|
void prepare(MainWindow& window) {
|
|
// Tests neither update the real watchdog heartbeat nor touch X11 focus.
|
|
window.heartbeatTimer_->stop();
|
|
window.deviceAuthRetryMs_ = 30;
|
|
}
|
|
|
|
bool selectionVisible(MainWindow& window) {
|
|
return window.fsm_.isIn(State::kDesktopList) &&
|
|
window.stack_->currentWidget() == window.desktopListPage_;
|
|
}
|
|
|
|
private slots:
|
|
void initTestCase() {
|
|
QVERIFY(certificates_.isValid());
|
|
certificatePath_ = certificates_.path() + "/server.pem";
|
|
keyPath_ = certificates_.path() + "/server.key";
|
|
QProcess openssl;
|
|
openssl.start("openssl", {"req", "-x509", "-newkey", "rsa:2048", "-nodes",
|
|
"-keyout", keyPath_, "-out", certificatePath_, "-days", "1",
|
|
"-subj", "/CN=127.0.0.1", "-addext", "subjectAltName=IP:127.0.0.1"});
|
|
QVERIFY(openssl.waitForFinished(10000));
|
|
QCOMPARE(openssl.exitCode(), 0);
|
|
}
|
|
void init() {
|
|
originalHome_ = qgetenv("HOME");
|
|
homeDirectory_ = std::make_unique<QTemporaryDir>();
|
|
QVERIFY(homeDirectory_->isValid());
|
|
qputenv("HOME", homeDirectory_->path().toUtf8());
|
|
}
|
|
void cleanup() {
|
|
if (originalHome_.isNull()) qunsetenv("HOME");
|
|
else qputenv("HOME", originalHome_);
|
|
homeDirectory_.reset();
|
|
}
|
|
void offlineBootRecoversWithoutRestart() {
|
|
TestPlatform server;
|
|
QVERIFY(server.listen(QHostAddress::LocalHost, 0));
|
|
const quint16 port = server.serverPort();
|
|
server.close();
|
|
configure(server, port);
|
|
MainWindow window;
|
|
prepare(window);
|
|
QTRY_VERIFY(window.deviceAuthTimer_->isActive());
|
|
QVERIFY(selectionVisible(window));
|
|
QVERIFY(!window.deviceAuthenticated_);
|
|
QVERIFY(window.desktops_.empty());
|
|
QCOMPARE(server.listRequests, 0);
|
|
QVERIFY(server.listen(QHostAddress::LocalHost, port));
|
|
QTRY_VERIFY(window.deviceAuthenticated_);
|
|
QTRY_COMPARE(window.desktops_.size(), size_t(1));
|
|
QCOMPARE(window.desktops_[0].id, QString("authorized-desktop"));
|
|
QVERIFY(selectionVisible(window));
|
|
QVERIFY(!window.deviceAuthTimer_->isActive());
|
|
}
|
|
void rejectedIdentityStaysOnSelectionAndRetries() {
|
|
TestPlatform server;
|
|
QVERIFY(server.listen(QHostAddress::LocalHost, 0));
|
|
configure(server, server.serverPort());
|
|
server.deny = true;
|
|
MainWindow window;
|
|
prepare(window);
|
|
QTRY_VERIFY(server.authRequests >= 3);
|
|
QVERIFY(selectionVisible(window));
|
|
QVERIFY(!window.deviceAuthenticated_);
|
|
QVERIFY(window.desktops_.empty());
|
|
QCOMPARE(server.listRequests, 0);
|
|
server.deny = false;
|
|
QTRY_VERIFY(window.deviceAuthenticated_);
|
|
QTRY_COMPARE(window.desktops_.size(), size_t(1));
|
|
QVERIFY(selectionVisible(window));
|
|
}
|
|
void slowAuthenticationDoesNotOverlap() {
|
|
TestPlatform server;
|
|
QVERIFY(server.listen(QHostAddress::LocalHost, 0));
|
|
configure(server, server.serverPort());
|
|
server.delayMs = 200;
|
|
MainWindow window;
|
|
prepare(window);
|
|
QTRY_COMPARE(server.authRequests, 1);
|
|
window.authenticateDevice();
|
|
window.authenticateDevice();
|
|
QVERIFY(window.deviceAuthInFlight_);
|
|
QTRY_VERIFY(window.deviceAuthenticated_);
|
|
QCOMPARE(server.authRequests, 1);
|
|
QVERIFY(selectionVisible(window));
|
|
}
|
|
void invalidConfigurationNeverShowsLogin() {
|
|
TestPlatform server;
|
|
configure(server, 1, true);
|
|
MainWindow window;
|
|
prepare(window);
|
|
QTRY_VERIFY(window.deviceAuthTimer_->isActive());
|
|
QVERIFY(selectionVisible(window));
|
|
QVERIFY(!window.deviceAuthenticated_);
|
|
QCOMPARE(server.authRequests, 0);
|
|
}
|
|
void localModeStillLoadsDesktops() {
|
|
MainWindow window;
|
|
prepare(window);
|
|
QTRY_VERIFY(window.deviceAuthenticated_);
|
|
QVERIFY(selectionVisible(window));
|
|
QVERIFY(!window.deviceAuthTimer_->isActive());
|
|
}
|
|
};
|
|
|
|
} // namespace vdi
|
|
|
|
QTEST_MAIN(vdi::MainWindowTest)
|
|
#include "test_main_window.moc"
|