diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000..90314ef --- /dev/null +++ b/.clang-format @@ -0,0 +1,65 @@ +--- +Language: Cpp +BasedOnStyle: LLVM + +AccessModifierOffset: -2 +AlignAfterOpenBracket: Align +AlignConsecutiveMacros: true +AlignConsecutiveAssignments: true +AlignEscapedNewlines: Right +AlignOperands: false +AlignTrailingComments: true +AllowAllArgumentsOnNextLine: true +AllowAllConstructorInitializersOnNextLine: true +AllowAllParametersOfDeclarationOnNextLine: true +AllowShortBlocksOnASingleLine: true +AllowShortCaseLabelsOnASingleLine: true +AllowShortFunctionsOnASingleLine: Empty +AllowShortIfStatementsOnASingleLine: Never +AllowShortLambdasOnASingleLine: All +AllowShortLoopsOnASingleLine: false +AlwaysBreakAfterDefinitionReturnType: None +AlwaysBreakAfterReturnType: None +AlwaysBreakBeforeMultilineStrings: false +AlwaysBreakTemplateDeclarations: Yes +BreakBeforeBraces: Attach +BreakBeforeTernaryOperators: false +BreakConstructorInitializers: AfterColon +ColumnLimit: 180 +CompactNamespaces: false +ConstructorInitializerAllOnOneLineOrOnePerLine: false +ExperimentalAutoDetectBinPacking: false +FixNamespaceComments: false +IncludeBlocks: Preserve +IndentCaseLabels: true +IndentWidth: 4 +PointerAlignment: Left +ReflowComments: false +SortIncludes: false +SortUsingDeclarations: false +SpaceAfterCStyleCast: false +SpaceAfterLogicalNot: false +SpaceAfterTemplateKeyword: true +SpaceBeforeCtorInitializerColon: true +SpaceBeforeInheritanceColon: true +SpaceBeforeParens: ControlStatements +SpaceBeforeRangeBasedForLoopColon: true +SpaceInEmptyParentheses: false +SpacesBeforeTrailingComments: 1 +SpacesInAngles: false +SpacesInCStyleCastParentheses: false +SpacesInContainerLiterals: false +SpacesInParentheses: false +SpacesInSquareBrackets: false +Standard: Auto +TabWidth: 4 +UseTab: Never + +AllowShortEnumsOnASingleLine: false + +BraceWrapping: + AfterEnum: false + +AlignConsecutiveDeclarations: AcrossEmptyLines + +NamespaceIndentation: All diff --git a/.gitignore b/.gitignore index 259148f..49a9e3b 100644 --- a/.gitignore +++ b/.gitignore @@ -30,3 +30,9 @@ *.exe *.out *.app + +.vscode/ +.cache/ +build/ + +compile_commands.json diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..a303861 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "subprojects/sdbus-cpp"] + path = subprojects/sdbus-cpp + url = https://github.com/Kistler-Group/sdbus-cpp/ diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..98f1980 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,58 @@ +cmake_minimum_required(VERSION 3.16) + +# Get version +file(READ "${CMAKE_SOURCE_DIR}/VERSION" VER_RAW) +string(STRIP ${VER_RAW} VER) + +project(hpa VERSION ${VER} LANGUAGES CXX) + +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +find_package(Qt6 6.5 REQUIRED COMPONENTS Widgets Quick QuickControls2) +find_package(PkgConfig REQUIRED) +set(CMAKE_CXX_STANDARD 23) + +pkg_check_modules( + deps + REQUIRED + IMPORTED_TARGET + hyprutils + polkit-agent-1 + polkit-qt6-1) + +qt_standard_project_setup(REQUIRES 6.5) + +qt_add_executable(hyprpolkitagent + src/main.cpp + src/core/Agent.cpp + src/core/Agent.hpp + src/core/PolkitListener.hpp + src/core/PolkitListener.cpp + src/QMLIntegration.cpp + src/QMLIntegration.hpp + src/SigDaemon.hpp + src/SigDaemon.cpp +) + +qt_add_qml_module(hyprpolkitagent + URI hpa + VERSION 1.0 + QML_FILES + qml/main.qml + SOURCES + src/QMLIntegration.cpp + src/QMLIntegration.hpp + src/SigDaemon.hpp + src/SigDaemon.cpp +) + +target_link_libraries(hyprpolkitagent + PRIVATE Qt6::Widgets Qt6::Quick Qt6::Gui Qt6::QuickControls2 PkgConfig::deps +) + +include(GNUInstallDirs) +install(TARGETS hyprpolkitagent + BUNDLE DESTINATION . + LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} + RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} +) diff --git a/VERSION b/VERSION new file mode 100644 index 0000000..6c6aa7c --- /dev/null +++ b/VERSION @@ -0,0 +1 @@ +0.1.0 \ No newline at end of file diff --git a/qml/main.qml b/qml/main.qml new file mode 100644 index 0000000..9fcd82e --- /dev/null +++ b/qml/main.qml @@ -0,0 +1,141 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts + +ApplicationWindow { + id: window + + property var windowWidth: 550 + property var windowHeight: 240 + + maximumWidth: windowWidth + maximumHeight: windowHeight + minimumWidth: windowWidth + minimumHeight: windowHeight + visible: true + + FontMetrics { + id: fontMetrics + } + + SystemPalette { + id: system + + colorGroup: SystemPalette.Active + } + + Item { + anchors.fill: parent + Keys.onEscapePressed: (e) => { + hpa.setResult("fail"); + } + Keys.onReturnPressed: (e) => { + hpa.setResult("auth:" + passwordField.text); + } + + ColumnLayout { + anchors.fill: parent + anchors.margins: 4 + + Text { + color: Qt.darker(system.windowText, 0.8) + font.bold: true + font.pointSize: Math.round(fontMetrics.height * 1.05) + text: "Authenticating for " + hpa.getUser() + Layout.alignment: Qt.AlignHCenter + } + + HSeparator { + Layout.topMargin: fontMetrics.height / 2 + Layout.bottomMargin: fontMetrics.height / 2 + } + + Text { + color: system.windowText + text: hpa.getMessage() + } + + TextField { + id: passwordField + + Layout.topMargin: fontMetrics.height / 2 + placeholderText: "Password" + Layout.alignment: Qt.AlignHCenter + hoverEnabled: true + persistentSelection: true + echoMode: TextInput.Password + focus: true + + Connections { + target: hpa + onFocusField: () => { + passwordField.focus = true; + } + } + + } + + Text { + id: errorLabel + + color: "red" + font.italic: true + Layout.topMargin: fontMetrics.height + text: "" + Layout.alignment: Qt.AlignHCenter + + Connections { + target: hpa + onSetErrorString: (e) => { + errorLabel.text = e; + } + } + + } + + Rectangle { + color: "transparent" + Layout.fillHeight: true + } + + HSeparator { + Layout.topMargin: fontMetrics.height / 2 + Layout.bottomMargin: fontMetrics.height / 2 + } + + RowLayout { + Layout.alignment: Qt.AlignRight + Layout.rightMargin: fontMetrics.height / 2 + + Button { + text: "Cancel" + onClicked: (e) => { + hpa.setResult("fail"); + } + } + + Button { + text: "Authenticate" + onClicked: (e) => { + hpa.setResult("auth:" + passwordField.text); + } + } + + } + + } + + } + + component Separator: Rectangle { + color: Qt.darker(window.palette.text, 1.5) + } + + component HSeparator: Separator { + implicitHeight: 1 + Layout.fillWidth: true + Layout.leftMargin: fontMetrics.height * 8 + Layout.rightMargin: fontMetrics.height * 8 + } + +} diff --git a/src/QMLIntegration.cpp b/src/QMLIntegration.cpp new file mode 100644 index 0000000..eff2e5a --- /dev/null +++ b/src/QMLIntegration.cpp @@ -0,0 +1,29 @@ +#include "QMLIntegration.hpp" + +#include "core/Agent.hpp" +#include "core/PolkitListener.hpp" + +void CQMLIntegration::onExit() { + g_pAgent->submitResultThreadSafe(result.toStdString()); +} + +void CQMLIntegration::setResult(QString str) { + result = str; + g_pAgent->submitResultThreadSafe(result.toStdString()); +} + +QString CQMLIntegration::getMessage() { + return g_pAgent->listener.session.inProgress ? g_pAgent->listener.session.message : "An application is requesting authentication."; +} + +QString CQMLIntegration::getUser() { + return g_pAgent->listener.session.inProgress ? g_pAgent->listener.session.selectedUser.toString() : "an unknown user"; +} + +void CQMLIntegration::setError(QString str) { + emit setErrorString(str); +} + +void CQMLIntegration::focus() { + emit focusField(); +} diff --git a/src/QMLIntegration.hpp b/src/QMLIntegration.hpp new file mode 100644 index 0000000..c58fba4 --- /dev/null +++ b/src/QMLIntegration.hpp @@ -0,0 +1,36 @@ +#pragma once + +#include +#include +#include +#include + +class CQMLIntegration : public QObject { + Q_OBJECT; + Q_PROPERTY(QString errorText MEMBER errorText); + + public: + explicit CQMLIntegration(QObject* parent = nullptr) : QObject(parent) { + ; + } + virtual ~CQMLIntegration() { + ; + } + + void setError(QString str); + void focus(); + + QString result = "fail", errorText = ""; + + Q_INVOKABLE QString getMessage(); + Q_INVOKABLE QString getUser(); + + Q_INVOKABLE void setResult(QString str); + + public slots: + void onExit(); + + signals: + void setErrorString(QString err); + void focusField(); +}; diff --git a/src/SigDaemon.cpp b/src/SigDaemon.cpp new file mode 100644 index 0000000..198f59d --- /dev/null +++ b/src/SigDaemon.cpp @@ -0,0 +1,81 @@ +#include "SigDaemon.hpp" +#include "core/Agent.hpp" + +#include + +#include +#include + +static int sighupFd[2]; +static int sigtermFd[2]; +static int sigintFd[2]; + +// +void CSigDaemon::onSignal(int signo) { + char a = 1; + if (signo == SIGHUP) + ::write(sighupFd[0], &a, sizeof(a)); + else if (signo == SIGINT) + ::write(sigintFd[0], &a, sizeof(a)); + else if (signo == SIGTERM) + ::write(sigtermFd[0], &a, sizeof(a)); +} + +CSigDaemon::CSigDaemon(QObject* parent) : QObject(parent) { + if (::socketpair(AF_UNIX, SOCK_STREAM, 0, sighupFd)) + qFatal("Couldn't create HUP socketpair"); + if (::socketpair(AF_UNIX, SOCK_STREAM, 0, sigtermFd)) + qFatal("Couldn't create TERM socketpair"); + if (::socketpair(AF_UNIX, SOCK_STREAM, 0, sigintFd)) + qFatal("Couldn't create INT socketpair"); + snHup = new QSocketNotifier(sighupFd[1], QSocketNotifier::Read, this); + connect(snHup, SIGNAL(activated(QSocketDescriptor)), this, SLOT(handleSigHup())); + snTerm = new QSocketNotifier(sigtermFd[1], QSocketNotifier::Read, this); + connect(snTerm, SIGNAL(activated(QSocketDescriptor)), this, SLOT(handleSigTerm())); + snInt = new QSocketNotifier(sigintFd[1], QSocketNotifier::Read, this); + connect(snInt, SIGNAL(activated(QSocketDescriptor)), this, SLOT(handleSigInt())); + + struct sigaction sa; + + sa.sa_handler = onSignal; + sigemptyset(&sa.sa_mask); + sa.sa_flags = 0; + sa.sa_flags |= SA_RESTART; + + if (sigaction(SIGHUP, &sa, 0)) + std::print(stderr, "sigaction for hup failed\n"); + + if (sigaction(SIGTERM, &sa, 0)) + std::print(stderr, "sigaction for term failed\n"); + + if (sigaction(SIGINT, &sa, 0)) + std::print(stderr, "sigaction for int failed\n"); +} + +void CSigDaemon::handleSigHup() { + std::print("> signal received: SIGHUP\n"); + snHup->setEnabled(false); + char tmp; + ::read(sighupFd[1], &tmp, sizeof(tmp)); + g_pAgent->resetAuthState(); + snHup->setEnabled(true); +} + +void CSigDaemon::handleSigInt() { + std::print("> signal received: SIGINT\n"); + snInt->setEnabled(false); + char tmp; + ::read(sigintFd[1], &tmp, sizeof(tmp)); + g_pAgent->resetAuthState(); + snInt->setEnabled(true); + exit(0); +} + +void CSigDaemon::handleSigTerm() { + std::print("> signal received: SIGTERM\n"); + snTerm->setEnabled(false); + char tmp; + ::read(sigtermFd[1], &tmp, sizeof(tmp)); + g_pAgent->resetAuthState(); + snTerm->setEnabled(true); +} diff --git a/src/SigDaemon.hpp b/src/SigDaemon.hpp new file mode 100644 index 0000000..5b7efde --- /dev/null +++ b/src/SigDaemon.hpp @@ -0,0 +1,23 @@ +#pragma once + +#include +#include +#include + +class CSigDaemon : public QObject { + Q_OBJECT; + public: + CSigDaemon(QObject* parent = nullptr); + + static void onSignal(int signo); + + public slots: + void handleSigHup(); + void handleSigTerm(); + void handleSigInt(); + + private: + QSocketNotifier* snHup = nullptr; + QSocketNotifier* snTerm = nullptr; + QSocketNotifier* snInt = nullptr; +}; diff --git a/src/core/Agent.cpp b/src/core/Agent.cpp new file mode 100644 index 0000000..a5506c1 --- /dev/null +++ b/src/core/Agent.cpp @@ -0,0 +1,96 @@ +#define POLKIT_AGENT_I_KNOW_API_IS_SUBJECT_TO_CHANGE 1 + +#include +#include + +#include "Agent.hpp" +#include "../QMLIntegration.hpp" +#include "../SigDaemon.hpp" + +CAgent::CAgent() { + ; +} + +CAgent::~CAgent() { + ; +} + +bool CAgent::start() { + sessionSubject = makeShared(getpid()); + + listener.registerListener(*sessionSubject, "/org/hyprland/PolicyKit1/AuthenticationAgent"); + + int argc = 1; + char* argv = (char*)"hyprpolkitagent"; + QApplication app(argc, &argv); + + sigDaemon = makeShared(); + + app.setApplicationName("Hyprland Polkit Agent"); + QGuiApplication::setQuitOnLastWindowClosed(false); + + app.exec(); + + return true; +} + +void CAgent::resetAuthState() { + if (authState.authing) { + authState.authing = false; + authState.qmlEngine.reset(); + authState.qmlIntegration.reset(); + } +} + +void CAgent::initAuthPrompt() { + resetAuthState(); + + if (!listener.session.inProgress) { + std::print(stderr, "INTERNAL ERROR: Spawning qml prompt but session isn't in progress\n"); + return; + } + + std::print("Spawning qml prompt\n"); + + authState.qmlEngine.reset(); + authState.authing = true; + + authState.qmlIntegration = makeShared(); + + if (qEnvironmentVariableIsEmpty("QT_QUICK_CONTROLS_STYLE")) + QQuickStyle::setStyle("org.kde.desktop"); + + authState.qmlEngine = makeShared(); + authState.qmlEngine->rootContext()->setContextProperty("hpa", authState.qmlIntegration.get()); + authState.qmlEngine->load(QUrl{u"qrc:/qt/qml/hpa/qml/main.qml"_qs}); + + authState.qmlIntegration->focusField(); +} + +bool CAgent::resultReady() { + std::lock_guard lg(lastAuthResult.resultMutex); + + return !lastAuthResult.used; +} + +void CAgent::submitResultThreadSafe(const std::string& result) { + std::lock_guard lg(lastAuthResult.resultMutex); + lastAuthResult.used = false; + lastAuthResult.result = result; + + const bool PASS = result.starts_with("auth:"); + + std::print("Got result from qml: {}\n", PASS ? "auth:**PASSWORD**" : result); + + if (PASS) + listener.submitPassword(result.substr(result.find(":") + 1).c_str()); + else + listener.cancelPending(); +} + +void CAgent::setAuthError(const QString& err) { + if (!authState.qmlIntegration) + return; + + authState.qmlIntegration->setErrorString(err); +} diff --git a/src/core/Agent.hpp b/src/core/Agent.hpp new file mode 100644 index 0000000..fb7984c --- /dev/null +++ b/src/core/Agent.hpp @@ -0,0 +1,53 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include "PolkitListener.hpp" +#include + +#include +using namespace Hyprutils::Memory; +#define SP CSharedPointer +#define WP CWeakPointer + +class CQMLIntegration; +class CSigDaemon; + +class CAgent { + public: + CAgent(); + ~CAgent(); + + void submitResultThreadSafe(const std::string& result); + void resetAuthState(); + bool start(); + void initAuthPrompt(); + void setAuthError(const QString& err); + + private: + struct { + bool authing = false; + SP qmlEngine; + SP qmlIntegration; + } authState; + + struct { + std::mutex resultMutex; + std::string result; + bool used = true; + } lastAuthResult; + + CPolkitListener listener; + SP sigDaemon; + SP sessionSubject; + + bool resultReady(); + + friend class CQMLIntegration; +}; + +inline std::unique_ptr g_pAgent; \ No newline at end of file diff --git a/src/core/PolkitListener.cpp b/src/core/PolkitListener.cpp new file mode 100644 index 0000000..8138db5 --- /dev/null +++ b/src/core/PolkitListener.cpp @@ -0,0 +1,142 @@ +#include +#include + +#include "PolkitListener.hpp" +#include "Agent.hpp" +#include + +#include + +using namespace PolkitQt1::Agent; + +CPolkitListener::CPolkitListener(QObject* parent) : Listener(parent) { + ; +} + +void CPolkitListener::initiateAuthentication(const QString& actionId, const QString& message, const QString& iconName, const PolkitQt1::Details& details, const QString& cookie, + const PolkitQt1::Identity::List& identities, AsyncResult* result) { + + std::print("> New authentication session\n"); + + if (session.inProgress) { + result->setError("Authentication in progress"); + result->setCompleted(); + std::print("> REJECTING: Another session present\n"); + return; + } + + if (identities.isEmpty()) { + result->setError("No identities, this is a problem with your system configuration."); + result->setCompleted(); + std::print("> REJECTING: No idents\n"); + return; + } + + session.selectedUser = identities.at(0); + session.cookie = cookie; + session.result = result; + session.actionId = actionId; + session.message = message; + session.iconName = iconName; + session.gainedAuth = false; + session.cancelled = false; + session.inProgress = true; + + g_pAgent->initAuthPrompt(); + + reattempt(); +} + +void CPolkitListener::reattempt() { + session.cancelled = false; + + session.session = new Session(session.selectedUser, session.cookie, session.result); + connect(session.session, SIGNAL(request(QString, bool)), this, SLOT(request(QString, bool))); + connect(session.session, SIGNAL(completed(bool)), this, SLOT(completed(bool))); + connect(session.session, SIGNAL(showError(QString)), this, SLOT(showError(QString))); + connect(session.session, SIGNAL(showInfo(QString)), this, SLOT(showInfo(QString))); + + session.session->initiate(); +} + +bool CPolkitListener::initiateAuthenticationFinish() { + std::print("> initiateAuthenticationFinish()\n"); + return true; +} + +void CPolkitListener::cancelAuthentication() { + std::print("> cancelAuthentication()\n"); + + session.cancelled = true; + + finishAuth(); +} + +void CPolkitListener::request(const QString& request, bool echo) { + std::print("> PKS request: {} echo: {}\n", request.toStdString(), echo); +} + +void CPolkitListener::completed(bool gainedAuthorization) { + std::print("> PKS completed: {}\n", gainedAuthorization ? "Auth successful" : "Auth unsuccessful"); + + session.gainedAuth = gainedAuthorization; + + if (!gainedAuthorization) + g_pAgent->setAuthError("Authentication failed"); + + finishAuth(); +} + +void CPolkitListener::showError(const QString& text) { + std::print("> PKS showError: {}\n", text.toStdString()); + + g_pAgent->setAuthError(text); +} + +void CPolkitListener::showInfo(const QString& text) { + std::print("> PKS showInfo: {}\n", text.toStdString()); +} + +void CPolkitListener::finishAuth() { + if (!session.inProgress) { + std::print("> finishAuth: ODD. !session.inProgress\n"); + return; + } + + if (!session.gainedAuth && !session.cancelled) { + std::print("> finishAuth: Did not gain auth. Reattempting.\n"); + session.session->deleteLater(); + reattempt(); + return; + } + + std::print("> finishAuth: Gained auth, cleaning up.\n"); + + session.inProgress = false; + + if (session.session) { + session.session->result()->setCompleted(); + session.session->deleteLater(); + } else + session.result->setCompleted(); + + g_pAgent->resetAuthState(); +} + +void CPolkitListener::submitPassword(const QString& pass) { + if (!session.session) + return; + + session.session->setResponse(pass); +} + +void CPolkitListener::cancelPending() { + if (!session.session) + return; + + session.session->cancel(); + + session.cancelled = true; + + finishAuth(); +} diff --git a/src/core/PolkitListener.hpp b/src/core/PolkitListener.hpp new file mode 100644 index 0000000..fae981b --- /dev/null +++ b/src/core/PolkitListener.hpp @@ -0,0 +1,47 @@ +#pragma once + +#include +#include + +#include +#include +#include +#include + +class CPolkitListener : public PolkitQt1::Agent::Listener { + Q_OBJECT; + Q_DISABLE_COPY(CPolkitListener); + + public: + CPolkitListener(QObject* parent = nullptr); + ~CPolkitListener() override {}; + + void submitPassword(const QString& pass); + void cancelPending(); + + public Q_SLOTS: + void initiateAuthentication(const QString& actionId, const QString& message, const QString& iconName, const PolkitQt1::Details& details, const QString& cookie, + const PolkitQt1::Identity::List& identities, PolkitQt1::Agent::AsyncResult* result) override; + bool initiateAuthenticationFinish() override; + void cancelAuthentication() override; + + void request(const QString& request, bool echo); + void completed(bool gainedAuthorization); + void showError(const QString& text); + void showInfo(const QString& text); + + private: + struct { + bool inProgress = false, cancelled = false, gainedAuth = false; + QString cookie, message, iconName, actionId; + PolkitQt1::Agent::AsyncResult* result = nullptr; + PolkitQt1::Identity selectedUser; + PolkitQt1::Agent::Session* session = nullptr; + } session; + + void reattempt(); + void finishAuth(); + + friend class CAgent; + friend class CQMLIntegration; +}; diff --git a/src/main.cpp b/src/main.cpp new file mode 100644 index 0000000..ba62387 --- /dev/null +++ b/src/main.cpp @@ -0,0 +1,7 @@ +#include "core/Agent.hpp" + +int main(int argc, char* argv[]) { + g_pAgent = std::make_unique(); + + return g_pAgent->start() == false ? 1 : 0; +}