功能:完善终端认证恢复、选择页及策略和指标采集
这个提交包含在:
1 父节点
d87fa47e24
当前提交
eef485306f
22 个文件已更改
+904
-151
没有与搜索条件相匹配的文件
@@ -0,0 +1,35 @@
|
||||
#include <QApplication>
|
||||
#include <QDir>
|
||||
#include <QPushButton>
|
||||
#include <QSettings>
|
||||
#include <QTimer>
|
||||
#include "desktop_list_page.h"
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
QApplication app(argc, argv);
|
||||
app.setOrganizationName("vdi-preview");
|
||||
app.setApplicationName("desktop-selection");
|
||||
const QStringList args = app.arguments();
|
||||
if (args.size() < 5) return 2;
|
||||
QSettings().setValue("appearance/light", args[4] == "light");
|
||||
vdi::DesktopListPage page;
|
||||
page.resize(args[2].toInt(), args[3].toInt());
|
||||
page.setCurrentUser(QStringLiteral("云终端"));
|
||||
if (!args.contains("empty")) {
|
||||
std::vector<vdi::DesktopInfo> desktops;
|
||||
for (const QString& os : {QStringLiteral("Windows"), QStringLiteral("Ubuntu"), QStringLiteral("麒麟")}) {
|
||||
vdi::DesktopInfo d;
|
||||
d.id = os; d.name = os + QStringLiteral(" 云桌面"); d.os = os;
|
||||
d.state = vdi::DesktopState::kAvailable;
|
||||
if (args.contains("states") && os == "Ubuntu") d.state = vdi::DesktopState::kPending;
|
||||
if (args.contains("states") && os == QStringLiteral("麒麟")) d.state = vdi::DesktopState::kUnassigned;
|
||||
desktops.push_back(d);
|
||||
}
|
||||
page.setDesktops(desktops);
|
||||
}
|
||||
if (args.contains("error")) page.setStatus(QStringLiteral("连接失败:管理平台暂不可用;正在自动重试,无需重启盒子"), true);
|
||||
if (args.contains("busy")) { page.setBusy(true); page.setStatus(QStringLiteral("正在连接,请稍候…")); }
|
||||
page.show();
|
||||
QTimer::singleShot(150, [&] { app.exit(page.grab().save(args[1]) ? 0 : 1); });
|
||||
return app.exec();
|
||||
}
|
||||
@@ -0,0 +1,218 @@
|
||||
#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"
|
||||
@@ -0,0 +1,47 @@
|
||||
#include "system_metrics.h"
|
||||
#include <QtTest>
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
#include <QTemporaryDir>
|
||||
class SystemMetricsTest : public QObject {
|
||||
Q_OBJECT
|
||||
private slots:
|
||||
void countersAndMissing() {
|
||||
QTemporaryDir root;
|
||||
auto put=[&](const QString& path,const QByteArray& value) { QFile f(root.path()+path); QVERIFY(f.open(QIODevice::WriteOnly)); f.write(value); };
|
||||
QDir().mkpath(root.path()+"/thermal/thermal_zone0");
|
||||
put("/stat","cpu 100 0 50 800 50 0 0 0 100 0\n");
|
||||
put("/meminfo","MemTotal: 1000 kB\nMemAvailable: 700 kB\n");
|
||||
put("/uptime","120.5 80\n");
|
||||
put("/thermal/thermal_zone0/type","soc-thermal\n");
|
||||
put("/thermal/thermal_zone0/temp","44370\n");
|
||||
vdi::SystemMetrics collector(root.path(),root.path()+"/thermal");
|
||||
auto sample=collector.sample();
|
||||
QVERIFY(sample["cpuPercent"].isNull());
|
||||
QCOMPARE(sample["memoryPercent"].toDouble(),30.0);
|
||||
QCOMPARE(sample["socTemperatureC"].toDouble(),44.37);
|
||||
QCOMPARE(sample["uptimeSeconds"].toDouble(),120.5);
|
||||
put("/stat","cpu 120 0 60 860 60 0 0 0 500 0\n");
|
||||
QCOMPARE(collector.sample()["cpuPercent"].toDouble(),30.0); // guest time is not counted twice
|
||||
QVERIFY(collector.sample()["cpuPercent"].isNull()); // no counter delta
|
||||
put("/stat","cpu 1 0 0 1 0 0 0 0\n");
|
||||
QVERIFY(collector.sample()["cpuPercent"].isNull()); // reboot/reset
|
||||
put("/stat","broken\n"); collector.sample();
|
||||
put("/stat","cpu 2 0 0 5 0 0 0 0\n");
|
||||
QVERIFY(collector.sample()["cpuPercent"].isNull());
|
||||
put("/meminfo","MemTotal: 100 kB\nMemAvailable: 101 kB\n");
|
||||
put("/uptime","nan\n");
|
||||
put("/thermal/thermal_zone0/type","gpu-thermal\n");
|
||||
sample=collector.sample();
|
||||
QVERIFY(sample["memoryPercent"].isNull());
|
||||
QVERIFY(sample["uptimeSeconds"].isNull());
|
||||
QVERIFY(sample["socTemperatureC"].isNull());
|
||||
}
|
||||
void unavailable() {
|
||||
vdi::SystemMetrics collector("/nonexistent/proc","/nonexistent/thermal");
|
||||
const auto sample=collector.sample();
|
||||
for (auto value : sample) QVERIFY(value.isNull());
|
||||
}
|
||||
};
|
||||
QTEST_GUILESS_MAIN(SystemMetricsTest)
|
||||
#include "test_system_metrics.moc"
|
||||
@@ -0,0 +1,33 @@
|
||||
#include <QtTest>
|
||||
#include <QTemporaryDir>
|
||||
#include <QFile>
|
||||
#include <QJsonDocument>
|
||||
#include "terminal_policy.h"
|
||||
class TerminalPolicyTest : public QObject {
|
||||
Q_OBJECT
|
||||
private slots:
|
||||
void durableBeforeRuntime() {
|
||||
QTemporaryDir dir; const QString path=dir.path()+"/policy.json";int calls=0;
|
||||
QJsonObject p{{"policyProtocol",1},{"configVersion",2},{"deploymentId","dispatch_1"},
|
||||
{"heartbeatInterval",30},{"commandPollInterval",5},{"desktopRefreshInterval",20},{"deviceToken","must-not-persist"}};
|
||||
auto apply=[&](int h,int c,int r){
|
||||
QFile file(path); QVERIFY(file.open(QIODevice::ReadOnly));
|
||||
const auto disk=QJsonDocument::fromJson(file.readAll()).object();
|
||||
QCOMPARE(disk.value("configVersion").toInt(),2);QVERIFY(!disk.contains("deviceToken"));
|
||||
QCOMPARE(h,30);QCOMPARE(c,5);QCOMPARE(r,20);++calls;
|
||||
};
|
||||
QCOMPARE(vdi::applyTerminalPolicy(p,path,apply),QString());QCOMPARE(calls,1);
|
||||
const auto original=QFileInfo(path).size();
|
||||
for (const auto key:{"heartbeatInterval","commandPollInterval","desktopRefreshInterval","policyProtocol","configVersion"}) {
|
||||
auto bad=p;bad[key]=1.5;
|
||||
QCOMPARE(vdi::applyTerminalPolicy(bad,path,apply),QString("invalid_policy"));
|
||||
}
|
||||
auto bad=p;bad["heartbeatInterval"]=90;
|
||||
QCOMPARE(vdi::applyTerminalPolicy(bad,path,apply),QString("invalid_policy"));
|
||||
QCOMPARE(QFileInfo(path).size(),original);QCOMPARE(calls,1);
|
||||
QCOMPARE(vdi::applyTerminalPolicy(p,dir.path()+"/missing/policy.json",apply),QString("persist_failed"));
|
||||
QCOMPARE(vdi::applyTerminalPolicy(p,path,{}),QString("runtime_unavailable"));QCOMPARE(calls,1);
|
||||
}
|
||||
};
|
||||
QTEST_GUILESS_MAIN(TerminalPolicyTest)
|
||||
#include "test_terminal_policy.moc"
|
||||
在新工单中引用
屏蔽一个用户