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

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