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

source code of kio/src/core/ksambashare.cpp