File indexing completed on 2024-05-05 16:13:20

0001 /*
0002     This file is part of the KDE project
0003     SPDX-FileCopyrightText: 2004 Jan Schaefer <j_schaef@informatik.uni-kl.de>
0004     SPDX-FileCopyrightText: 2010 Rodrigo Belem <rclbelem@gmail.com>
0005     SPDX-FileCopyrightText: 2020 Harald Sitter <sitter@kde.org>
0006 
0007     SPDX-License-Identifier: LGPL-2.0-only
0008 */
0009 
0010 #include "ksambashare.h"
0011 #include "kiocoredebug.h"
0012 #include "ksambashare_p.h"
0013 #include "ksambasharedata.h"
0014 #include "ksambasharedata_p.h"
0015 
0016 #include "../utils_p.h"
0017 
0018 #include <QDebug>
0019 #include <QFile>
0020 #include <QFileInfo>
0021 #include <QHostInfo>
0022 #include <QLoggingCategory>
0023 #include <QMap>
0024 #include <QProcess>
0025 #include <QRegularExpression>
0026 #include <QStandardPaths>
0027 #include <QStringList>
0028 #include <QTextStream>
0029 
0030 #include <KDirWatch>
0031 #include <KUser>
0032 
0033 Q_DECLARE_LOGGING_CATEGORY(KIO_CORE_SAMBASHARE)
0034 Q_LOGGING_CATEGORY(KIO_CORE_SAMBASHARE, "kf.kio.core.sambashare", QtWarningMsg)
0035 
0036 KSambaSharePrivate::KSambaSharePrivate(KSambaShare *parent)
0037     : q_ptr(parent)
0038     , data()
0039     , userSharePath()
0040     , skipUserShare(false)
0041 {
0042     setUserSharePath();
0043 #if KIOCORE_BUILD_DEPRECATED_SINCE(4, 6)
0044     findSmbConf();
0045 #endif
0046     data = parse(getNetUserShareInfo());
0047 }
0048 
0049 KSambaSharePrivate::~KSambaSharePrivate()
0050 {
0051 }
0052 
0053 bool KSambaSharePrivate::isSambaInstalled()
0054 {
0055     const bool daemonExists =
0056         !QStandardPaths::findExecutable(QStringLiteral("smbd"), {QStringLiteral("/usr/sbin/"), QStringLiteral("/usr/local/sbin/")}).isEmpty();
0057     if (!daemonExists) {
0058         qCDebug(KIO_CORE_SAMBASHARE) << "KSambaShare: Could not find smbd";
0059     }
0060 
0061     const bool clientExists = !QStandardPaths::findExecutable(QStringLiteral("testparm")).isEmpty();
0062     if (!clientExists) {
0063         qCDebug(KIO_CORE_SAMBASHARE) << "KSambaShare: Could not find testparm tool, most likely samba-client isn't installed";
0064     }
0065 
0066     return daemonExists && clientExists;
0067 }
0068 
0069 #if KIOCORE_BUILD_DEPRECATED_SINCE(4, 6)
0070 // Default smb.conf locations
0071 // sorted by priority, most priority first
0072 static const char *const DefaultSambaConfigFilePathList[] = {"/etc/samba/smb.conf",
0073                                                              "/etc/smb.conf",
0074                                                              "/usr/local/etc/smb.conf",
0075                                                              "/usr/local/samba/lib/smb.conf",
0076                                                              "/usr/samba/lib/smb.conf",
0077                                                              "/usr/lib/smb.conf",
0078                                                              "/usr/local/lib/smb.conf"};
0079 
0080 // Try to find the samba config file path
0081 // in several well-known paths
0082 bool KSambaSharePrivate::findSmbConf()
0083 {
0084     for (const char *str : DefaultSambaConfigFilePathList) {
0085         const QString filePath = QString::fromLatin1(str);
0086         if (QFile::exists(filePath)) {
0087             smbConf = filePath;
0088             return true;
0089         }
0090     }
0091 
0092     qCDebug(KIO_CORE_SAMBASHARE) << "KSambaShare: Could not find smb.conf!";
0093 
0094     return false;
0095 }
0096 #endif
0097 
0098 void KSambaSharePrivate::setUserSharePath()
0099 {
0100     const QString rawString = testparmParamValue(QStringLiteral("usershare path"));
0101     const QFileInfo fileInfo(rawString);
0102     if (fileInfo.isDir()) {
0103         userSharePath = rawString;
0104     }
0105 }
0106 
0107 int KSambaSharePrivate::runProcess(const QString &progName, const QStringList &args, QByteArray &stdOut, QByteArray &stdErr)
0108 {
0109     QProcess process;
0110 
0111     process.setProcessChannelMode(QProcess::SeparateChannels);
0112     const QString exec = QStandardPaths::findExecutable(progName);
0113     if (exec.isEmpty()) {
0114         qCWarning(KIO_CORE) << "Could not find an executable named:" << progName;
0115         return -1;
0116     }
0117 
0118     process.start(exec, args);
0119     // TODO: make it async in future
0120     process.waitForFinished();
0121 
0122     stdOut = process.readAllStandardOutput();
0123     stdErr = process.readAllStandardError();
0124     return process.exitCode();
0125 }
0126 
0127 QString KSambaSharePrivate::testparmParamValue(const QString &parameterName)
0128 {
0129     if (!isSambaInstalled()) {
0130         return QString();
0131     }
0132 
0133     QByteArray stdErr;
0134     QByteArray stdOut;
0135 
0136     const QStringList args{
0137         QStringLiteral("-d0"),
0138         QStringLiteral("-s"),
0139         QStringLiteral("--parameter-name"),
0140         parameterName,
0141     };
0142 
0143     runProcess(QStringLiteral("testparm"), args, stdOut, stdErr);
0144 
0145     // TODO: parse and process error messages.
0146     // create a parser for the error output and
0147     // send error message somewhere
0148     if (!stdErr.isEmpty()) {
0149         QList<QByteArray> errArray = stdErr.trimmed().split('\n');
0150         errArray.removeAll("\n");
0151         errArray.erase(std::remove_if(errArray.begin(),
0152                                       errArray.end(),
0153                                       [](QByteArray &line) {
0154                                           return line.startsWith("Load smb config files from");
0155                                       }),
0156                        errArray.end());
0157         errArray.removeOne("Loaded services file OK.");
0158         errArray.removeOne("Weak crypto is allowed");
0159 
0160         const int netbiosNameErrorIdx = errArray.indexOf("WARNING: The 'netbios name' is too long (max. 15 chars).");
0161         if (netbiosNameErrorIdx >= 0) {
0162             // netbios name must be of at most 15 characters long
0163             // means either netbios name is badly configured
0164             // or not set and the default value is being used, it being "$(hostname)-W"
0165             // which means any hostname longer than 13 characters will cause this warning
0166             // when no netbios name was defined
0167             // See https://www.novell.com/documentation/open-enterprise-server-2018/file_samba_cifs_lx/data/bc855e3.html
0168             const QString defaultNetbiosName = QHostInfo::localHostName().append(QStringLiteral("-W"));
0169             if (defaultNetbiosName.length() > 14) {
0170                 qCDebug(KIO_CORE) << "Your samba 'netbios name' parameter was longer than the authorized 15 characters.\n"
0171                                   << "It may be because your hostname is longer than 13 and samba default 'netbios name' defaults to 'hostname-W', here:"
0172                                   << defaultNetbiosName << "\n"
0173                                   << "If that it is the case simply define a 'netbios name' parameter in /etc/samba/smb.conf at most 15 characters long";
0174             } else {
0175                 qCDebug(KIO_CORE) << "Your samba 'netbios name' parameter was longer than the authorized 15 characters."
0176                                   << "Please define a 'netbios name' parameter in /etc/samba/smb.conf at most 15 characters long";
0177             }
0178             errArray.removeAt(netbiosNameErrorIdx);
0179         }
0180         if (errArray.size() > 0) {
0181             qCDebug(KIO_CORE) << "We got some errors while running testparm" << errArray.join("\n");
0182         }
0183     }
0184 
0185     if (!stdOut.isEmpty()) {
0186         return QString::fromLocal8Bit(stdOut.trimmed());
0187     }
0188 
0189     return QString();
0190 }
0191 
0192 QByteArray KSambaSharePrivate::getNetUserShareInfo()
0193 {
0194     if (skipUserShare || !isSambaInstalled()) {
0195         return QByteArray();
0196     }
0197 
0198     QByteArray stdOut;
0199     QByteArray stdErr;
0200 
0201     const QStringList args{
0202         QStringLiteral("usershare"),
0203         QStringLiteral("info"),
0204     };
0205 
0206     runProcess(QStringLiteral("net"), args, stdOut, stdErr);
0207 
0208     if (!stdErr.isEmpty()) {
0209         if (stdErr.contains("You do not have permission to create a usershare")) {
0210             skipUserShare = true;
0211         } else if (stdErr.contains("usershares are currently disabled")) {
0212             skipUserShare = true;
0213         } else {
0214             // TODO: parse and process other error messages.
0215             // create a parser for the error output and
0216             // send error message somewhere
0217             qCDebug(KIO_CORE) << "We got some errors while running 'net usershare info'";
0218             qCDebug(KIO_CORE) << stdErr;
0219         }
0220     }
0221 
0222     return stdOut;
0223 }
0224 
0225 QStringList KSambaSharePrivate::shareNames() const
0226 {
0227     return data.keys();
0228 }
0229 
0230 QStringList KSambaSharePrivate::sharedDirs() const
0231 {
0232     QStringList dirs;
0233 
0234     QMap<QString, KSambaShareData>::ConstIterator i;
0235     for (i = data.constBegin(); i != data.constEnd(); ++i) {
0236         if (!dirs.contains(i.value().path())) {
0237             dirs << i.value().path();
0238         }
0239     }
0240 
0241     return dirs;
0242 }
0243 
0244 KSambaShareData KSambaSharePrivate::getShareByName(const QString &shareName) const
0245 {
0246     return data.value(shareName);
0247 }
0248 
0249 QList<KSambaShareData> KSambaSharePrivate::getSharesByPath(const QString &path) const
0250 {
0251     QList<KSambaShareData> shares;
0252 
0253     QMap<QString, KSambaShareData>::ConstIterator i;
0254     for (i = data.constBegin(); i != data.constEnd(); ++i) {
0255         if (i.value().path() == path) {
0256             shares << i.value();
0257         }
0258     }
0259 
0260     return shares;
0261 }
0262 
0263 bool KSambaSharePrivate::isShareNameValid(const QString &name) const
0264 {
0265     // Samba forbidden chars
0266     const QRegularExpression notToMatchRx(QStringLiteral("[%<>*\?|/+=;:\",]"));
0267     return !notToMatchRx.match(name).hasMatch();
0268 }
0269 
0270 bool KSambaSharePrivate::isDirectoryShared(const QString &path) const
0271 {
0272     QMap<QString, KSambaShareData>::ConstIterator i;
0273     for (i = data.constBegin(); i != data.constEnd(); ++i) {
0274         if (i.value().path() == path) {
0275             return true;
0276         }
0277     }
0278 
0279     return false;
0280 }
0281 
0282 bool KSambaSharePrivate::isShareNameAvailable(const QString &name) const
0283 {
0284     // Samba does not allow to name a share with a user name registered in the system
0285     return (!KUser::allUserNames().contains(name) && !data.contains(name));
0286 }
0287 
0288 KSambaShareData::UserShareError KSambaSharePrivate::isPathValid(const QString &path) const
0289 {
0290     QFileInfo pathInfo(path);
0291 
0292     if (!pathInfo.exists()) {
0293         return KSambaShareData::UserSharePathNotExists;
0294     }
0295 
0296     if (!pathInfo.isDir()) {
0297         return KSambaShareData::UserSharePathNotDirectory;
0298     }
0299 
0300     if (pathInfo.isRelative()) {
0301         if (pathInfo.makeAbsolute()) {
0302             return KSambaShareData::UserSharePathNotAbsolute;
0303         }
0304     }
0305 
0306     // TODO: check if the user is root
0307     if (KSambaSharePrivate::testparmParamValue(QStringLiteral("usershare owner only")) == QLatin1String("Yes")) {
0308         if (!pathInfo.permission(QFile::ReadUser | QFile::WriteUser)) {
0309             return KSambaShareData::UserSharePathNotAllowed;
0310         }
0311     }
0312 
0313     return KSambaShareData::UserSharePathOk;
0314 }
0315 
0316 KSambaShareData::UserShareError KSambaSharePrivate::isAclValid(const QString &acl) const
0317 {
0318     // NOTE: capital D is not missing from the regex net usershare will in fact refuse to consider it valid
0319     //   - verified 2020-08-20
0320     static const auto pattern = uR"--((?:(?:(\w(\w|\s)*)\\|)(\w+\s*):([fFrRd]{1})(?:,|))*)--";
0321     static const QRegularExpression aclRx(QRegularExpression::anchoredPattern(pattern));
0322     // TODO: check if user is a valid smb user
0323     return aclRx.match(acl).hasMatch() ? KSambaShareData::UserShareAclOk : KSambaShareData::UserShareAclInvalid;
0324 }
0325 
0326 bool KSambaSharePrivate::areGuestsAllowed() const
0327 {
0328     return KSambaSharePrivate::testparmParamValue(QStringLiteral("usershare allow guests")) != QLatin1String("No");
0329 }
0330 
0331 KSambaShareData::UserShareError KSambaSharePrivate::guestsAllowed(const KSambaShareData::GuestPermission &guestok) const
0332 {
0333     if (guestok == KSambaShareData::GuestsAllowed && !areGuestsAllowed()) {
0334         return KSambaShareData::UserShareGuestsNotAllowed;
0335     }
0336 
0337     return KSambaShareData::UserShareGuestsOk;
0338 }
0339 
0340 KSambaShareData::UserShareError KSambaSharePrivate::add(const KSambaShareData &shareData)
0341 {
0342     // TODO:
0343     // * check for usershare max shares
0344 
0345     if (!isSambaInstalled()) {
0346         return KSambaShareData::UserShareSystemError;
0347     }
0348 
0349     if (data.contains(shareData.name())) {
0350         if (data.value(shareData.name()).path() != shareData.path()) {
0351             return KSambaShareData::UserShareNameInUse;
0352         }
0353     }
0354 
0355     QString guestok =
0356         QStringLiteral("guest_ok=%1").arg((shareData.guestPermission() == KSambaShareData::GuestsNotAllowed) ? QStringLiteral("n") : QStringLiteral("y"));
0357 
0358     const QStringList args{
0359         QStringLiteral("usershare"),
0360         QStringLiteral("add"),
0361         shareData.name(),
0362         shareData.path(),
0363         shareData.comment(),
0364         shareData.acl(),
0365         guestok,
0366     };
0367 
0368     QByteArray stdOut;
0369     int ret = runProcess(QStringLiteral("net"), args, stdOut, m_stdErr);
0370 
0371     // TODO: parse and process error messages.
0372     if (!m_stdErr.isEmpty()) {
0373         // create a parser for the error output and
0374         // send error message somewhere
0375         qCWarning(KIO_CORE) << "We got some errors while running 'net usershare add'" << args;
0376         qCWarning(KIO_CORE) << m_stdErr;
0377     }
0378 
0379     if (ret == 0 && !data.contains(shareData.name())) {
0380         // It needs to be added in this function explicitly, otherwise another instance of
0381         // KSambaShareDataPrivate will be created and added to data when the share
0382         // definition changes on-disk and we re-parse the data.
0383         data.insert(shareData.name(), shareData);
0384     }
0385 
0386     return (ret == 0) ? KSambaShareData::UserShareOk : KSambaShareData::UserShareSystemError;
0387 }
0388 
0389 KSambaShareData::UserShareError KSambaSharePrivate::remove(const KSambaShareData &shareData)
0390 {
0391     if (!isSambaInstalled()) {
0392         return KSambaShareData::UserShareSystemError;
0393     }
0394 
0395     if (!data.contains(shareData.name())) {
0396         return KSambaShareData::UserShareNameInvalid;
0397     }
0398 
0399     const QStringList args{
0400         QStringLiteral("usershare"),
0401         QStringLiteral("delete"),
0402         shareData.name(),
0403     };
0404 
0405     QByteArray stdOut;
0406     int ret = runProcess(QStringLiteral("net"), args, stdOut, m_stdErr);
0407 
0408     // TODO: parse and process error messages.
0409     if (!m_stdErr.isEmpty()) {
0410         // create a parser for the error output and
0411         // send error message somewhere
0412         qCWarning(KIO_CORE) << "We got some errors while running 'net usershare delete'" << args;
0413         qCWarning(KIO_CORE) << m_stdErr;
0414     }
0415 
0416     return (ret == 0) ? KSambaShareData::UserShareOk : KSambaShareData::UserShareSystemError;
0417 
0418     // NB: the share file gets deleted which leads us to reload and drop the ShareData, hence no explicit remove
0419 }
0420 
0421 QMap<QString, KSambaShareData> KSambaSharePrivate::parse(const QByteArray &usershareData)
0422 {
0423     static const char16_t headerPattern[] = uR"--(^\s*\[([^%<>*?|/+=;:",]+)\])--";
0424     static const QRegularExpression headerRx(QRegularExpression::anchoredPattern(headerPattern));
0425 
0426     static const char16_t valPattern[] = uR"--(^\s*([\w\d\s]+)=(.*)$)--";
0427     static const QRegularExpression OptValRx(QRegularExpression::anchoredPattern(valPattern));
0428 
0429     QTextStream stream(usershareData);
0430     QString currentShare;
0431     QMap<QString, KSambaShareData> shares;
0432 
0433     while (!stream.atEnd()) {
0434         const QString line = stream.readLine().trimmed();
0435 
0436         QRegularExpressionMatch match;
0437         if ((match = headerRx.match(line)).hasMatch()) {
0438             currentShare = match.captured(1).trimmed();
0439 
0440             if (!shares.contains(currentShare)) {
0441                 KSambaShareData shareData;
0442                 shareData.dd->name = currentShare;
0443                 shares.insert(currentShare, shareData);
0444             }
0445         } else if ((match = OptValRx.match(line)).hasMatch()) {
0446             const QString key = match.captured(1).trimmed();
0447             const QString value = match.captured(2).trimmed();
0448             KSambaShareData shareData = shares[currentShare];
0449 
0450             if (key == QLatin1String("path")) {
0451                 // Samba accepts paths with and w/o trailing slash, we
0452                 // use and expect path without slash
0453                 shareData.dd->path = Utils::trailingSlashRemoved(value);
0454             } else if (key == QLatin1String("comment")) {
0455                 shareData.dd->comment = value;
0456             } else if (key == QLatin1String("usershare_acl")) {
0457                 shareData.dd->acl = value;
0458             } else if (key == QLatin1String("guest_ok")) {
0459                 shareData.dd->guestPermission = value;
0460             } else {
0461                 qCWarning(KIO_CORE) << "Something nasty happen while parsing 'net usershare info'"
0462                                     << "share:" << currentShare << "key:" << key;
0463             }
0464         } else if (line.trimmed().isEmpty()) {
0465             continue;
0466         } else {
0467             return shares;
0468         }
0469     }
0470 
0471     return shares;
0472 }
0473 
0474 void KSambaSharePrivate::slotFileChange(const QString &path)
0475 {
0476     if (path != userSharePath) {
0477         return;
0478     }
0479     data = parse(getNetUserShareInfo());
0480     qCDebug(KIO_CORE) << "reloading data; path changed:" << path;
0481     Q_Q(KSambaShare);
0482     Q_EMIT q->changed();
0483 }
0484 
0485 KSambaShare::KSambaShare()
0486     : QObject(nullptr)
0487     , d_ptr(new KSambaSharePrivate(this))
0488 {
0489     Q_D(KSambaShare);
0490     if (!d->userSharePath.isEmpty() && QFileInfo::exists(d->userSharePath)) {
0491         KDirWatch::self()->addDir(d->userSharePath, KDirWatch::WatchFiles);
0492         connect(KDirWatch::self(), &KDirWatch::dirty, this, [d](const QString &path) {
0493             d->slotFileChange(path);
0494         });
0495     }
0496 }
0497 
0498 KSambaShare::~KSambaShare()
0499 {
0500     Q_D(const KSambaShare);
0501     if (KDirWatch::exists() && KDirWatch::self()->contains(d->userSharePath)) {
0502         KDirWatch::self()->removeDir(d->userSharePath);
0503     }
0504     delete d_ptr;
0505 }
0506 
0507 #if KIOCORE_BUILD_DEPRECATED_SINCE(4, 6)
0508 QString KSambaShare::smbConfPath() const
0509 {
0510     Q_D(const KSambaShare);
0511     return d->smbConf;
0512 }
0513 #endif
0514 
0515 bool KSambaShare::isDirectoryShared(const QString &path) const
0516 {
0517     Q_D(const KSambaShare);
0518     return d->isDirectoryShared(path);
0519 }
0520 
0521 bool KSambaShare::isShareNameAvailable(const QString &name) const
0522 {
0523     Q_D(const KSambaShare);
0524     return d->isShareNameValid(name) && d->isShareNameAvailable(name);
0525 }
0526 
0527 QStringList KSambaShare::shareNames() const
0528 {
0529     Q_D(const KSambaShare);
0530     return d->shareNames();
0531 }
0532 
0533 QStringList KSambaShare::sharedDirectories() const
0534 {
0535     Q_D(const KSambaShare);
0536     return d->sharedDirs();
0537 }
0538 
0539 KSambaShareData KSambaShare::getShareByName(const QString &name) const
0540 {
0541     Q_D(const KSambaShare);
0542     return d->getShareByName(name);
0543 }
0544 
0545 QList<KSambaShareData> KSambaShare::getSharesByPath(const QString &path) const
0546 {
0547     Q_D(const KSambaShare);
0548     return d->getSharesByPath(path);
0549 }
0550 
0551 QString KSambaShare::lastSystemErrorString() const
0552 {
0553     Q_D(const KSambaShare);
0554     return QString::fromUtf8(d->m_stdErr);
0555 }
0556 
0557 bool KSambaShare::areGuestsAllowed() const
0558 {
0559     Q_D(const KSambaShare);
0560     return d->areGuestsAllowed();
0561 }
0562 
0563 class KSambaShareSingleton
0564 {
0565 public:
0566     KSambaShare instance;
0567 };
0568 
0569 Q_GLOBAL_STATIC(KSambaShareSingleton, _instance)
0570 
0571 KSambaShare *KSambaShare::instance()
0572 {
0573     return &_instance()->instance;
0574 }
0575 
0576 #include "moc_ksambashare.cpp"