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 ¶meterName) 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"