File indexing completed on 2024-04-28 05:27:02

0001 /* vi: ts=8 sts=4 sw=4
0002  *
0003  * This file is part of the KDE project, module kdesu.
0004  * SPDX-FileCopyrightText: 1998 Pietro Iglio <iglio@fub.it>
0005  * SPDX-FileCopyrightText: 1999, 2000 Geert Jansen <jansen@kde.org>
0006  * SPDX-License-Identifier: Artistic-2.0
0007  */
0008 
0009 #include <config-kde-cli-tools.h>
0010 
0011 #include <errno.h>
0012 #include <pwd.h>
0013 #include <stdlib.h>
0014 #include <string.h>
0015 #include <unistd.h>
0016 
0017 #include <sys/resource.h>
0018 #include <sys/time.h>
0019 #if HAVE_SYS_WAIT_H
0020 #include <sys/wait.h>
0021 #endif
0022 #if HAVE_SYS_PRCTL_H
0023 #include <sys/prctl.h>
0024 #endif
0025 #if HAVE_SYS_PROCCTL_H
0026 #include <sys/procctl.h>
0027 #include <unistd.h>
0028 #endif
0029 
0030 #include <QApplication>
0031 #include <QCommandLineParser>
0032 #include <QFile>
0033 #include <QFileInfo>
0034 #include <QLoggingCategory>
0035 #include <private/qtx11extras_p.h>
0036 
0037 #include <KSharedConfig>
0038 #include <kaboutdata.h>
0039 #include <kconfiggroup.h>
0040 #include <klocalizedstring.h>
0041 #include <kmessagebox.h>
0042 #include <kshell.h>
0043 #include <kstartupinfo.h>
0044 #include <kuser.h>
0045 #include <kwindowsystem.h>
0046 
0047 #include <kdesu/client.h>
0048 #include <kdesu/defaults.h>
0049 #include <kdesu/suprocess.h>
0050 
0051 #include "sudlg.h"
0052 
0053 #define ERR strerror(errno)
0054 
0055 static QLoggingCategory category("org.kde.kdesu");
0056 
0057 QByteArray command;
0058 const char *Version = PROJECT_VERSION;
0059 
0060 // NOTE: if you change the position of the -u switch, be sure to adjust it
0061 // at the beginning of main()
0062 
0063 static int startApp(QCommandLineParser &p);
0064 
0065 int main(int argc, char *argv[])
0066 {
0067     // disable ptrace
0068 #if HAVE_PR_SET_DUMPABLE
0069     prctl(PR_SET_DUMPABLE, 0);
0070 #endif
0071 #if HAVE_PROC_TRACE_CTL
0072     int mode = PROC_TRACE_CTL_DISABLE;
0073     procctl(P_PID, getpid(), PROC_TRACE_CTL, &mode);
0074 #endif
0075 
0076     QApplication app(argc, argv);
0077 
0078     // FIXME: this can be considered a poor man's solution, as it's not
0079     // directly obvious to a gui user. :)
0080     // anyway, i vote against removing it even when we have a proper gui
0081     // implementation.  -- ossi
0082     QByteArray duser = qgetenv("ADMIN_ACCOUNT");
0083     if (duser.isEmpty()) {
0084         duser = "root";
0085     }
0086 
0087     KLocalizedString::setApplicationDomain(QByteArrayLiteral("kdesu"));
0088 
0089     KAboutData aboutData(QStringLiteral("kdesu"),
0090                          i18n("KDE su"),
0091                          QLatin1String(Version),
0092                          i18n("Runs a program with elevated privileges."),
0093                          KAboutLicense::Artistic,
0094                          i18n("Copyright (c) 1998-2000 Geert Jansen, Pietro Iglio"));
0095     aboutData.addAuthor(i18n("Geert Jansen"), i18n("Maintainer"), QStringLiteral("jansen@kde.org"), QStringLiteral("http://www.stack.nl/~geertj/"));
0096     aboutData.addAuthor(i18n("Pietro Iglio"), i18n("Original author"), QStringLiteral("iglio@fub.it"));
0097     app.setWindowIcon(QIcon::fromTheme(QStringLiteral("dialog-password")));
0098 
0099     KAboutData::setApplicationData(aboutData);
0100 
0101     // NOTE: if you change the position of the -u switch, be sure to adjust it
0102     // at the beginning of main()
0103     QCommandLineParser parser;
0104     aboutData.setupCommandLine(&parser);
0105     parser.addPositionalArgument(QStringLiteral("command"), i18n("Specifies the command to run as separate arguments"));
0106     parser.addOption(QCommandLineOption(QStringLiteral("c"), i18n("Specifies the command to run as one string"), QStringLiteral("command")));
0107     parser.addOption(QCommandLineOption(QStringLiteral("f"), i18n("Run command under target uid if <file> is not writable"), QStringLiteral("file")));
0108     parser.addOption(QCommandLineOption(QStringLiteral("u"), i18n("Specifies the target uid"), QStringLiteral("user"), QString::fromLatin1(duser)));
0109     parser.addOption(QCommandLineOption(QStringLiteral("n"), i18n("Do not keep password")));
0110     parser.addOption(QCommandLineOption(QStringLiteral("s"), i18n("Stop the daemon (forgets all passwords)")));
0111     parser.addOption(QCommandLineOption(QStringLiteral("t"), i18n("Enable terminal output (no password keeping)")));
0112     parser.addOption(
0113         QCommandLineOption(QStringLiteral("p"), i18n("Set priority value: 0 <= prio <= 100, 0 is lowest"), QStringLiteral("prio"), QStringLiteral("50")));
0114     parser.addOption(QCommandLineOption(QStringLiteral("r"), i18n("Use realtime scheduling")));
0115     parser.addOption(QCommandLineOption(QStringLiteral("noignorebutton"), i18n("Do not display ignore button")));
0116     parser.addOption(QCommandLineOption(QStringLiteral("i"), i18n("Specify icon to use in the password dialog"), QStringLiteral("icon name")));
0117     parser.addOption(QCommandLineOption(QStringLiteral("d"), i18n("Do not show the command to be run in the dialog")));
0118 #if WITH_X11
0119     /* KDialog originally used --embed for attaching the dialog box.  However this is misleading and so we changed to --attach.
0120      * For consistancy, we silently map --embed to --attach */
0121     parser.addOption(QCommandLineOption(QStringLiteral("attach"),
0122                                         i18nc("Transient means that the kdesu app will be attached to the app specified by the winid so that it is like a "
0123                                               "dialog box rather than some separate program",
0124                                               "Makes the dialog transient for an X app specified by winid"),
0125                                         QStringLiteral("winid")));
0126     parser.addOption(QCommandLineOption(QStringLiteral("embed"), i18n("Embed into a window"), QStringLiteral("winid")));
0127 #endif
0128 
0129     // KApplication::disableAutoDcopRegistration();
0130     // kdesu doesn't process SM events, so don't even connect to ksmserver
0131     QByteArray session_manager = qgetenv("SESSION_MANAGER");
0132     if (!session_manager.isEmpty()) {
0133         unsetenv("SESSION_MANAGER");
0134     }
0135     // but propagate it to the started app
0136     if (!session_manager.isEmpty()) {
0137         setenv("SESSION_MANAGER", session_manager.data(), 1);
0138     }
0139 
0140     {
0141 #if WITH_X11
0142         KStartupInfoId id;
0143         id.initId();
0144         id.setupStartupEnv(); // make DESKTOP_STARTUP_ID env. var. available again
0145 #endif
0146     }
0147 
0148     parser.process(app);
0149     aboutData.processCommandLine(&parser);
0150     int result = startApp(parser);
0151 
0152     if (result == 127) {
0153         KMessageBox::error(nullptr, i18n("Cannot execute command '%1'.", QString::fromLocal8Bit(command)));
0154     }
0155     if (result == -2) {
0156         KMessageBox::error(nullptr, i18n("Cannot execute command '%1'. It contains invalid characters.", QString::fromLocal8Bit(command)));
0157     }
0158 
0159     return result;
0160 }
0161 
0162 static int startApp(QCommandLineParser &p)
0163 {
0164     // Stop daemon and exit?
0165     if (p.isSet(QStringLiteral("s"))) {
0166         KDESu::Client client;
0167         if (client.ping() == -1) {
0168             qCCritical(category) << "Daemon not running -- nothing to stop\n";
0169             p.showHelp(1);
0170         }
0171         if (client.stopServer() != -1) {
0172             qCDebug(category) << "Daemon stopped\n";
0173             exit(0);
0174         }
0175         qCCritical(category) << "Could not stop daemon\n";
0176         p.showHelp(1);
0177     }
0178 
0179     QString icon;
0180     if (p.isSet(QStringLiteral("i"))) {
0181         icon = p.value(QStringLiteral("i"));
0182     }
0183 
0184     bool prompt = true;
0185     if (p.isSet(QStringLiteral("d"))) {
0186         prompt = false;
0187     }
0188 
0189     // Get target uid
0190     const QByteArray user = p.value(QStringLiteral("u")).toLocal8Bit();
0191     QByteArray auth_user = user;
0192     struct passwd *pw = getpwnam(user.constData());
0193     if (pw == nullptr) {
0194         qCCritical(category) << "User " << user << " does not exist\n";
0195         p.showHelp(1);
0196     }
0197     bool other_uid = (getuid() != pw->pw_uid);
0198     bool change_uid = other_uid;
0199     if (!change_uid) {
0200         char *cur_user = getenv("USER");
0201         if (!cur_user) {
0202             cur_user = getenv("LOGNAME");
0203         }
0204         change_uid = (!cur_user || user != cur_user);
0205     }
0206 
0207     // If file is writeable, do not change uid
0208     QString file = p.value(QStringLiteral("f"));
0209     if (other_uid && !file.isEmpty()) {
0210         if (file.startsWith(QLatin1Char('/'))) {
0211             file = QStandardPaths::locate(QStandardPaths::GenericConfigLocation, file);
0212             if (file.isEmpty()) {
0213                 qCCritical(category) << "Config file not found: " << file;
0214                 p.showHelp(1);
0215             }
0216         }
0217         QFileInfo fi(file);
0218         if (!fi.exists()) {
0219             qCCritical(category) << "File does not exist: " << file;
0220             p.showHelp(1);
0221         }
0222         change_uid = !fi.isWritable();
0223     }
0224 
0225     // Get priority/scheduler
0226     QString tmp = p.value(QStringLiteral("p"));
0227     bool ok;
0228     int priority = tmp.toInt(&ok);
0229     if (!ok || (priority < 0) || (priority > 100)) {
0230         qCCritical(category) << i18n("Illegal priority: %1", tmp);
0231         p.showHelp(1);
0232     }
0233     int scheduler = SuProcess::SchedNormal;
0234     if (p.isSet(QStringLiteral("r"))) {
0235         scheduler = SuProcess::SchedRealtime;
0236     }
0237     if ((priority > 50) || (scheduler != SuProcess::SchedNormal)) {
0238         change_uid = true;
0239         auth_user = "root";
0240     }
0241 
0242     // Get command
0243     if (p.isSet(QStringLiteral("c"))) {
0244         command = p.value(QStringLiteral("c")).toLocal8Bit();
0245         // Accepting additional arguments here is somewhat weird,
0246         // but one can conceive use cases: have a complex command with
0247         // redirections and additional file names which need to be quoted
0248         // safely.
0249     } else {
0250         if (p.positionalArguments().count() == 0) {
0251             qCCritical(category) << i18n("No command specified.");
0252             p.showHelp(1);
0253         }
0254     }
0255     const auto positionalArguments = p.positionalArguments();
0256     for (const QString &arg : positionalArguments) {
0257         command += ' ';
0258         command += QFile::encodeName(KShell::quoteArg(arg));
0259     }
0260 
0261     // Don't change uid if we're don't need to.
0262     if (!change_uid) {
0263         int result = system(command.constData());
0264         result = WEXITSTATUS(result);
0265         return result;
0266     }
0267 
0268     // Check for daemon and start if necessary
0269     bool just_started = false;
0270     bool have_daemon = true;
0271     KDESu::Client client;
0272     if (client.ping() == -1) {
0273         if (client.startServer() == -1) {
0274             qCWarning(category) << "Could not start daemon, reduced functionality.\n";
0275             have_daemon = false;
0276         }
0277         just_started = true;
0278     }
0279 
0280     // Try to exec the command with kdesud.
0281     bool keep = !p.isSet(QStringLiteral("n")) && have_daemon;
0282     bool terminal = p.isSet(QStringLiteral("t"));
0283     bool withIgnoreButton = !p.isSet(QStringLiteral("noignorebutton"));
0284     int winid = -1;
0285     bool attach = p.isSet(QStringLiteral("attach"));
0286     if (attach) {
0287         winid = p.value(QStringLiteral("attach")).toInt(&attach, 0); // C style parsing.  If the string begins with "0x", base 16 is used; if the string begins
0288                                                                      // with "0", base 8 is used; otherwise, base 10 is used.
0289         if (!attach) {
0290             qCWarning(category) << "Specified winid to attach to is not a valid number";
0291         }
0292     } else if (p.isSet(QStringLiteral("embed"))) {
0293         /* KDialog originally used --embed for attaching the dialog box.  However this is misleading and so we changed to --attach.
0294          * For consistancy, we silently map --embed to --attach */
0295         attach = true;
0296         winid = p.value(QStringLiteral("embed")).toInt(&attach, 0); // C style parsing.  If the string begins with "0x", base 16 is used; if the string begins
0297                                                                     // with "0", base 8 is used; otherwise, base 10 is used.
0298         if (!attach) {
0299             qCWarning(category) << "Specified winid to attach to is not a valid number";
0300         }
0301     }
0302 
0303     QList<QByteArray> env;
0304     QByteArray options;
0305     env << ("DESKTOP_STARTUP_ID=" + QX11Info::nextStartupId());
0306 
0307     //     TODO: Maybe should port this to XDG_*, somehow?
0308     //     if (pw->pw_uid)
0309     //     {
0310     //        // Only propagate KDEHOME for non-root users,
0311     //        // root uses KDEROOTHOME
0312     //
0313     //        // Translate the KDEHOME of this user to the new user.
0314     //        QString kdeHome = KGlobal::dirs()->relativeLocation("home", KGlobal::dirs()->localkdedir());
0315     //        if (kdeHome[0] != '/')
0316     //           kdeHome.prepend("~/");
0317     //        else
0318     //           kdeHome.clear(); // Use default
0319     //
0320     //        env << ("KDEHOME="+ QFile::encodeName(kdeHome));
0321     //     }
0322 
0323     KUser u;
0324     env << (QByteArray)("KDESU_USER=" + u.loginName().toLocal8Bit());
0325 
0326     if (keep && !terminal && !just_started) {
0327         client.setPriority(priority);
0328         client.setScheduler(scheduler);
0329         int result = client.exec(command, user, options, env);
0330         if (result == 0) {
0331             result = client.exitCode();
0332             return result;
0333         }
0334     }
0335 
0336     // Set core dump size to 0 because we will have
0337     // root's password in memory.
0338     struct rlimit rlim;
0339     rlim.rlim_cur = rlim.rlim_max = 0;
0340     if (setrlimit(RLIMIT_CORE, &rlim)) {
0341         qCCritical(category) << "rlimit(): " << ERR;
0342         p.showHelp(1);
0343     }
0344 
0345     // Read configuration
0346     KConfigGroup config(KSharedConfig::openConfig(), "Passwords");
0347     int timeout = config.readEntry("Timeout", defTimeout);
0348 
0349     // Check if we need a password
0350     SuProcess proc;
0351     proc.setUser(auth_user);
0352     int needpw = proc.checkNeedPassword();
0353     if (needpw < 0) {
0354         QString err = i18n("Su returned with an error.\n");
0355         KMessageBox::error(nullptr, err);
0356         p.showHelp(1);
0357     }
0358     if (needpw == 0) {
0359         keep = 0;
0360         qDebug() << "Don't need password!!\n";
0361     }
0362 
0363     const QString cmd = QString::fromLocal8Bit(command);
0364     for (const QChar character : cmd) {
0365         if (!character.isPrint() && character.category() != QChar::Other_Surrogate) {
0366             return -2;
0367         }
0368     }
0369 
0370     // Start the dialog
0371     QString password;
0372     if (needpw) {
0373 #if WITH_X11
0374         KStartupInfoId id;
0375         id.initId();
0376         KStartupInfoData data;
0377         data.setSilent(KStartupInfoData::Yes);
0378         KStartupInfo::sendChange(id, data);
0379 #endif
0380         KDEsuDialog dlg(user, auth_user, keep && !terminal, icon, withIgnoreButton);
0381         if (prompt) {
0382             dlg.addCommentLine(i18n("Command:"), QFile::decodeName(command));
0383         }
0384         if (defKeep) {
0385             dlg.setKeepPassword(true);
0386         }
0387 
0388         if ((priority != 50) || (scheduler != SuProcess::SchedNormal)) {
0389             QString prio;
0390             if (scheduler == SuProcess::SchedRealtime) {
0391                 prio += i18n("realtime: ");
0392             }
0393             prio += QStringLiteral("%1/100").arg(priority);
0394             if (prompt) {
0395                 dlg.addCommentLine(i18n("Priority:"), prio);
0396             }
0397         }
0398 
0399         // Attach dialog
0400 #if WITH_X11
0401         if (attach) {
0402             dlg.setAttribute(Qt::WA_NativeWindow, true);
0403             KWindowSystem::setMainWindow(dlg.windowHandle(), WId(winid));
0404         }
0405 #endif
0406         int ret = dlg.exec();
0407         if (ret == KDEsuDialog::Rejected) {
0408 #if WITH_X11
0409             KStartupInfo::sendFinish(id);
0410 #endif
0411             p.showHelp(1);
0412         }
0413         if (ret == KDEsuDialog::AsUser) {
0414             change_uid = false;
0415         }
0416         password = dlg.password();
0417         keep = dlg.keepPassword();
0418 #if WITH_X11
0419         data.setSilent(KStartupInfoData::No);
0420         KStartupInfo::sendChange(id, data);
0421 #endif
0422     }
0423 
0424     // Some events may need to be handled (like a button animation)
0425     qApp->processEvents();
0426 
0427     // Run command
0428     if (!change_uid) {
0429         int result = system(command.constData());
0430         result = WEXITSTATUS(result);
0431         return result;
0432     } else if (keep && have_daemon) {
0433         client.setPass(password.toLocal8Bit().constData(), timeout);
0434         client.setPriority(priority);
0435         client.setScheduler(scheduler);
0436         int result = client.exec(command, user, options, env);
0437         if (result == 0) {
0438             result = client.exitCode();
0439             return result;
0440         }
0441     } else {
0442         SuProcess proc;
0443         proc.setTerminal(terminal);
0444         proc.setErase(true);
0445         proc.setUser(user);
0446         proc.setEnvironment(env);
0447         proc.setPriority(priority);
0448         proc.setScheduler(scheduler);
0449         proc.setCommand(command);
0450         int result = proc.exec(password.toLocal8Bit().constData());
0451         return result;
0452     }
0453     return -1;
0454 }