KIO

ksambashare.cpp
1 /*
2  This file is part of the KDE project
3  SPDX-FileCopyrightText: 2004 Jan Schaefer <[email protected]>
4  SPDX-FileCopyrightText: 2010 Rodrigo Belem <[email protected]>
5  SPDX-FileCopyrightText: 2020 Harald Sitter <[email protected]>
6 
7  SPDX-License-Identifier: LGPL-2.0-only
8 */
9 
10 #include "ksambashare.h"
11 #include "kiocoredebug.h"
12 #include "ksambashare_p.h"
13 #include "ksambasharedata.h"
14 #include "ksambasharedata_p.h"
15 
16 #include <QDebug>
17 #include <QFile>
18 #include <QFileInfo>
19 #include <QHostInfo>
20 #include <QLoggingCategory>
21 #include <QMap>
22 #include <QProcess>
23 #include <QRegularExpression>
24 #include <QStandardPaths>
25 #include <QStringList>
26 #include <QTextStream>
27 
28 #include <KDirWatch>
29 #include <KUser>
30 
31 Q_DECLARE_LOGGING_CATEGORY(KIO_CORE_SAMBASHARE)
32 Q_LOGGING_CATEGORY(KIO_CORE_SAMBASHARE, "kf.kio.core.sambashare", QtWarningMsg)
33 
34 // Default smb.conf locations
35 // sorted by priority, most priority first
36 static const char *const DefaultSambaConfigFilePathList[] = {"/etc/samba/smb.conf",
37  "/etc/smb.conf",
38  "/usr/local/etc/smb.conf",
39  "/usr/local/samba/lib/smb.conf",
40  "/usr/samba/lib/smb.conf",
41  "/usr/lib/smb.conf",
42  "/usr/local/lib/smb.conf"};
43 
44 KSambaSharePrivate::KSambaSharePrivate(KSambaShare *parent)
45  : q_ptr(parent)
46  , data()
47  , userSharePath()
48  , skipUserShare(false)
49 {
50  setUserSharePath();
51 #if KIOCORE_BUILD_DEPRECATED_SINCE(4, 6)
52  findSmbConf();
53 #endif
54  data = parse(getNetUserShareInfo());
55 }
56 
57 KSambaSharePrivate::~KSambaSharePrivate()
58 {
59 }
60 
61 bool KSambaSharePrivate::isSambaInstalled()
62 {
63  const bool daemonExists =
64  !QStandardPaths::findExecutable(QStringLiteral("smbd"), {QStringLiteral("/usr/sbin/"), QStringLiteral("/usr/local/sbin/")}).isEmpty();
65  if (!daemonExists) {
66  qCDebug(KIO_CORE_SAMBASHARE) << "KSambaShare: Could not find smbd";
67  }
68 
69  const bool clientExists = !QStandardPaths::findExecutable(QStringLiteral("testparm")).isEmpty();
70  if (!clientExists) {
71  qCDebug(KIO_CORE_SAMBASHARE) << "KSambaShare: Could not find testparm tool, most likely samba-client isn't installed";
72  }
73 
74  return daemonExists && clientExists;
75 }
76 
77 #if KIOCORE_BUILD_DEPRECATED_SINCE(4, 6)
78 // Try to find the samba config file path
79 // in several well-known paths
80 bool KSambaSharePrivate::findSmbConf()
81 {
82  for (const char *str : DefaultSambaConfigFilePathList) {
83  const QString filePath = QString::fromLatin1(str);
84  if (QFile::exists(filePath)) {
85  smbConf = filePath;
86  return true;
87  }
88  }
89 
90  qCDebug(KIO_CORE_SAMBASHARE) << "KSambaShare: Could not find smb.conf!";
91 
92  return false;
93 }
94 #endif
95 
96 void KSambaSharePrivate::setUserSharePath()
97 {
98  const QString rawString = testparmParamValue(QStringLiteral("usershare path"));
99  const QFileInfo fileInfo(rawString);
100  if (fileInfo.isDir()) {
101  userSharePath = rawString;
102  }
103 }
104 
105 int KSambaSharePrivate::runProcess(const QString &progName, const QStringList &args, QByteArray &stdOut, QByteArray &stdErr)
106 {
107  QProcess process;
108 
110  process.start(progName, args);
111  // TODO: make it async in future
112  process.waitForFinished();
113 
114  stdOut = process.readAllStandardOutput();
115  stdErr = process.readAllStandardError();
116  return process.exitCode();
117 }
118 
119 QString KSambaSharePrivate::testparmParamValue(const QString &parameterName)
120 {
121  if (!isSambaInstalled()) {
122  return QString();
123  }
124 
125  QByteArray stdErr;
126  QByteArray stdOut;
127 
128  const QStringList args{
129  QStringLiteral("-d0"),
130  QStringLiteral("-s"),
131  QStringLiteral("--parameter-name"),
132  parameterName,
133  };
134 
135  runProcess(QStringLiteral("testparm"), args, stdOut, stdErr);
136 
137  // TODO: parse and process error messages.
138  // create a parser for the error output and
139  // send error message somewhere
140  if (!stdErr.isEmpty()) {
141  QList<QByteArray> err;
142  err << stdErr.trimmed().split('\n');
143  // ignore first two lines
144  if (err.count() > 0 && err.at(0).startsWith("Load smb config files from")) {
145  err.removeFirst();
146  }
147  if (err.count() > 0 && err.at(0).startsWith("Loaded services file OK.")) {
148  err.removeFirst();
149  }
150  if (err.count() > 0 && err.at(0).startsWith("WARNING: The 'netbios name' is too long (max. 15 chars).")) {
151  // netbios name must be of at most 15 characters long
152  // means either netbios name is badly configured
153  // or not set and the default value is being used, it being "$(hostname)-W"
154  // which means any hostname longer than 13 characters will cause this warning
155  // when no netbios name was defined
156  // See https://www.novell.com/documentation/open-enterprise-server-2018/file_samba_cifs_lx/data/bc855e3.html
157  const QString defaultNetbiosName = QHostInfo::localHostName().append(QStringLiteral("-W"));
158  if (defaultNetbiosName.length() > 14) {
159  qCDebug(KIO_CORE) << "Your samba 'netbios name' parameter was longer than the authorized 15 characters.\n"
160  << "It may be because your hostname is longer than 13 and samba default 'netbios name' defaults to 'hostname-W', here:"
161  << defaultNetbiosName << "\n"
162  << "If that it is the case simply define a 'netbios name' parameter in /etc/samba/smb.conf at most 15 characters long";
163  } else {
164  qCDebug(KIO_CORE) << "Your samba 'netbios name' parameter was longer than the authorized 15 characters."
165  << "Please define a 'netbios name' parameter in /etc/samba/smb.conf at most 15 characters long";
166  }
167  err.removeFirst();
168  }
169  if (err.count() > 0) {
170  qCDebug(KIO_CORE) << "We got some errors while running testparm" << err.join("\n");
171  }
172  }
173 
174  if (!stdOut.isEmpty()) {
175  return QString::fromLocal8Bit(stdOut.trimmed());
176  }
177 
178  return QString();
179 }
180 
181 QByteArray KSambaSharePrivate::getNetUserShareInfo()
182 {
183  if (skipUserShare || !isSambaInstalled()) {
184  return QByteArray();
185  }
186 
187  QByteArray stdOut;
188  QByteArray stdErr;
189 
190  const QStringList args{
191  QStringLiteral("usershare"),
192  QStringLiteral("info"),
193  };
194 
195  runProcess(QStringLiteral("net"), args, stdOut, stdErr);
196 
197  if (!stdErr.isEmpty()) {
198  if (stdErr.contains("You do not have permission to create a usershare")) {
199  skipUserShare = true;
200  } else if (stdErr.contains("usershares are currently disabled")) {
201  skipUserShare = true;
202  } else {
203  // TODO: parse and process other error messages.
204  // create a parser for the error output and
205  // send error message somewhere
206  qCDebug(KIO_CORE) << "We got some errors while running 'net usershare info'";
207  qCDebug(KIO_CORE) << stdErr;
208  }
209  }
210 
211  return stdOut;
212 }
213 
214 QStringList KSambaSharePrivate::shareNames() const
215 {
216  return data.keys();
217 }
218 
219 QStringList KSambaSharePrivate::sharedDirs() const
220 {
222 
224  for (i = data.constBegin(); i != data.constEnd(); ++i) {
225  if (!dirs.contains(i.value().path())) {
226  dirs << i.value().path();
227  }
228  }
229 
230  return dirs;
231 }
232 
233 KSambaShareData KSambaSharePrivate::getShareByName(const QString &shareName) const
234 {
235  return data.value(shareName);
236 }
237 
238 QList<KSambaShareData> KSambaSharePrivate::getSharesByPath(const QString &path) const
239 {
240  QList<KSambaShareData> shares;
241 
243  for (i = data.constBegin(); i != data.constEnd(); ++i) {
244  if (i.value().path() == path) {
245  shares << i.value();
246  }
247  }
248 
249  return shares;
250 }
251 
252 bool KSambaSharePrivate::isShareNameValid(const QString &name) const
253 {
254  // Samba forbidden chars
255  const QRegularExpression notToMatchRx(QStringLiteral("[%<>*\?|/+=;:\",]"));
256  return !notToMatchRx.match(name).hasMatch();
257 }
258 
259 bool KSambaSharePrivate::isDirectoryShared(const QString &path) const
260 {
262  for (i = data.constBegin(); i != data.constEnd(); ++i) {
263  if (i.value().path() == path) {
264  return true;
265  }
266  }
267 
268  return false;
269 }
270 
271 bool KSambaSharePrivate::isShareNameAvailable(const QString &name) const
272 {
273  // Samba does not allow to name a share with a user name registered in the system
274  return (!KUser::allUserNames().contains(name) && !data.contains(name));
275 }
276 
277 KSambaShareData::UserShareError KSambaSharePrivate::isPathValid(const QString &path) const
278 {
279  QFileInfo pathInfo(path);
280 
281  if (!pathInfo.exists()) {
282  return KSambaShareData::UserSharePathNotExists;
283  }
284 
285  if (!pathInfo.isDir()) {
286  return KSambaShareData::UserSharePathNotDirectory;
287  }
288 
289  if (pathInfo.isRelative()) {
290  if (pathInfo.makeAbsolute()) {
291  return KSambaShareData::UserSharePathNotAbsolute;
292  }
293  }
294 
295  // TODO: check if the user is root
296  if (KSambaSharePrivate::testparmParamValue(QStringLiteral("usershare owner only")) == QLatin1String("Yes")) {
297  if (!pathInfo.permission(QFile::ReadUser | QFile::WriteUser)) {
298  return KSambaShareData::UserSharePathNotAllowed;
299  }
300  }
301 
302  return KSambaShareData::UserSharePathOk;
303 }
304 
305 KSambaShareData::UserShareError KSambaSharePrivate::isAclValid(const QString &acl) const
306 {
307  // NOTE: capital D is not missing from the regex net usershare will in fact refuse to consider it valid
308  // - verified 2020-08-20
309  const QRegularExpression aclRx(QRegularExpression::anchoredPattern(QStringLiteral("(?:(?:(\\w(\\w|\\s)*)\\\\|)(\\w+\\s*):([fFrRd]{1})(?:,|))*")));
310  // TODO: check if user is a valid smb user
311  return aclRx.match(acl).hasMatch() ? KSambaShareData::UserShareAclOk : KSambaShareData::UserShareAclInvalid;
312 }
313 
314 bool KSambaSharePrivate::areGuestsAllowed() const
315 {
316  return KSambaSharePrivate::testparmParamValue(QStringLiteral("usershare allow guests")) != QLatin1String("No");
317 }
318 
319 KSambaShareData::UserShareError KSambaSharePrivate::guestsAllowed(const KSambaShareData::GuestPermission &guestok) const
320 {
321  if (guestok == KSambaShareData::GuestsAllowed && !areGuestsAllowed()) {
322  return KSambaShareData::UserShareGuestsNotAllowed;
323  }
324 
325  return KSambaShareData::UserShareGuestsOk;
326 }
327 
328 KSambaShareData::UserShareError KSambaSharePrivate::add(const KSambaShareData &shareData)
329 {
330  // TODO:
331  // * check for usershare max shares
332 
333  if (!isSambaInstalled()) {
334  return KSambaShareData::UserShareSystemError;
335  }
336 
337  if (data.contains(shareData.name())) {
338  if (data.value(shareData.name()).path() != shareData.path()) {
339  return KSambaShareData::UserShareNameInUse;
340  }
341  }
342 
343  QString guestok =
344  QStringLiteral("guest_ok=%1").arg((shareData.guestPermission() == KSambaShareData::GuestsNotAllowed) ? QStringLiteral("n") : QStringLiteral("y"));
345 
346  const QStringList args{
347  QStringLiteral("usershare"),
348  QStringLiteral("add"),
349  shareData.name(),
350  shareData.path(),
351  shareData.comment(),
352  shareData.acl(),
353  guestok,
354  };
355 
356  QByteArray stdOut;
357  int ret = runProcess(QStringLiteral("net"), args, stdOut, m_stdErr);
358 
359  // TODO: parse and process error messages.
360  if (!m_stdErr.isEmpty()) {
361  // create a parser for the error output and
362  // send error message somewhere
363  qCWarning(KIO_CORE) << "We got some errors while running 'net usershare add'" << args;
364  qCWarning(KIO_CORE) << m_stdErr;
365  }
366 
367  if (ret == 0 && !data.contains(shareData.name())) {
368  // It needs to be added in this function explicitly, otherwise another instance of
369  // KSambaShareDataPrivate will be created and added to data when the share
370  // definition changes on-disk and we re-parse the data.
371  data.insert(shareData.name(), shareData);
372  }
373 
374  return (ret == 0) ? KSambaShareData::UserShareOk : KSambaShareData::UserShareSystemError;
375 }
376 
377 KSambaShareData::UserShareError KSambaSharePrivate::remove(const KSambaShareData &shareData)
378 {
379  if (!isSambaInstalled()) {
380  return KSambaShareData::UserShareSystemError;
381  }
382 
383  if (!data.contains(shareData.name())) {
384  return KSambaShareData::UserShareNameInvalid;
385  }
386 
387  const QStringList args{
388  QStringLiteral("usershare"),
389  QStringLiteral("delete"),
390  shareData.name(),
391  };
392 
393  QByteArray stdOut;
394  int ret = runProcess(QStringLiteral("net"), args, stdOut, m_stdErr);
395 
396  // TODO: parse and process error messages.
397  if (!m_stdErr.isEmpty()) {
398  // create a parser for the error output and
399  // send error message somewhere
400  qCWarning(KIO_CORE) << "We got some errors while running 'net usershare delete'" << args;
401  qCWarning(KIO_CORE) << m_stdErr;
402  }
403 
404  return (ret == 0) ? KSambaShareData::UserShareOk : KSambaShareData::UserShareSystemError;
405 
406  // NB: the share file gets deleted which leads us to reload and drop the ShareData, hence no explicit remove
407 }
408 
409 QMap<QString, KSambaShareData> KSambaSharePrivate::parse(const QByteArray &usershareData)
410 {
411  const QRegularExpression headerRx(
412  QRegularExpression::anchoredPattern(QStringLiteral("^\\s*\\["
413  "([^%<>*\?|/+=;:\",]+)"
414  "\\]")));
415 
416  const QRegularExpression OptValRx(
417  QRegularExpression::anchoredPattern(QStringLiteral("^\\s*([\\w\\d\\s]+)"
418  "="
419  "(.*)$")));
420 
421  QTextStream stream(usershareData);
422  QString currentShare;
424 
425  while (!stream.atEnd()) {
426  const QString line = stream.readLine().trimmed();
427 
429  if ((match = headerRx.match(line)).hasMatch()) {
430  currentShare = match.captured(1).trimmed();
431 
432  if (!shares.contains(currentShare)) {
433  KSambaShareData shareData;
434  shareData.dd->name = currentShare;
435  shares.insert(currentShare, shareData);
436  }
437  } else if ((match = OptValRx.match(line)).hasMatch()) {
438  const QString key = match.captured(1).trimmed();
439  const QString value = match.captured(2).trimmed();
440  KSambaShareData shareData = shares[currentShare];
441 
442  if (key == QLatin1String("path")) {
443  // Samba accepts paths with and w/o trailing slash, we
444  // use and expect path without slash
445  shareData.dd->path = value.endsWith(QLatin1Char('/')) ? value.chopped(1) : value;
446  } else if (key == QLatin1String("comment")) {
447  shareData.dd->comment = value;
448  } else if (key == QLatin1String("usershare_acl")) {
449  shareData.dd->acl = value;
450  } else if (key == QLatin1String("guest_ok")) {
451  shareData.dd->guestPermission = value;
452  } else {
453  qCWarning(KIO_CORE) << "Something nasty happen while parsing 'net usershare info'"
454  << "share:" << currentShare << "key:" << key;
455  }
456  } else if (line.trimmed().isEmpty()) {
457  continue;
458  } else {
459  return shares;
460  }
461  }
462 
463  return shares;
464 }
465 
466 void KSambaSharePrivate::_k_slotFileChange(const QString &path)
467 {
468  if (path != userSharePath) {
469  return;
470  }
471  data = parse(getNetUserShareInfo());
472  qCDebug(KIO_CORE) << "reloading data; path changed:" << path;
473  Q_Q(KSambaShare);
474  Q_EMIT q->changed();
475 }
476 
477 KSambaShare::KSambaShare()
478  : QObject(nullptr)
479  , d_ptr(new KSambaSharePrivate(this))
480 {
481  Q_D(const KSambaShare);
482  if (!d->userSharePath.isEmpty() && QFileInfo::exists(d->userSharePath)) {
483  KDirWatch::self()->addDir(d->userSharePath, KDirWatch::WatchFiles);
484  connect(KDirWatch::self(), &KDirWatch::dirty, this, [this](const QString &path) {
485  Q_D(KSambaShare);
486  d->_k_slotFileChange(path);
487  });
488  }
489 }
490 
491 KSambaShare::~KSambaShare()
492 {
493  Q_D(const KSambaShare);
494  if (KDirWatch::exists() && KDirWatch::self()->contains(d->userSharePath)) {
495  KDirWatch::self()->removeDir(d->userSharePath);
496  }
497  delete d_ptr;
498 }
499 
500 #if KIOCORE_BUILD_DEPRECATED_SINCE(4, 6)
502 {
503  Q_D(const KSambaShare);
504  return d->smbConf;
505 }
506 #endif
507 
509 {
510  Q_D(const KSambaShare);
511  return d->isDirectoryShared(path);
512 }
513 
515 {
516  Q_D(const KSambaShare);
517  return d->isShareNameValid(name) && d->isShareNameAvailable(name);
518 }
519 
521 {
522  Q_D(const KSambaShare);
523  return d->shareNames();
524 }
525 
527 {
528  Q_D(const KSambaShare);
529  return d->sharedDirs();
530 }
531 
533 {
534  Q_D(const KSambaShare);
535  return d->getShareByName(name);
536 }
537 
539 {
540  Q_D(const KSambaShare);
541  return d->getSharesByPath(path);
542 }
543 
545 {
546  Q_D(const KSambaShare);
547  return QString::fromUtf8(d->m_stdErr);
548 }
549 
551 {
552  Q_D(const KSambaShare);
553  return d->areGuestsAllowed();
554 }
555 
556 class KSambaShareSingleton
557 {
558 public:
559  KSambaShare instance;
560 };
561 
562 Q_GLOBAL_STATIC(KSambaShareSingleton, _instance)
563 
565 {
566  return &_instance()->instance;
567 }
568 
569 #include "moc_ksambashare.cpp"
QStringList shareNames() const
Returns the list of available shares.
QString captured(int nth) const const
QString & append(QChar ch)
static KDirWatch * self()
bool contains(const Key &key) const const
bool isDirectoryShared(const QString &path) const
Whether or not the given path is shared by Samba.
QList< QByteArray > split(char sep) const const
QByteArray trimmed() const const
QString path() const
void removeFirst()
QMap::const_iterator constBegin() const const
const T & at(int i) const const
bool contains(const QString &str, Qt::CaseSensitivity cs) const const
bool isEmpty() const const
bool startsWith(const QByteArray &ba) const const
QString findExecutable(const QString &executableName, const QStringList &paths)
This class represents a Samba user share.
static KSambaShare * instance()
bool exists() const const
bool areGuestsAllowed() const
Check whether usershares may enable guests.
static QStringList allUserNames(uint maxCount=KCOREADDONS_UINT_MAX)
QString chopped(int len) const const
int count(const T &value) const const
QString fromLocal8Bit(const char *str, int size)
QString fromUtf8(const char *str, int size)
bool isShareNameAvailable(const QString &name) const
Tests that a share name is valid and does not conflict with system users names or shares...
KHEALTHCERTIFICATE_EXPORT QVariant parse(const QByteArray &data)
QString anchoredPattern(const QString &expression)
bool isEmpty() const const
QString trimmed() const const
QMap::const_iterator constEnd() const const
bool endsWith(const QString &s, Qt::CaseSensitivity cs) const const
KCOREADDONS_EXPORT Result match(QStringView pattern, QStringView str)
bool exists() const const
KSambaShareData::GuestPermission guestPermission() const
void setProcessChannelMode(QProcess::ProcessChannelMode mode)
QList< KSambaShareData > getSharesByPath(const QString &path) const
Returns a list of KSambaShareData matching the path.
QString lastSystemErrorString() const
Used to obtain UserShareSystemError error strings.
void insert(int i, const T &value)
QString arg(qlonglong a, int fieldWidth, int base, QChar fillChar) const const
QString name() const
Q_D(Todo)
bool contains(char ch) const const
QString comment() const
int length() const const
QString localHostName()
void removeDir(const QString &path)
QString fromLatin1(const char *str, int size)
void addDir(const QString &path, WatchModes watchModes=WatchDirOnly)
QMap::iterator insert(const Key &key, const T &value)
QStringList sharedDirectories() const
Returns a list of all directories shared by local users in Samba.
static bool exists()
QString smbConfPath() const
Returns the path to the used smb.conf file or empty string if no file was found.
void dirty(const QString &path)
QMetaObject::Connection connect(const QObject *sender, const char *signal, const QObject *receiver, const char *method, Qt::ConnectionType type)
QString acl() const
Returns a containing a string describing the permission added to the users, such as "[DOMAIN\]usernam...
KSambaShareData getShareByName(const QString &name) const
Returns the KSambaShareData object of the share name.
QByteArray readAllStandardOutput()
int exitCode() const const
void start(const QString &program, const QStringList &arguments, QIODevice::OpenMode mode)
Q_EMITQ_EMIT
QByteArray readAllStandardError()
This class lists Samba user shares and monitors them for addition, update and removal.
Definition: ksambashare.h:25
const T value(const Key &key, const T &defaultValue) const const
KStandardDirs * dirs()
bool waitForFinished(int msecs)
This file is part of the KDE documentation.
Documentation copyright © 1996-2022 The KDE developers.
Generated on Thu Jan 27 2022 22:53:29 by doxygen 1.8.11 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.