diff --git a/CMakeLists.txt b/CMakeLists.txt index e22f940..64f4357 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -22,6 +22,8 @@ set ( HDRS set ( SRCS passworddialog.cpp + sudo.cpp + su.cpp main.cpp ) @@ -56,11 +58,13 @@ add_executable(lxqt-sudo target_link_libraries(lxqt-sudo Qt5::Widgets + util lxqt ) target_compile_definitions(lxqt-sudo PRIVATE "LXQTSUDO_SUDO=\"sudo\"" + PRIVATE "LXQTSUDO_SU=\"su\"" PRIVATE "LXQTSUDO=\"lxqt-sudo\"" PRIVATE "LXQT_VERSION=\"${LXQT_VERSION}\"" ) diff --git a/main.cpp b/main.cpp index 9fdeceb..d3f727a 100644 --- a/main.cpp +++ b/main.cpp @@ -27,30 +27,34 @@ #include #include "passworddialog.h" -#include -#include -#include -#include +#include #include -#include -#include #include +extern const QString app_master; const QString app_master{QStringLiteral(LXQTSUDO)}; const QString app_version{QStringLiteral(LXQT_VERSION)}; -const QString sudo_prog{QStringLiteral(LXQTSUDO_SUDO)}; -const QString sudo_pwd_prompt{QStringLiteral("Password:\n")}; + +extern const QString sudo_prog; +extern int sudo(QStringList const & args, PasswordDialog & dlg); + +extern const QString su_prog; +extern int su(QStringList const & args, PasswordDialog & dlg); void usage(QString const & err = QString()) { if (!err.isEmpty()) QTextStream(stderr) << err << '\n'; QTextStream(stdout) - << QObject::tr("Usage: %1 command [arguments...]\n\n" - "GUI frontend for %2\n\n" + << QObject::tr("Usage: %1 [option] [command [arguments...]]\n\n" + "GUI frontend for %2/%3\n\n" "Arguments:\n" - " command Command to run.\n" - " arguments Optional arguments for command.\n\n").arg(app_master).arg(sudo_prog); + " option:\n" + " -h|--help Print this help.\n" + " -v|--version Print version information.\n" + " -s|--su Use %3(1) as backend (instead of the default %2(8)).\n" + " command Command to run.\n" + " arguments Optional arguments for command.\n\n").arg(app_master).arg(sudo_prog).arg(su_prog); if (!err.isEmpty()) QMessageBox(QMessageBox::Critical, app_master, err, QMessageBox::Ok).exec(); } @@ -61,12 +65,20 @@ void version() << QObject::tr("%1 version %2\n").arg(app_master).arg(app_version); } -int master(int argc, char **argv) +enum backend_t +{ + BACK_SUDO + , BACK_SU +}; + +int main(int argc, char **argv) { - //master LXQt::Application app(argc, argv, true); app.setQuitOnLastWindowClosed(false); + backend_t backend = BACK_SUDO; + QStringList args = app.arguments(); + args.removeAt(0); if (1 >= argc) { usage(QObject::tr("%1: no command to run provided!").arg(app_master)); @@ -83,94 +95,23 @@ int master(int argc, char **argv) { version(); return 0; + } else if ("-s" == arg1 || "--su" == arg1) + { + backend = BACK_SU; + args.removeAt(0); } //any other arguments we simply forward to sudo } - QStringList args = app.arguments(); - //check for provided command is done before - args.removeAt(0); PasswordDialog dlg(args); dlg.setModal(true); - app.setActiveWindow(&dlg); + lxqtApp->setActiveWindow(&dlg); - QScopedPointer sudo{new QProcess}; - QObject::connect(&dlg, &QDialog::finished, [&sudo, &dlg] (int result) - { - if (QDialog::Accepted == result) - { - sudo->write(QByteArray{}.append(dlg.password().append('\n'))); - } else - { - sudo->terminate(); - if (!sudo->waitForFinished(1000)) - sudo->kill(); - } - }); - - //start background process -> sudo - sudo->setProcessChannelMode(QProcess::ForwardedOutputChannel); - sudo->setReadChannel(QProcess::StandardError); - - QString last_line; - int ret; - QObject::connect(sudo.data(), static_cast(&QProcess::finished) - , [&app, &ret, &last_line, &dlg] (int exitCode, QProcess::ExitStatus exitStatus) - { - ret = QProcess::NormalExit == exitStatus ? exitCode : 255; - if (0 != ret && last_line.startsWith(QStringLiteral("%1:").arg(sudo_prog))) - QMessageBox(QMessageBox::Critical, dlg.windowTitle() - , QObject::tr("Child '%1' process failed!\n%2").arg(sudo_prog).arg(last_line), QMessageBox::Ok).exec(); - app.quit(); - }); - - QObject::connect(sudo.data(), &QProcess::readyReadStandardError, [&sudo, &dlg, &last_line] - { - QByteArray err = sudo->readAllStandardError(); - if (sudo_pwd_prompt == err.constData()) - { - dlg.show(); - return; - } - - QTextStream{stderr, QIODevice::WriteOnly} << err; - int nl_pos = err.lastIndexOf('\n'); - if (-1 == nl_pos) - last_line += err; - else - { - if (err.endsWith('\n')) - err.remove(err.size() - 1, 1); - nl_pos = err.lastIndexOf('\n'); - if (-1 != nl_pos) - err.remove(0, nl_pos + 1); - last_line = err; - } - }); - - //forward all stdin to child - QTextStream std_in{stdin, QIODevice::ReadOnly}; - QSocketNotifier stdin_watcher{0/*stdin*/, QSocketNotifier::Read}; - QObject::connect(&stdin_watcher, &QSocketNotifier::activated, [&std_in, &sudo] - { - QString line{std_in.readLine()}; - if (!std_in.atEnd()) - line += QLatin1Char('\n'); - sudo->write(line.toStdString().c_str()); - if (std_in.atEnd()) - sudo->closeWriteChannel(); - }); - - sudo->start(sudo_prog, QStringList() << QStringLiteral("-S") - << QStringLiteral("-p") << sudo_pwd_prompt - << args); - app.exec(); - - sudo->waitForFinished(-1); - return ret; -} - -int main(int argc, char **argv) -{ - return master(argc, argv); + switch (backend) + { + case BACK_SUDO: + return sudo(args, dlg); + case BACK_SU: + return su(args, dlg); + } } diff --git a/man/lxqt-sudo.1 b/man/lxqt-sudo.1 index 333ca12..70a17bb 100644 --- a/man/lxqt-sudo.1 +++ b/man/lxqt-sudo.1 @@ -2,12 +2,20 @@ .SH NAME \fBlxqt-sudo\fR \- execute a command as privileged user .SH SYNOPSIS -\fBlxqt-sudo\fR \fIcommand\fR [\fIarguments\fR] +\fBlxqt-sudo\fR [\fIoption\fR] [\fIcommand\fR [\fIarguments\fR]] .SH DESCRIPTION -\fBlxqt-sudo\fR is a graphical QT frontend for plain \fBsudo(8)\fR (for requesting optional password in GUI fashion). +\fBlxqt-sudo\fR is a graphical QT frontend for plain \fBsudo(8)\fR or \fBsu(1)\fR(for requesting optional password in GUI fashion). .br -When invoked it simply spawns child \fIsudo\fR process with requested \fIcommand\fR (and \fIarguments\fR). If \fIsudo\fR requests user's password, -the GUI password dialog is shown and (after submit) the password is provided to \fIsudo\fR. +When invoked it simply spawns child \fIsudo\fR or \fIsu\fR process with requested \fIcommand\fR (and optional \fIarguments\fR). If \fIsudo\fR/\fIsu\fR requests user's password, +the GUI password dialog is shown and (after submit) the password is provided to backend. +.SH OPTIONS +\fBoption\fR is one of: +.br + -h|--help Print help. +.br + -v|--version Print version information. +.br + -s|--su Use \fBsu\fR as backend (instead of the default \fBsudo\fR). .SH "REPORTING BUGS" Report bugs to https://github.com/lxde/lxqt/issues .SH "SEE ALSO" diff --git a/su.cpp b/su.cpp new file mode 100644 index 0000000..50edbc6 --- /dev/null +++ b/su.cpp @@ -0,0 +1,193 @@ +/* BEGIN_COMMON_COPYRIGHT_HEADER + * (c)LGPL2+ + * + * LXQt - a lightweight, Qt based, desktop toolset + * http://lxqt.org + * + * Copyright: 2015 LXQt team + * Authors: + * Palo Kisa + * + * This program or library is free software; you can redistribute it + * and/or modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + + * You should have received a copy of the GNU Lesser General + * Public License along with this library; if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301 USA + * + * END_COMMON_COPYRIGHT_HEADER */ + +#include +#include "passworddialog.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +extern const QString app_master; +extern const QString su_prog; +const QString su_prog{QStringLiteral(LXQTSUDO_SU)}; +static const QString su_pwd_prompt_end{QStringLiteral(": ")}; +static const QChar nl{QLatin1Char('\n')}; + +static void child(QString const & arg, PasswordDialog & dlg) +{ + std::string s_prog = su_prog.toStdString() + , s_arg = arg.toStdString(); + + char const * params[] = { + s_prog.c_str() + , "-c" + , s_arg.c_str() + , nullptr + }; + + setsid(); //session leader + execvp(params[0], const_cast(params)); + + //exec never returns in case of success + QTextStream{stderr, QIODevice::WriteOnly} << QObject::tr("%1: Failed to exec '%2': %3\n").arg(app_master).arg(su_prog).arg(strerror(errno)); + exit(1); +} + +static void stopChild(int & childPid + , int & pwdFd + , int & ret) +{ + close(pwdFd); + kill(childPid, SIGINT); + int res, status; + for (int cnt = 10; 0 == (res = waitpid(childPid, &status, WNOHANG)) && 0 < cnt; --cnt) + QThread::msleep(100); + + if (0 == res) + { + kill(childPid, SIGKILL); + ret = 1; + } else + { + ret = WIFEXITED(status) ? WEXITSTATUS(status) : 1; + } + childPid = -1; +} + +static int parent(int childPid, int pwdFd, PasswordDialog & dlg) +{ + //set the FD as non-blocking + if (0 != fcntl(pwdFd, F_SETFL, O_NONBLOCK)) + { + QMessageBox(QMessageBox::Critical, dlg.windowTitle() + , QObject::tr("Failed to set non-block: %1").arg(strerror(errno)), QMessageBox::Ok).exec(); + return 1; + } + + FILE * pwd_f = fdopen(pwdFd, "r+"); + if (nullptr == pwd_f) + { + QMessageBox(QMessageBox::Critical, dlg.windowTitle() + , QObject::tr("Failed to fdopen: %1").arg(strerror(errno)), QMessageBox::Ok).exec(); + return 1; + } + + QTextStream child_str{pwd_f}; + + int ret; + QObject::connect(&dlg, &QDialog::finished, [&] (int result) + { + if (QDialog::Accepted == result) + { + child_str << dlg.password().append(nl); + child_str.flush(); + } else + { + stopChild(childPid, pwdFd, ret); + lxqtApp->quit(); + } + }); + + bool check_pwd = true; + QString last_line; + QScopedPointer pwd_watcher{new QSocketNotifier{pwdFd, QSocketNotifier::Read}}; + QObject::connect(pwd_watcher.data(), &QSocketNotifier::activated, [&] + { + QString line = child_str.readAll(); + if (line.isEmpty()) + { + pwd_watcher.reset(nullptr); + if (last_line.startsWith(QStringLiteral("%1:").arg(su_prog))) + { + pwd_watcher.reset(nullptr); //stop the notifications events + stopChild(childPid, pwdFd, ret); + QMessageBox(QMessageBox::Critical, dlg.windowTitle() + , QObject::tr("Child '%1' process failed!\n%2").arg(su_prog).arg(last_line), QMessageBox::Ok).exec(); + } + lxqtApp->quit(); + } else + { + if (check_pwd) + { + //check only first output of child(su) + check_pwd = false; + if (!line.contains(nl) && line.endsWith(su_pwd_prompt_end)) + { + //if now echo is turned off, su requests password struct termios tios; + struct termios tios; + Q_ASSERT(0 == tcgetattr(pwdFd, &tios)); + if (!(ECHO & tios.c_lflag)) + { + dlg.show(); + return; + } + } + } + QTextStream{stderr, QIODevice::WriteOnly} << line; + //assuming text oriented output + QStringList lines = line.split(nl, QString::SkipEmptyParts); + last_line = lines.isEmpty() ? QString() : lines.back(); + } + + }); + + lxqtApp->exec(); + + if (0 < childPid) + { + int res, status; + res = waitpid(childPid, &status, 0); + ret = (childPid == res && WIFEXITED(status)) ? WEXITSTATUS(status) : 1; + } + + return ret; +} + +int su(QStringList const & args, PasswordDialog & dlg) +{ + int pid, fd; + if (1 != args.size()) + QMessageBox(QMessageBox::Critical, dlg.windowTitle() + , QObject::tr("With %1 backend only one argument/command is supported!").arg(su_prog), QMessageBox::Ok).exec(); + else if (0 == (pid = forkpty(&fd, nullptr, nullptr, nullptr))) + child(args[0], dlg); //never returns + else if (-1 == pid) + QMessageBox(QMessageBox::Critical, dlg.windowTitle() + , QObject::tr("Failed to fork: %1").arg(strerror(errno)), QMessageBox::Ok).exec(); + else + return parent(pid, fd, dlg); + + return 1; +} + diff --git a/sudo.cpp b/sudo.cpp new file mode 100644 index 0000000..c028e8e --- /dev/null +++ b/sudo.cpp @@ -0,0 +1,116 @@ +/* BEGIN_COMMON_COPYRIGHT_HEADER + * (c)LGPL2+ + * + * LXQt - a lightweight, Qt based, desktop toolset + * http://lxqt.org + * + * Copyright: 2015 LXQt team + * Authors: + * Palo Kisa + * + * This program or library is free software; you can redistribute it + * and/or modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + + * You should have received a copy of the GNU Lesser General + * Public License along with this library; if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301 USA + * + * END_COMMON_COPYRIGHT_HEADER */ + +#include +#include "passworddialog.h" +#include +#include +#include +#include + +extern const QString sudo_prog; +const QString sudo_prog{QStringLiteral(LXQTSUDO_SUDO)}; +const QString sudo_pwd_prompt{QStringLiteral("Password:\n")}; + +int sudo(QStringList const & args, PasswordDialog & dlg) +{ + QScopedPointer sudo{new QProcess}; + QObject::connect(&dlg, &QDialog::finished, [&sudo, &dlg] (int result) + { + if (QDialog::Accepted == result) + { + sudo->write(QByteArray{}.append(dlg.password().append('\n'))); + } else + { + sudo->terminate(); + if (!sudo->waitForFinished(1000)) + sudo->kill(); + } + }); + + //start background process -> sudo + sudo->setProcessChannelMode(QProcess::ForwardedOutputChannel); + sudo->setReadChannel(QProcess::StandardError); + + QString last_line; + int ret; + QObject::connect(sudo.data(), static_cast(&QProcess::finished) + , [&ret, &last_line, &dlg] (int exitCode, QProcess::ExitStatus exitStatus) + { + ret = QProcess::NormalExit == exitStatus ? exitCode : 255; + if (0 != ret && last_line.startsWith(QStringLiteral("%1:").arg(sudo_prog))) + QMessageBox(QMessageBox::Critical, dlg.windowTitle() + , QObject::tr("Child '%1' process failed!\n%2").arg(sudo_prog).arg(last_line), QMessageBox::Ok).exec(); + lxqtApp->quit(); + }); + + QObject::connect(sudo.data(), &QProcess::readyReadStandardError, [&sudo, &dlg, &last_line] + { + QByteArray err = sudo->readAllStandardError(); + if (sudo_pwd_prompt == err.constData()) + { + dlg.show(); + return; + } + + QTextStream{stderr, QIODevice::WriteOnly} << err; + int nl_pos = err.lastIndexOf('\n'); + if (-1 == nl_pos) + last_line += err; + else + { + if (err.endsWith('\n')) + err.remove(err.size() - 1, 1); + nl_pos = err.lastIndexOf('\n'); + if (-1 != nl_pos) + err.remove(0, nl_pos + 1); + last_line = err; + } + }); + + //forward all stdin to child + QTextStream std_in{stdin, QIODevice::ReadOnly}; + QSocketNotifier stdin_watcher{0/*stdin*/, QSocketNotifier::Read}; + QObject::connect(&stdin_watcher, &QSocketNotifier::activated, [&std_in, &sudo] + { + QString line{std_in.readLine()}; + if (!std_in.atEnd()) + line += QLatin1Char('\n'); + sudo->write(line.toStdString().c_str()); + if (std_in.atEnd()) + sudo->closeWriteChannel(); + }); + + sudo->start(sudo_prog, QStringList() << QStringLiteral("-S") + << QStringLiteral("-p") << sudo_pwd_prompt + << args); + lxqtApp->exec(); + + sudo->waitForFinished(-1); + return ret; +} +