File indexing completed on 2024-04-21 03:55:09

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