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 | |
33 | Q_DECLARE_LOGGING_CATEGORY(KIO_CORE_SAMBASHARE) |
34 | Q_LOGGING_CATEGORY(KIO_CORE_SAMBASHARE, "kf.kio.core.sambashare" , QtWarningMsg) |
35 | |
36 | KSambaSharePrivate::KSambaSharePrivate(KSambaShare *parent) |
37 | : q_ptr(parent) |
38 | , data() |
39 | , userSharePath() |
40 | , skipUserShare(false) |
41 | { |
42 | setUserSharePath(); |
43 | data = parse(usershareData: getNetUserShareInfo()); |
44 | } |
45 | |
46 | KSambaSharePrivate::~KSambaSharePrivate() |
47 | { |
48 | } |
49 | |
50 | bool 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 | |
66 | void 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 | |
75 | int 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 | |
95 | QString KSambaSharePrivate::testparmParamValue(const QString ¶meterName) |
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 | |
160 | QByteArray 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 | |
193 | QStringList KSambaSharePrivate::shareNames() const |
194 | { |
195 | return data.keys(); |
196 | } |
197 | |
198 | QStringList 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 | |
212 | KSambaShareData KSambaSharePrivate::getShareByName(const QString &shareName) const |
213 | { |
214 | return data.value(key: shareName); |
215 | } |
216 | |
217 | QList<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 | |
231 | bool 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 | |
238 | bool 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 | |
250 | bool 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 | |
256 | KSambaShareData::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 | |
284 | KSambaShareData::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 | |
294 | bool KSambaSharePrivate::areGuestsAllowed() const |
295 | { |
296 | return KSambaSharePrivate::testparmParamValue(QStringLiteral("usershare allow guests" )) != QLatin1String("No" ); |
297 | } |
298 | |
299 | KSambaShareData::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 | |
308 | KSambaShareData::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 | |
357 | KSambaShareData::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 | |
389 | QMap<QString, KSambaShareData> KSambaSharePrivate::parse(const QByteArray &usershareData) |
390 | { |
391 | static const char16_t [] = uR"--(^\s*\[([^%<>*?|/+=;:",]+)\])--" ; |
392 | static const QRegularExpression (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 | |
442 | void 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 | |
453 | KSambaShare::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 | |
466 | KSambaShare::~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 | |
475 | bool KSambaShare::isDirectoryShared(const QString &path) const |
476 | { |
477 | Q_D(const KSambaShare); |
478 | return d->isDirectoryShared(path); |
479 | } |
480 | |
481 | bool KSambaShare::isShareNameAvailable(const QString &name) const |
482 | { |
483 | Q_D(const KSambaShare); |
484 | return d->isShareNameValid(name) && d->isShareNameAvailable(name); |
485 | } |
486 | |
487 | QStringList KSambaShare::shareNames() const |
488 | { |
489 | Q_D(const KSambaShare); |
490 | return d->shareNames(); |
491 | } |
492 | |
493 | QStringList KSambaShare::sharedDirectories() const |
494 | { |
495 | Q_D(const KSambaShare); |
496 | return d->sharedDirs(); |
497 | } |
498 | |
499 | KSambaShareData KSambaShare::getShareByName(const QString &name) const |
500 | { |
501 | Q_D(const KSambaShare); |
502 | return d->getShareByName(shareName: name); |
503 | } |
504 | |
505 | QList<KSambaShareData> KSambaShare::getSharesByPath(const QString &path) const |
506 | { |
507 | Q_D(const KSambaShare); |
508 | return d->getSharesByPath(path); |
509 | } |
510 | |
511 | QString KSambaShare::lastSystemErrorString() const |
512 | { |
513 | Q_D(const KSambaShare); |
514 | return QString::fromUtf8(ba: d->m_stdErr); |
515 | } |
516 | |
517 | bool KSambaShare::areGuestsAllowed() const |
518 | { |
519 | Q_D(const KSambaShare); |
520 | return d->areGuestsAllowed(); |
521 | } |
522 | |
523 | class KSambaShareSingleton |
524 | { |
525 | public: |
526 | KSambaShare instance; |
527 | }; |
528 | |
529 | Q_GLOBAL_STATIC(KSambaShareSingleton, _instance) |
530 | |
531 | KSambaShare *KSambaShare::instance() |
532 | { |
533 | return &_instance()->instance; |
534 | } |
535 | |
536 | #include "moc_ksambashare.cpp" |
537 | |