File indexing completed on 2025-01-05 05:07:03

0001 // SPDX-License-Identifier: GPL-2.0-or-later
0002 // SPDX-FileCopyrightText: 2011 Craig Drummond <craig.p.drummond@gmail.com>
0003 // SPDX-FileCopyrightText: 2018 Alexis Lopes Zubeta <contact@azubieta.net>
0004 // SPDX-FileCopyrightText: 2020 Tomaz Canabrava <tcanabrava@kde.org>
0005 /*
0006  * UFW KControl Module
0007  */
0008 
0009 #include "helper.h"
0010 #include <QByteArray>
0011 #include <QDebug>
0012 #include <QDir>
0013 #include <QFile>
0014 #include <QProcess>
0015 #include <QProcessEnvironment>
0016 #include <QString>
0017 #include <QStringList>
0018 #include <QStandardPaths>
0019 #include <sys/stat.h>
0020 
0021 #include <KAuth/HelperSupport>
0022 #include <KLocalizedString>
0023 
0024 #include "ufw_helper_config.h"
0025 
0026 namespace
0027 {
0028 constexpr int FILE_PERMS = 0644;
0029 constexpr int DIR_PERMS = 0755;
0030 const QString KCM_UFW_DIR = QStringLiteral("/etc/kcm_ufw");
0031 const QString PROFILE_EXTENSION = QStringLiteral(".ufw");
0032 
0033 void setPermissions(const QString &f, int perms)
0034 {
0035     // Clear any umask before setting file perms
0036     mode_t oldMask(umask(0000));
0037     ::chmod(QFile::encodeName(f).constData(), perms);
0038     // Reset umask
0039     ::umask(oldMask);
0040 }
0041 
0042 void checkFolder()
0043 {
0044     QDir d(KCM_UFW_DIR);
0045 
0046     if (!d.exists()) {
0047         d.mkpath(KCM_UFW_DIR);
0048         setPermissions(d.absolutePath(), DIR_PERMS);
0049     }
0050 }
0051 
0052 } // namespace
0053 
0054 namespace UFW
0055 {
0056 ActionReply Helper::query(const QVariantMap &args)
0057 {
0058     ActionReply reply =
0059         args[QStringLiteral("defaults")].toBool() ? run({"--status", "--defaults", "--list", "--modules"}, "query") : run({"--status", "--list"}, "query");
0060 
0061     if (args[QStringLiteral("profiles")].toBool()) {
0062         QDir dir(KCM_UFW_DIR);
0063         const QStringList profiles = dir.entryList({"*" + PROFILE_EXTENSION});
0064         QMap<QString, QVariant> data;
0065         for (const QString &profile : profiles) {
0066             QFile f(dir.canonicalPath() + QChar('/') + profile);
0067             if (f.open(QIODevice::ReadOnly)) {
0068                 data.insert(profile, f.readAll());
0069             }
0070         }
0071         reply.addData(QStringLiteral("profiles"), data);
0072     }
0073 
0074     return reply;
0075 }
0076 
0077 QStringList getLogFromSystemd(const QString &lastLine)
0078 {
0079     QString program = QStringLiteral("journalctl");
0080     QStringList arguments{"-xb", "-n", "100", "-g", "UFW"};
0081 
0082     QProcess myProcess;
0083     myProcess.start(program, arguments);
0084     myProcess.waitForFinished();
0085 
0086     auto resultString = QString(myProcess.readAllStandardOutput());
0087     const auto resultList = resultString.split(QStringLiteral("\n"));
0088 
0089     // Example Line from Systemd:
0090     // Dec 06 17:42:45 tomatoland kernel: [UFW BLOCK] IN=wlan0 OUT= MAC= SRC=192.168.50.181 DST=224.0.0.252 LEN=56 TOS=0x00
0091     //     PREC=0x00 TTL=255 ID=52151 PROTO=UDP SPT=5355 DPT=5355 LEN=36
0092     // We need to remove everything up to the space after ']'.
0093 
0094     QStringList result;
0095     for (const QString &line : resultList) {
0096         if (!lastLine.isEmpty() && line == lastLine) {
0097             result.clear();
0098             continue;
0099         }
0100         result.append(line);
0101     }
0102     return result;
0103 }
0104 
0105 ActionReply Helper::queryapps(const QVariantMap &args)
0106 {
0107     Q_UNUSED(args);
0108     QProcess ufw;
0109     ActionReply reply;
0110 
0111     const QString ufwexe = QStandardPaths::findExecutable("ufw", {"/usr/sbin", "/usr/bin", "/sbin", "/bin"});
0112 
0113     if (ufwexe.isEmpty()) {
0114         qDebug() << "Executable not found: ufw";
0115         return reply;
0116     }
0117 
0118     ufw.start(ufwexe, {"app", "list"}, QIODevice::ReadOnly);
0119     if (ufw.waitForStarted()) {
0120         ufw.waitForFinished();
0121     }
0122 
0123     auto result = QString::fromLocal8Bit(ufw.readAllStandardOutput()).split(QLatin1Char('\n'), Qt::SkipEmptyParts);
0124 
0125     // The first line of the array is "Available Applications:", remove that.
0126     if (result.count()) {
0127         result.removeAt(0);
0128     }
0129 
0130     for (auto &value : result) {
0131         value = value.trimmed();
0132     }
0133 
0134     reply.setData({{"response", result}});
0135     return reply;
0136 }
0137 
0138 ActionReply Helper::viewlog(const QVariantMap &args)
0139 {
0140     ActionReply reply;
0141     QString lastLine = args["lastLine"].toString();
0142 
0143     QStringList result = getLogFromSystemd(lastLine);
0144     reply.addData(QStringLiteral("lines"), result);
0145     return reply;
0146 }
0147 
0148 ActionReply Helper::modify(const QVariantMap &args)
0149 {
0150     QString cmd = args[QStringLiteral("cmd")].toString();
0151 
0152     return QStringLiteral("setStatus") == cmd    ? setStatus(args, cmd)
0153         : QStringLiteral("addRules") == cmd      ? addRules(args, cmd)
0154         : QStringLiteral("removeRule") == cmd    ? removeRule(args, cmd)
0155         : QStringLiteral("moveRule") == cmd      ? moveRule(args, cmd)
0156         : QStringLiteral("editRule") == cmd      ? editRule(args, cmd)
0157         : QStringLiteral("reset") == cmd         ? reset(cmd)
0158         : QStringLiteral("setDefaults") == cmd   ? setDefaults(args, cmd)
0159         : QStringLiteral("setModules") == cmd    ? setModules(args, cmd)
0160         : QStringLiteral("setProfile") == cmd    ? setProfile(args, cmd)
0161         : QStringLiteral("saveProfile") == cmd   ? saveProfile(args, cmd)
0162         : QStringLiteral("deleteProfile") == cmd ? deleteProfile(args, cmd)
0163                                                  : ActionReply::HelperErrorReply(STATUS_INVALID_CMD);
0164 }
0165 
0166 ActionReply Helper::setStatus(const QVariantMap &args, const QString &cmd)
0167 {
0168     const QString enabled = args[QStringLiteral("status")].toBool() ? "true" : "false";
0169 
0170     return run({"--setEnabled=" + enabled}, {"--status"}, cmd);
0171 }
0172 
0173 ActionReply Helper::setDefaults(const QVariantMap &args, const QString &cmd)
0174 {
0175     QStringList pquery({"--defaults"});
0176     if (args[QStringLiteral("ipv6")].toBool()) {
0177         pquery.append(QStringLiteral("--list"));
0178     }
0179 
0180     const QString defaults = args[QStringLiteral("xml")].toString();
0181 
0182     return run({"--setDefaults=" + defaults}, pquery, cmd);
0183 }
0184 
0185 ActionReply Helper::setModules(const QVariantMap &args, const QString &cmd)
0186 {
0187     return run({"--setModules=" + args["xml"].toString()}, {"--modules"}, cmd);
0188 }
0189 
0190 ActionReply Helper::setProfile(const QVariantMap &args, const QString &cmd)
0191 {
0192     QStringList cmdArgs;
0193 
0194     if (args.contains(QStringLiteral("ruleCount"))) {
0195         unsigned int count = args[QStringLiteral("ruleCount")].toUInt();
0196 
0197         cmdArgs.append(QStringLiteral("--clearRules"));
0198         for (unsigned int i = 0; i < count; ++i) {
0199             const QString argument = args["rule" + QString::number(i)].toString();
0200             cmdArgs.append("--add=" + argument);
0201         }
0202     }
0203 
0204     if (args.contains(QStringLiteral("defaults"))) {
0205         cmdArgs << "--setDefaults=" + args[QStringLiteral("defaults")].toString();
0206     }
0207     if (args.contains(QStringLiteral("modules"))) {
0208         cmdArgs << "--setModules=" + args[QStringLiteral("modules")].toString();
0209     }
0210 
0211     if (cmdArgs.isEmpty()) {
0212         auto action = ActionReply::HelperErrorReply(STATUS_INVALID_ARGUMENTS);
0213         action.setErrorDescription(i18n("Invalid arguments passed to the profile"));
0214         return action;
0215     }
0216 
0217     checkFolder();
0218     return run(cmdArgs, {"--status", "--defaults", "--list", "--modules"}, cmd);
0219 }
0220 
0221 ActionReply Helper::saveProfile(const QVariantMap &args, const QString &cmd)
0222 {
0223     QString name(args[QStringLiteral("name")].toString()), xml(args["xml"].toString());
0224     ActionReply reply;
0225     auto prepareData = [&] {
0226         reply.addData(QStringLiteral("cmd"), cmd);
0227         reply.addData(QStringLiteral("name"), name);
0228         reply.addData("profiles", QDir(KCM_UFW_DIR).entryList({"*" + PROFILE_EXTENSION}));
0229     };
0230 
0231     if (name.isEmpty() || xml.isEmpty()) {
0232         reply = ActionReply::HelperErrorReply(STATUS_INVALID_ARGUMENTS);
0233         prepareData();
0234         return reply;
0235     }
0236 
0237     checkFolder();
0238 
0239     QFile f(QString(KCM_UFW_DIR) + "/" + name + PROFILE_EXTENSION);
0240 
0241     if (!f.open(QIODevice::WriteOnly)) {
0242         reply = ActionReply::HelperErrorReply(STATUS_OPERATION_FAILED);
0243         reply.setErrorDescription(i18n("Error saving the profile."));
0244         prepareData();
0245         return reply;
0246     }
0247 
0248     QTextStream(&f) << xml;
0249     f.close();
0250     setPermissions(f.fileName(), FILE_PERMS);
0251     prepareData();
0252     return reply;
0253 }
0254 
0255 ActionReply Helper::deleteProfile(const QVariantMap &args, const QString &cmd)
0256 {
0257     QString name(args[QStringLiteral("name")].toString());
0258     ActionReply reply;
0259     auto prepareData = [&] {
0260         reply.addData(QStringLiteral("cmd"), cmd);
0261         reply.addData(QStringLiteral("name"), name);
0262         reply.addData(QStringLiteral("profiles"), QDir(KCM_UFW_DIR).entryList({"*" + PROFILE_EXTENSION}));
0263     };
0264 
0265     if (name.isEmpty()) {
0266         reply = ActionReply::HelperErrorReply(STATUS_INVALID_ARGUMENTS);
0267         reply.setErrorDescription(i18n("Invalid arguments passed to delete profile"));
0268         prepareData();
0269         return reply;
0270     }
0271 
0272     if (!QFile::remove(QString(KCM_UFW_DIR) + "/" + name + PROFILE_EXTENSION)) {
0273         reply = ActionReply::HelperErrorReply(STATUS_OPERATION_FAILED);
0274         reply.setErrorDescription(i18n("Could not remove the profile from disk."));
0275         prepareData();
0276         return reply;
0277     }
0278 
0279     prepareData();
0280     return reply;
0281 }
0282 
0283 ActionReply Helper::addRules(const QVariantMap &args, const QString &cmd)
0284 {
0285     int count = args[QStringLiteral("count")].toInt();
0286 
0287     if (count <= 0) {
0288         ActionReply reply = ActionReply::HelperErrorReply(STATUS_INVALID_ARGUMENTS);
0289         reply.setErrorDescription(i18n("Invalid argument passed to add Rules"));
0290         return reply;
0291     }
0292     QStringList cmdArgs;
0293 
0294     for (int i = 0; i < count; ++i) {
0295         cmdArgs << "--add=" + args["xml" + QString::number(i)].toString();
0296     }
0297     qDebug() << "Cmd args passed to ufw:" << cmdArgs;
0298 
0299     checkFolder();
0300     return run(cmdArgs, {"--list"}, cmd);
0301 }
0302 
0303 ActionReply Helper::removeRule(const QVariantMap &args, const QString &cmd)
0304 {
0305     checkFolder();
0306     return run({"--remove=" + args["index"].toString()}, {"--list"}, cmd);
0307 }
0308 
0309 ActionReply Helper::moveRule(const QVariantMap &args, const QString &cmd)
0310 {
0311     checkFolder();
0312     const QString from = QString::number(args[QStringLiteral("from")].toUInt());
0313     const QString to = QString::number(args[QStringLiteral("to")].toUInt());
0314 
0315     return run({"--move=" + from + ':' + to}, {"--list"}, cmd);
0316 }
0317 
0318 ActionReply Helper::editRule(const QVariantMap &args, const QString &cmd)
0319 {
0320     checkFolder();
0321 
0322     qDebug() << args[QStringLiteral("xml")].toString();
0323 
0324     return run({"--update=" + args["xml"].toString()}, {"--list"}, cmd);
0325 }
0326 
0327 ActionReply Helper::reset(const QString &cmd)
0328 {
0329     return run({"--reset"}, {"--status", "--defaults", "--list", "--modules"}, cmd);
0330 }
0331 
0332 ActionReply Helper::run(const QStringList &args, const QStringList &second, const QString &cmd)
0333 {
0334     ActionReply reply = run(args, cmd);
0335     if (reply.errorCode() == EXIT_SUCCESS) {
0336         reply = run(second, cmd);
0337     }
0338     return reply;
0339 }
0340 
0341 ActionReply Helper::run(const QStringList &args, const QString &cmd)
0342 {
0343     QProcess ufw;
0344     ActionReply reply;
0345     ufw.start(UFW_PLUGIN_HELPER_PATH, args, QIODevice::ReadOnly);
0346     if (ufw.waitForStarted()) {
0347         ufw.waitForFinished();
0348     }
0349 
0350     int exitCode(ufw.exitCode());
0351 
0352     if (exitCode != EXIT_SUCCESS) {
0353         QString errorString = ufw.readAllStandardError().simplified();
0354 
0355         const QString errorPrefix = QStringLiteral("ERROR: ");
0356         if (errorString.startsWith(errorPrefix)) {
0357             errorString = errorString.mid(errorPrefix.length());
0358         }
0359 
0360         reply = ActionReply::HelperErrorReply(exitCode);
0361         reply.setErrorDescription(i18n("An error occurred in command '%1': %2", cmd, errorString));
0362         reply.addData(QStringLiteral("cmd"), cmd);
0363         return reply;
0364     }
0365 
0366     // reply.addData(QStringLiteral("response"), ufw.readAllStandardOutput());
0367     QString output = ufw.readAllStandardOutput();
0368     qDebug() << "Command" << UFW_PLUGIN_HELPER_PATH << args << output;
0369 
0370     reply.addData("response", output);
0371     reply.addData(QStringLiteral("cmd"), cmd);
0372     return reply;
0373 }
0374 
0375 }
0376 
0377 KAUTH_HELPER_MAIN("org.kde.ufw", UFW::Helper)