mirror of https://github.com/cutefishos/launcher
You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
347 lines
15 KiB
C++
347 lines
15 KiB
C++
|
1 month ago
|
#include <QCoreApplication>
|
||
|
|
#include <QFile>
|
||
|
|
#include <QJsonArray>
|
||
|
|
#include <QLocalSocket>
|
||
|
|
#include <QSignalSpy>
|
||
|
|
#include <QTemporaryDir>
|
||
|
|
#include <QTest>
|
||
|
|
|
||
|
|
#include <csignal>
|
||
|
|
#include <unistd.h>
|
||
|
|
|
||
|
|
#include "common/wire.h"
|
||
|
|
#include "daemon/config.h"
|
||
|
|
#include "daemon/greeterserver.h"
|
||
|
|
#include "daemon/helperprocess.h"
|
||
|
|
#include "daemon/sessions.h"
|
||
|
|
#include "daemon/state.h"
|
||
|
|
|
||
|
|
namespace {
|
||
|
|
|
||
|
|
// When set, the test binary is the helper: it accepts "right" and its session
|
||
|
|
// runs until terminated.
|
||
|
|
constexpr char kFakeHelper[] = "DM_TEST_FAKE_HELPER";
|
||
|
|
|
||
|
|
QByteArray readLine()
|
||
|
|
{
|
||
|
|
QByteArray line;
|
||
|
|
char c = 0;
|
||
|
|
while (read(STDIN_FILENO, &c, 1) == 1 && c != '\n')
|
||
|
|
line.append(c);
|
||
|
|
return line;
|
||
|
|
}
|
||
|
|
|
||
|
|
void writeLine(const QJsonObject& message)
|
||
|
|
{
|
||
|
|
const QByteArray bytes = dm::encodeLine(message);
|
||
|
|
if (write(STDOUT_FILENO, bytes.constData(), size_t(bytes.size())) != bytes.size())
|
||
|
|
_exit(3);
|
||
|
|
}
|
||
|
|
|
||
|
|
int fakeHelper()
|
||
|
|
{
|
||
|
|
const auto login = dm::decodeLine(readLine());
|
||
|
|
if (!login || login->value(QStringLiteral("user")).toString() != QLatin1String("alice"))
|
||
|
|
return 2;
|
||
|
|
writeLine({{QStringLiteral("type"), QStringLiteral("question")},
|
||
|
|
{QStringLiteral("style"), QStringLiteral("secret")},
|
||
|
|
{QStringLiteral("text"), QStringLiteral("Password:")}});
|
||
|
|
const auto answer = dm::decodeLine(readLine());
|
||
|
|
if (!answer || answer->value(QStringLiteral("response")).toString() != QLatin1String("right")) {
|
||
|
|
writeLine({{QStringLiteral("type"), QStringLiteral("error")},
|
||
|
|
{QStringLiteral("auth"), true},
|
||
|
|
{QStringLiteral("message"), QStringLiteral("Authentication failure")}});
|
||
|
|
return 1;
|
||
|
|
}
|
||
|
|
writeLine({{QStringLiteral("type"), QStringLiteral("authenticated")}});
|
||
|
|
const auto start = dm::decodeLine(readLine());
|
||
|
|
if (!start || start->value(QStringLiteral("cmd")).toArray().isEmpty())
|
||
|
|
return 0;
|
||
|
|
writeLine({{QStringLiteral("type"), QStringLiteral("started")},
|
||
|
|
{QStringLiteral("pid"), qint64(getpid())},
|
||
|
|
{QStringLiteral("session"), QStringLiteral("42")}});
|
||
|
|
for (;;)
|
||
|
|
pause();
|
||
|
|
}
|
||
|
|
|
||
|
|
QByteArray frame(const QJsonObject& message)
|
||
|
|
{
|
||
|
|
return dm::encodeFrame(message);
|
||
|
|
}
|
||
|
|
|
||
|
|
void writeFile(const QString& path, const QByteArray& contents)
|
||
|
|
{
|
||
|
|
QFile file(path);
|
||
|
|
QVERIFY(file.open(QIODevice::WriteOnly));
|
||
|
|
file.write(contents);
|
||
|
|
}
|
||
|
|
|
||
|
|
} // namespace
|
||
|
|
|
||
|
|
class DisplayManagerTest : public QObject
|
||
|
|
{
|
||
|
|
Q_OBJECT
|
||
|
|
private slots:
|
||
|
|
|
||
|
|
void framesSurviveSplitsAndBatches()
|
||
|
|
{
|
||
|
|
const QByteArray first = frame({{QStringLiteral("type"), QStringLiteral("success")}});
|
||
|
|
const QByteArray second = frame({{QStringLiteral("type"), QStringLiteral("cancel_session")}});
|
||
|
|
dm::FrameReader reader;
|
||
|
|
reader.append(first.left(3));
|
||
|
|
QVERIFY(!reader.next());
|
||
|
|
reader.append(first.mid(3) + second);
|
||
|
|
const auto a = reader.next();
|
||
|
|
const auto b = reader.next();
|
||
|
|
QVERIFY(a && b);
|
||
|
|
QCOMPARE(a->value(QStringLiteral("type")).toString(), QStringLiteral("success"));
|
||
|
|
QCOMPARE(b->value(QStringLiteral("type")).toString(), QStringLiteral("cancel_session"));
|
||
|
|
QVERIFY(!reader.next());
|
||
|
|
QVERIFY(!reader.failed());
|
||
|
|
}
|
||
|
|
|
||
|
|
void oversizedOrMalformedFramesFailTheReader()
|
||
|
|
{
|
||
|
|
dm::FrameReader oversized;
|
||
|
|
const quint32 length = dm::kMaxFrameSize + 1;
|
||
|
|
oversized.append(QByteArray(reinterpret_cast<const char*>(&length), sizeof(length)));
|
||
|
|
QVERIFY(!oversized.next());
|
||
|
|
QVERIFY(oversized.failed());
|
||
|
|
|
||
|
|
dm::FrameReader malformed;
|
||
|
|
const QByteArray payload("[1,2]");
|
||
|
|
const quint32 size = quint32(payload.size());
|
||
|
|
malformed.append(QByteArray(reinterpret_cast<const char*>(&size), sizeof(size)) + payload);
|
||
|
|
QVERIFY(!malformed.next());
|
||
|
|
QVERIFY(malformed.failed());
|
||
|
|
}
|
||
|
|
|
||
|
|
void linesRoundTrip()
|
||
|
|
{
|
||
|
|
const QJsonObject message {{QStringLiteral("type"), QStringLiteral("answer")},
|
||
|
|
{QStringLiteral("response"), QJsonValue()}};
|
||
|
|
const QByteArray line = dm::encodeLine(message);
|
||
|
|
QVERIFY(line.endsWith('\n'));
|
||
|
|
QCOMPARE(line.count('\n'), 1);
|
||
|
|
QCOMPARE(*dm::decodeLine(line), message);
|
||
|
|
QVERIFY(!dm::decodeLine("not json"));
|
||
|
|
}
|
||
|
|
|
||
|
|
void sessionFilesParse()
|
||
|
|
{
|
||
|
|
QTemporaryDir directory;
|
||
|
|
writeFile(directory.filePath(QStringLiteral("plasma.desktop")),
|
||
|
|
"[Desktop Entry]\n"
|
||
|
|
"# a comment\n"
|
||
|
|
"Name=Plasma (Wayland)\n"
|
||
|
|
"Comment=Plasma by KDE\n"
|
||
|
|
"Exec=/usr/bin/startplasma-wayland --arg \"two words\" %f\n"
|
||
|
|
"DesktopNames=KDE;Plasma;\n"
|
||
|
|
"[Other Group]\n"
|
||
|
|
"Exec=ignored\n");
|
||
|
|
const auto entry = dm::readSessionFile(directory.filePath(QStringLiteral("plasma.desktop")));
|
||
|
|
QVERIFY(entry);
|
||
|
|
QCOMPARE(entry->id, QStringLiteral("plasma"));
|
||
|
|
QCOMPARE(entry->name, QStringLiteral("Plasma (Wayland)"));
|
||
|
|
QCOMPARE(entry->exec, (QStringList {QStringLiteral("/usr/bin/startplasma-wayland"),
|
||
|
|
QStringLiteral("--arg"), QStringLiteral("two words")}));
|
||
|
|
QCOMPARE(entry->desktopNames, (QStringList {QStringLiteral("KDE"), QStringLiteral("Plasma")}));
|
||
|
|
}
|
||
|
|
|
||
|
|
void hiddenMissingOrCommandlessSessionsAreNotOffered()
|
||
|
|
{
|
||
|
|
QTemporaryDir directory;
|
||
|
|
writeFile(directory.filePath(QStringLiteral("hidden.desktop")),
|
||
|
|
"[Desktop Entry]\nName=Hidden\nExec=/bin/true\nHidden=true\n");
|
||
|
|
writeFile(directory.filePath(QStringLiteral("nodisplay.desktop")),
|
||
|
|
"[Desktop Entry]\nName=NoDisplay\nExec=/bin/true\nNoDisplay=true\n");
|
||
|
|
writeFile(directory.filePath(QStringLiteral("missing.desktop")),
|
||
|
|
"[Desktop Entry]\nName=Missing\nExec=/bin/true\nTryExec=/nonexistent/program\n");
|
||
|
|
writeFile(directory.filePath(QStringLiteral("noexec.desktop")),
|
||
|
|
"[Desktop Entry]\nName=NoExec\n");
|
||
|
|
writeFile(directory.filePath(QStringLiteral("good.desktop")),
|
||
|
|
"[Desktop Entry]\nName=Good\nExec=/bin/true\nTryExec=true\n");
|
||
|
|
const auto sessions = dm::findSessions({directory.path()});
|
||
|
|
QCOMPARE(sessions.size(), 1);
|
||
|
|
QCOMPARE(sessions.first().id, QStringLiteral("good"));
|
||
|
|
}
|
||
|
|
|
||
|
|
void earlierDirectoriesWinAndIdsNameFilesOnly()
|
||
|
|
{
|
||
|
|
QTemporaryDir local, system;
|
||
|
|
writeFile(local.filePath(QStringLiteral("cutefish.desktop")),
|
||
|
|
"[Desktop Entry]\nName=Local\nExec=/bin/true\n");
|
||
|
|
writeFile(system.filePath(QStringLiteral("cutefish.desktop")),
|
||
|
|
"[Desktop Entry]\nName=System\nExec=/bin/false\n");
|
||
|
|
writeFile(local.filePath(QStringLiteral("gnome.desktop")),
|
||
|
|
"[Desktop Entry]\nName=GNOME\nExec=/bin/true\nHidden=true\n");
|
||
|
|
writeFile(system.filePath(QStringLiteral("gnome.desktop")),
|
||
|
|
"[Desktop Entry]\nName=GNOME\nExec=/bin/true\n");
|
||
|
|
const QStringList directories {local.path(), system.path()};
|
||
|
|
QCOMPARE(dm::findSession(directories, QStringLiteral("cutefish"))->name,
|
||
|
|
QStringLiteral("Local"));
|
||
|
|
// A hidden local entry masks the system one.
|
||
|
|
QVERIFY(!dm::findSession(directories, QStringLiteral("gnome")));
|
||
|
|
QVERIFY(!dm::findSession(directories, QStringLiteral("../cutefish")));
|
||
|
|
QVERIFY(!dm::findSession(directories, QStringLiteral(".hidden")));
|
||
|
|
QVERIFY(!dm::findSession(directories, QString()));
|
||
|
|
}
|
||
|
|
|
||
|
|
void environmentsCarryWhatPamSystemdReads()
|
||
|
|
{
|
||
|
|
dm::SessionEntry gnome;
|
||
|
|
gnome.id = QStringLiteral("gnome");
|
||
|
|
gnome.desktopNames = {QStringLiteral("GNOME")};
|
||
|
|
const QStringList session = dm::sessionEnvironment(gnome, 2);
|
||
|
|
QVERIFY(session.contains(QStringLiteral("XDG_SESSION_TYPE=wayland")));
|
||
|
|
QVERIFY(session.contains(QStringLiteral("XDG_SESSION_CLASS=user")));
|
||
|
|
QVERIFY(session.contains(QStringLiteral("XDG_SEAT=seat0")));
|
||
|
|
QVERIFY(session.contains(QStringLiteral("XDG_VTNR=2")));
|
||
|
|
QVERIFY(session.contains(QStringLiteral("DESKTOP_SESSION=gnome")));
|
||
|
|
QVERIFY(session.contains(QStringLiteral("XDG_SESSION_DESKTOP=GNOME")));
|
||
|
|
QVERIFY(session.contains(QStringLiteral("XDG_CURRENT_DESKTOP=GNOME")));
|
||
|
|
|
||
|
|
const QStringList bare = dm::sessionEnvironment(dm::SessionEntry {}, 2);
|
||
|
|
QVERIFY(bare.contains(QStringLiteral("XDG_SESSION_TYPE=wayland")));
|
||
|
|
for (const QString& variable : bare)
|
||
|
|
QVERIFY(!variable.startsWith(QLatin1String("DESKTOP_SESSION=")));
|
||
|
|
|
||
|
|
const QStringList greeter = dm::greeterEnvironment(1, QStringLiteral("/run/x.sock"));
|
||
|
|
QVERIFY(greeter.contains(QStringLiteral("XDG_SESSION_CLASS=greeter")));
|
||
|
|
QVERIFY(greeter.contains(QStringLiteral("XDG_VTNR=1")));
|
||
|
|
QVERIFY(greeter.contains(QStringLiteral("GREETD_SOCK=/run/x.sock")));
|
||
|
|
}
|
||
|
|
|
||
|
|
void stateRemembersTheLastLogin()
|
||
|
|
{
|
||
|
|
QTemporaryDir directory;
|
||
|
|
dm::State state(directory.filePath(QStringLiteral("state/state.conf")));
|
||
|
|
QVERIFY(state.lastUser().isEmpty());
|
||
|
|
state.remember(QStringLiteral("alice"), QStringLiteral("gnome"));
|
||
|
|
state.remember(QStringLiteral("bob"), QStringLiteral("cutefish-wayland"));
|
||
|
|
dm::State reread(directory.filePath(QStringLiteral("state/state.conf")));
|
||
|
|
QCOMPARE(reread.lastUser(), QStringLiteral("bob"));
|
||
|
|
QCOMPARE(reread.lastSession(QStringLiteral("alice")), QStringLiteral("gnome"));
|
||
|
|
QCOMPARE(reread.lastSession(QStringLiteral("bob")), QStringLiteral("cutefish-wayland"));
|
||
|
|
}
|
||
|
|
|
||
|
|
void configFallsBackToDefaults()
|
||
|
|
{
|
||
|
|
const dm::Config defaults = dm::Config::load(QStringLiteral("/nonexistent/dm.conf"));
|
||
|
|
QCOMPARE(defaults.greeterVt, 1);
|
||
|
|
QCOMPARE(defaults.sessionVt, 2);
|
||
|
|
QCOMPARE(defaults.greeterUser, QStringLiteral("cutefish-greeter"));
|
||
|
|
|
||
|
|
QTemporaryDir directory;
|
||
|
|
writeFile(directory.filePath(QStringLiteral("dm.conf")),
|
||
|
|
"[Greeter]\nVt=8\nCommand=/usr/bin/cutefish-compositor --login --no-xwayland\n"
|
||
|
|
"[Session]\nVt=9\n[Daemon]\nRuntimeDirectory=/run/test\n");
|
||
|
|
const dm::Config config = dm::Config::load(directory.filePath(QStringLiteral("dm.conf")));
|
||
|
|
QCOMPARE(config.greeterVt, 8);
|
||
|
|
QCOMPARE(config.sessionVt, 9);
|
||
|
|
QCOMPARE(config.greeterCommand.last(), QStringLiteral("--no-xwayland"));
|
||
|
|
QCOMPARE(config.runtimeDirectory, QStringLiteral("/run/test"));
|
||
|
|
QCOMPARE(config.greeterUser, QStringLiteral("cutefish-greeter"));
|
||
|
|
}
|
||
|
|
|
||
|
|
void greeterServerSpeaksGreetd()
|
||
|
|
{
|
||
|
|
QTemporaryDir directory;
|
||
|
|
const QString path = directory.filePath(QStringLiteral("greeter.sock"));
|
||
|
|
dm::GreeterServer server;
|
||
|
|
QVERIFY(server.listen(path, getuid()));
|
||
|
|
QCOMPARE(QFile::permissions(path) & (QFile::ReadGroup | QFile::ReadOther), QFileDevice::Permissions());
|
||
|
|
|
||
|
|
QSignalSpy created(&server, &dm::GreeterServer::createSession);
|
||
|
|
QSignalSpy started(&server, &dm::GreeterServer::startSession);
|
||
|
|
QLocalSocket client;
|
||
|
|
client.connectToServer(path);
|
||
|
|
QVERIFY(client.waitForConnected(1000));
|
||
|
|
|
||
|
|
// Two requests at once: the second waits for the first's answer.
|
||
|
|
client.write(frame({{QStringLiteral("type"), QStringLiteral("create_session")},
|
||
|
|
{QStringLiteral("username"), QStringLiteral("alice")}}));
|
||
|
|
client.write(frame({{QStringLiteral("type"), QStringLiteral("start_session")},
|
||
|
|
{QStringLiteral("cmd"), QJsonArray {}},
|
||
|
|
{QStringLiteral("env"), QJsonArray {QStringLiteral("LANG=C")}},
|
||
|
|
{QStringLiteral("session"), QStringLiteral("gnome")}}));
|
||
|
|
client.flush();
|
||
|
|
QTRY_COMPARE(created.count(), 1);
|
||
|
|
QCOMPARE(created.first().first().toString(), QStringLiteral("alice"));
|
||
|
|
QTest::qWait(50);
|
||
|
|
QCOMPARE(started.count(), 0);
|
||
|
|
|
||
|
|
server.respond(dm::GreeterServer::authMessage(QStringLiteral("secret"), QStringLiteral("Password:")));
|
||
|
|
QTRY_COMPARE(started.count(), 1);
|
||
|
|
QCOMPARE(started.first().at(1).toStringList(), QStringList {QStringLiteral("LANG=C")});
|
||
|
|
QCOMPARE(started.first().at(2).toString(), QStringLiteral("gnome"));
|
||
|
|
server.respond(dm::GreeterServer::success());
|
||
|
|
|
||
|
|
dm::FrameReader reader;
|
||
|
|
QList<QJsonObject> responses;
|
||
|
|
QTRY_VERIFY([&] {
|
||
|
|
reader.append(client.readAll());
|
||
|
|
while (auto response = reader.next())
|
||
|
|
responses.append(*response);
|
||
|
|
return responses.size() == 2;
|
||
|
|
}());
|
||
|
|
QCOMPARE(responses[0].value(QStringLiteral("type")).toString(), QStringLiteral("auth_message"));
|
||
|
|
QCOMPARE(responses[0].value(QStringLiteral("auth_message_type")).toString(),
|
||
|
|
QStringLiteral("secret"));
|
||
|
|
QCOMPARE(responses[1].value(QStringLiteral("type")).toString(), QStringLiteral("success"));
|
||
|
|
}
|
||
|
|
|
||
|
|
void helperProtocolRunsALogin()
|
||
|
|
{
|
||
|
|
qputenv("CUTEFISH_DM_HELPER", QCoreApplication::applicationFilePath().toLocal8Bit());
|
||
|
|
qputenv(kFakeHelper, "1");
|
||
|
|
dm::HelperProcess helper;
|
||
|
|
QSignalSpy questions(&helper, &dm::HelperProcess::question);
|
||
|
|
QSignalSpy authenticated(&helper, &dm::HelperProcess::authenticated);
|
||
|
|
QSignalSpy started(&helper, &dm::HelperProcess::started);
|
||
|
|
QSignalSpy finished(&helper, &dm::HelperProcess::finished);
|
||
|
|
helper.login(QStringLiteral("cutefish-dm"), QStringLiteral("alice"), true, 2);
|
||
|
|
QTRY_COMPARE(questions.count(), 1);
|
||
|
|
QCOMPARE(questions.first().first().toString(), QStringLiteral("secret"));
|
||
|
|
helper.answer(QStringLiteral("right"));
|
||
|
|
QTRY_COMPARE(authenticated.count(), 1);
|
||
|
|
QVERIFY(helper.isAuthenticated());
|
||
|
|
helper.start({QStringLiteral("/bin/true")}, {QStringLiteral("XDG_VTNR=2")});
|
||
|
|
QTRY_COMPARE(started.count(), 1);
|
||
|
|
QCOMPARE(helper.sessionId(), QStringLiteral("42"));
|
||
|
|
QVERIFY(helper.hasStarted());
|
||
|
|
helper.terminate();
|
||
|
|
QTRY_COMPARE(finished.count(), 1);
|
||
|
|
qunsetenv(kFakeHelper);
|
||
|
|
}
|
||
|
|
|
||
|
|
void helperReportsAWrongPassword()
|
||
|
|
{
|
||
|
|
qputenv("CUTEFISH_DM_HELPER", QCoreApplication::applicationFilePath().toLocal8Bit());
|
||
|
|
qputenv(kFakeHelper, "1");
|
||
|
|
dm::HelperProcess helper;
|
||
|
|
QSignalSpy failed(&helper, &dm::HelperProcess::failed);
|
||
|
|
QSignalSpy finished(&helper, &dm::HelperProcess::finished);
|
||
|
|
QSignalSpy questions(&helper, &dm::HelperProcess::question);
|
||
|
|
helper.login(QStringLiteral("cutefish-dm"), QStringLiteral("alice"), true, 2);
|
||
|
|
QTRY_COMPARE(questions.count(), 1);
|
||
|
|
helper.answer(QStringLiteral("wrong"));
|
||
|
|
QTRY_COMPARE(finished.count(), 1);
|
||
|
|
QCOMPARE(failed.count(), 1);
|
||
|
|
QVERIFY(failed.first().first().toBool());
|
||
|
|
QVERIFY(!helper.isAuthenticated());
|
||
|
|
qunsetenv(kFakeHelper);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
int main(int argc, char* argv[])
|
||
|
|
{
|
||
|
|
if (qEnvironmentVariableIsSet(kFakeHelper))
|
||
|
|
return fakeHelper();
|
||
|
|
QCoreApplication app(argc, argv);
|
||
|
|
DisplayManagerTest test;
|
||
|
|
return QTest::qExec(&test, argc, argv);
|
||
|
|
}
|
||
|
|
|
||
|
|
#include "dm_test.moc"
|