Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
cmake_minimum_required(VERSION 3.16)

project(alg-app-store VERSION 0.2.28 LANGUAGES CXX)
project(alg-app-store VERSION 0.3.0 LANGUAGES CXX)

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
Expand Down Expand Up @@ -34,6 +34,7 @@ set(SOURCES
# Core
src/core/alpm_wrapper.cpp
src/core/aur_helper.cpp
src/core/auth_manager.cpp
src/core/package_manager.cpp

# GUI
Expand All @@ -55,6 +56,7 @@ set(HEADERS
# Core
src/core/alpm_wrapper.h
src/core/aur_helper.h
src/core/auth_manager.h
src/core/package_manager.h

# GUI
Expand Down Expand Up @@ -87,6 +89,11 @@ target_link_libraries(${PROJECT_NAME} PRIVATE
# Link directories
link_directories(${ALPM_LIBRARY_DIRS})

# Add version definition
target_compile_definitions(${PROJECT_NAME} PRIVATE
APP_VERSION="${PROJECT_VERSION}"
)

# Compiler flags
target_compile_options(${PROJECT_NAME} PRIVATE
-Wall
Expand Down
2 changes: 1 addition & 1 deletion TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
- [] Add detailed mirrorlist tab to set mirrorlist
- [x] Add version information
- [] Implement an AUR helper in core to remove dependence on paru and yay
- [] Ask password only once on startup - startup_auth
- [x] Ask password only once on startup - startup_auth
- [] Improve settings page - settings_tab
- [] Move all styles to single stylesheet - style_and_theme
- [] Clean up UI; make UI look more modern (check gnome's styling options) - style_and_theme
Expand Down
102 changes: 102 additions & 0 deletions src/core/auth_manager.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
#include "auth_manager.h"
#include "../utils/logger.h"
#include <QProcess>
#include <QInputDialog>
#include <QMessageBox>

AuthManager& AuthManager::instance() {
static AuthManager instance;
return instance;
}

AuthManager::AuthManager()
: QObject(nullptr) {
}

AuthManager::~AuthManager() {
// Zero out the password in memory
m_password.fill('\0');
m_password.clear();
}

bool AuthManager::authenticate(QWidget* parent) {
if (m_authenticated) {
return true;
}

const int maxAttempts = 3;

for (int attempt = 0; attempt < maxAttempts; ++attempt) {
bool ok = false;
QString prompt = "Enter your password to continue:";
if (attempt > 0) {
prompt = QString("Incorrect password. Attempt %1 of %2.\nEnter your password:")
.arg(attempt + 1)
.arg(maxAttempts);
}

QString password = QInputDialog::getText(
parent,
"Authentication Required",
prompt,
QLineEdit::Password,
QString(),
&ok
);

if (!ok) {
// User cancelled
Logger::info("Authentication cancelled by user");
return false;
}

QByteArray passwordBytes = password.toUtf8();

if (validatePassword(passwordBytes)) {
m_password = passwordBytes;
m_authenticated = true;
Logger::info("Authentication successful");
return true;
}

Logger::warning(QString("Authentication failed (attempt %1/%2)")
.arg(attempt + 1)
.arg(maxAttempts));
}

QMessageBox::critical(parent, "Authentication Failed",
"Maximum authentication attempts exceeded.\n"
"The application will now exit.");

return false;
}

bool AuthManager::isAuthenticated() const {
return m_authenticated;
}

void AuthManager::writePasswordToProcess(QProcess* process) {
if (!process || !m_authenticated) {
return;
}

process->write(m_password + "\n");
process->closeWriteChannel();
}

bool AuthManager::validatePassword(const QByteArray& password) {
QProcess process;
process.start("sudo", QStringList() << "-S" << "-p" << "" << "true");

if (!process.waitForStarted(3000)) {
Logger::error("Failed to start sudo validation process");
return false;
}

process.write(password + "\n");
process.closeWriteChannel();

process.waitForFinished(10000);

return process.exitCode() == 0;
}
62 changes: 62 additions & 0 deletions src/core/auth_manager.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
#ifndef AUTH_MANAGER_H
#define AUTH_MANAGER_H

#include <QObject>
#include <QByteArray>

class QProcess;
class QWidget;

/**
* @brief Singleton class for managing sudo authentication.
*
* Prompts for the user's password once at startup and reuses it
* for all subsequent privileged operations via sudo -S.
*
* Memory Management:
* - m_password: Zeroed out in destructor for security
*/
class AuthManager : public QObject {
Q_OBJECT

public:
static AuthManager& instance();

~AuthManager() override;

// Disable copy and move
AuthManager(const AuthManager&) = delete;
AuthManager& operator=(const AuthManager&) = delete;
AuthManager(AuthManager&&) = delete;
AuthManager& operator=(AuthManager&&) = delete;

/**
* @brief Show password dialog and validate credentials.
* @param parent Parent widget for the dialog
* @return true if authentication succeeded
*/
bool authenticate(QWidget* parent = nullptr);

/**
* @brief Check if user has been authenticated this session.
*/
bool isAuthenticated() const;

/**
* @brief Write the stored password to a QProcess's stdin.
*
* Call this after QProcess::start() for any process that uses
* sudo -S to read the password from stdin.
*/
void writePasswordToProcess(QProcess* process);

private:
AuthManager();

bool validatePassword(const QByteArray& password);

QByteArray m_password;
bool m_authenticated = false;
};

#endif // AUTH_MANAGER_H
72 changes: 37 additions & 35 deletions src/core/package_manager.cpp
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
#include "package_manager.h"
#include "auth_manager.h"
#include "../utils/logger.h"
#include <QStandardPaths>
#include <QFile>
Expand Down Expand Up @@ -40,7 +41,7 @@ void PackageManager::detectHelper() {
return;
}

// Check for paru - deprecate because paru doesn't allow running with pkexec
// Check for paru - deprecated because paru doesn't allow running as root
// QString paruPath = QStandardPaths::findExecutable("paru");
// if (!paruPath.isEmpty()) {
// m_helper = Helper::Paru;
Expand Down Expand Up @@ -72,17 +73,14 @@ void PackageManager::installPackage(const QString& packageName, const QString& r
bool isAUR = repoLower == "aur";
QString helper = getHelperName();

QString command;
if (isAUR && (m_helper == Helper::Yay)) {
// AUR packages - use pkexec to get userpassword before hand
// AUR packages need sudo for yay
// Paru has a problem here, so default to yay
command = QString("pkexec %1 -S %2 --noconfirm").arg(helper, packageName);
executeCommand("sudo", QStringList() << "-S" << helper << "-S" << packageName << "--noconfirm", true);
} else {
// Official repos and chaotic-aur need root access and use pacman
command = QString("pkexec pacman -S %1 --noconfirm").arg(packageName);
executeCommand("sudo", QStringList() << "-S" << "pacman" << "-S" << packageName << "--noconfirm", true);
}

executeCommand("sh", QStringList() << "-c" << command);
}

void PackageManager::uninstallPackage(const QString& packageName, const QString& repository) {
Expand All @@ -92,9 +90,7 @@ void PackageManager::uninstallPackage(const QString& packageName, const QString&
emit operationStarted(QString("Uninstalling %1...").arg(packageName));

// Uninstall always needs root (even for AUR packages, they're in the system db once installed)
QString command = QString("pkexec pacman -Rdd %1 --noconfirm").arg(packageName);

executeCommand("sh", QStringList() << "-c" << command);
executeCommand("sudo", QStringList() << "-S" << "pacman" << "-Rdd" << packageName << "--noconfirm", true);
}

void PackageManager::updatePackage(const QString& packageName, const QString& repository) {
Expand All @@ -108,16 +104,13 @@ void PackageManager::updatePackage(const QString& packageName, const QString& re
bool isAUR = repoLower == "aur";
QString helper = getHelperName();

QString command;
if (isAUR && (m_helper == Helper::Yay)) {
// AUR packages - run helper as regular user (no pkexec)
command = QString("%1 -S %2 --noconfirm").arg(helper, packageName);
// AUR packages - run helper as regular user (no sudo)
executeCommand(helper, QStringList() << "-S" << packageName << "--noconfirm");
} else {
// Official repos and chaotic-aur need root access and use pacman
command = QString("pkexec pacman -S %1 --noconfirm").arg(packageName);
executeCommand("sudo", QStringList() << "-S" << "pacman" << "-S" << packageName << "--noconfirm", true);
}

executeCommand("sh", QStringList() << "-c" << command);
}

void PackageManager::updateAllPackages() {
Expand All @@ -126,35 +119,38 @@ void PackageManager::updateAllPackages() {
Logger::info("Updating all packages");
emit operationStarted("Updating all packages...");

QString command = QString("pkexec %1 -Syu --noconfirm")
.arg(getHelperName());

executeCommand("sh", QStringList() << "-c" << command);
executeCommand("sudo", QStringList() << "-S" << getHelperName() << "-Syu" << "--noconfirm", true);
}

void PackageManager::executeCommand(const QString& command, const QStringList& args) {
void PackageManager::executeCommand(const QString& command, const QStringList& args, bool needsAuth) {
if (m_process->state() != QProcess::NotRunning) {
Logger::warning("Another operation is already running");
emit operationError("Another operation is already in progress");
return;
}

// Merge stdout and stderr so we capture all output
m_process->setProcessChannelMode(QProcess::MergedChannels);

Logger::debug(QString("Executing: %1 %2").arg(command, args.join(" ")));

// Emit the actual command being executed to the UI for visibility
QString fullCommand = command + " " + args.join(" ");
emit operationOutput(QString(">> Executing: %1\n").arg(fullCommand));

m_process->start(command, args);

// Check if process started successfully
if (!m_process->waitForStarted(3000)) {
QString error = QString("Failed to start process: %1").arg(m_process->errorString());
Logger::error(error);
emit operationError(error);
return;
}

// Write stored password to sudo's stdin if this is an elevated operation
if (needsAuth) {
AuthManager::instance().writePasswordToProcess(m_process.get());
}
}

Expand Down Expand Up @@ -191,29 +187,35 @@ void PackageManager::cancelRunningOperation() {
if (m_process && m_process->state() != QProcess::NotRunning) {
Logger::warning("Cancelling running operation...");
emit operationOutput("\n>>> Operation cancelled by user <<<\n");
// When using pkexec, we need to kill the actual pacman/yay/paru process
// not just the pkexec wrapper. Use pkill to terminate all package manager processes.

// When using sudo, we need to kill the actual pacman/yay/paru process
// not just the sudo wrapper. Use pkill to terminate all package manager processes.
QProcess killProcess;
killProcess.start("pkexec", QStringList() << "bash" << "-c"
killProcess.start("sudo", QStringList() << "-S" << "bash" << "-c"
<< "pkill -TERM pacman; pkill -TERM yay; pkill -TERM paru");
if (killProcess.waitForStarted(3000)) {
AuthManager::instance().writePasswordToProcess(&killProcess);
}
killProcess.waitForFinished(2000);

// Also terminate the QProcess wrapper
m_process->terminate();

// Wait up to 3 seconds for graceful termination
if (!m_process->waitForFinished(3000)) {
// Force kill if still running
Logger::warning("Process did not terminate gracefully, forcing kill...");
killProcess.start("pkexec", QStringList() << "bash" << "-c"
killProcess.start("sudo", QStringList() << "-S" << "bash" << "-c"
<< "pkill -KILL pacman; pkill -KILL yay; pkill -KILL paru");
if (killProcess.waitForStarted(3000)) {
AuthManager::instance().writePasswordToProcess(&killProcess);
}
killProcess.waitForFinished(2000);

m_process->kill();
m_process->waitForFinished(1000);
}

emit operationCompleted(false, "Operation cancelled by user");
Logger::info("Operation cancelled successfully");
} else {
Expand Down
2 changes: 1 addition & 1 deletion src/core/package_manager.h
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ class PackageManager : public QObject {
PackageManager();

void detectHelper();
void executeCommand(const QString& command, const QStringList& args);
void executeCommand(const QString& command, const QStringList& args, bool needsAuth = false);

Helper m_helper = Helper::Pacman;
std::unique_ptr<QProcess> m_process;
Expand Down
Loading