File indexing completed on 2023-09-24 04:08:38
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 ¶meterName) 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"