diff --git a/smb/discovery.h b/smb/discovery.h index 9388292d..1d9e921d 100644 --- a/smb/discovery.h +++ b/smb/discovery.h @@ -1,52 +1,53 @@ /* Copyright 2019 Harald Sitter This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3 of the License or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of version 3 of the license. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #ifndef DISCOVERY_H #define DISCOVERY_H #include #include class Discovery { public: typedef QSharedPointer Ptr; Discovery(); virtual ~Discovery(); + virtual QString udsName() const = 0; virtual KIO::UDSEntry toEntry() const = 0; }; class Discoverer { public: Discoverer(); virtual ~Discoverer(); virtual void start() = 0; virtual void stop() = 0; virtual bool isFinished() const = 0; // Implement as signal! virtual void newDiscovery(Discovery::Ptr discovery) = 0; virtual void finished() = 0; }; #endif // DISCOVERY_H diff --git a/smb/dnssddiscoverer.cpp b/smb/dnssddiscoverer.cpp index 6294fdfd..9ddbff7c 100644 --- a/smb/dnssddiscoverer.cpp +++ b/smb/dnssddiscoverer.cpp @@ -1,118 +1,123 @@ /* Copyright 2019 Harald Sitter This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3 of the License or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of version 3 of the license. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include "dnssddiscoverer.h" #include "kio_smb.h" DNSSDDiscovery::DNSSDDiscovery(KDNSSD::RemoteService::Ptr service) : m_service(service) { } +QString DNSSDDiscovery::udsName() const +{ + return m_service->serviceName(); +} + KIO::UDSEntry DNSSDDiscovery::toEntry() const { KIO::UDSEntry entry; - entry.fastInsert(KIO::UDSEntry::UDS_NAME, m_service->serviceName()); + entry.fastInsert(KIO::UDSEntry::UDS_NAME, udsName()); entry.fastInsert(KIO::UDSEntry::UDS_FILE_TYPE, S_IFDIR); entry.fastInsert(KIO::UDSEntry::UDS_ACCESS, (S_IRUSR | S_IXUSR | S_IRGRP | S_IXGRP | S_IROTH | S_IXOTH)); entry.fastInsert(KIO::UDSEntry::UDS_ICON_NAME, "network-server"); // TODO: it may be better to resolve the host to an ip address. dnssd // being able to find a service doesn't mean name resolution is // properly set up for its domain. So, we may not be able to resolve // this without help from avahi. OTOH KDNSSD doesn't have API for this // and from a platform POV we should probably assume that if avahi // is functional it is also set up as resolution provider. // Given the plugin design on glibc's libnss however I am not sure // that assumption will be true all the time. ~sitter, 2018 QUrl u; u.setScheme(QStringLiteral("smb")); u.setHost(m_service->hostName()); const int defaultPort = 445; if (m_service->port() > 0 && m_service->port() != defaultPort) { u.setPort(m_service->port()); } u.setPath("/"); // https://bugs.kde.org/show_bug.cgi?id=388922 entry.fastInsert(KIO::UDSEntry::UDS_URL, u.url()); entry.fastInsert(KIO::UDSEntry::UDS_MIME_TYPE, QStringLiteral("application/x-smb-server")); return entry; } DNSSDDiscoverer::DNSSDDiscoverer() { connect(&m_browser, &KDNSSD::ServiceBrowser::serviceAdded, this, [=](KDNSSD::RemoteService::Ptr service){ qCDebug(KIO_SMB_LOG) << "DNSSD added:" << service->serviceName() << service->type() << service->domain() << service->hostName() << service->port(); // Manual contains check. We need to use the == of the underlying // objects, not the pointers. The same service may have >1 // RemoteService* instances representing it, so the == impl of // RemoteService::Ptr is useless here. for (const auto &servicePtr : qAsConst(m_services)) { if (*service == *servicePtr) { return; } } connect(service.data(), &KDNSSD::RemoteService::resolved, this, [=] { ++m_resolvedCount; emit newDiscovery(Discovery::Ptr(new DNSSDDiscovery(service))); maybeFinish(); }); // Schedule resolution of hostname. We'll later call resolve // which will block until the resolution is done. This basically // gives us a head start on discovery. service->resolveAsync(); m_services.append(service); }); connect(&m_browser, &KDNSSD::ServiceBrowser::finished, this, &DNSSDDiscoverer::stop); } void DNSSDDiscoverer::start() { m_browser.startBrowse(); } void DNSSDDiscoverer::stop() { m_browser.disconnect(); m_disconnected = true; maybeFinish(); } bool DNSSDDiscoverer::isFinished() const { return m_disconnected && m_services.count() == m_resolvedCount; } void DNSSDDiscoverer::maybeFinish() { if (isFinished()) { emit finished(); } } diff --git a/smb/dnssddiscoverer.h b/smb/dnssddiscoverer.h index c751f202..bf652a3c 100644 --- a/smb/dnssddiscoverer.h +++ b/smb/dnssddiscoverer.h @@ -1,64 +1,65 @@ /* Copyright 2019 Harald Sitter This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3 of the License or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of version 3 of the license. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #ifndef DNSSDDISCOVERER_H #define DNSSDDISCOVERER_H #include #include #include #include "discovery.h" class DNSSDDiscovery : public Discovery { public: DNSSDDiscovery(KDNSSD::RemoteService::Ptr service); + QString udsName() const override; KIO::UDSEntry toEntry() const override; private: KDNSSD::RemoteService::Ptr m_service; }; class DNSSDDiscoverer : public QObject, public Discoverer { Q_OBJECT public: DNSSDDiscoverer(); void start() override; bool isFinished() const override; signals: void newDiscovery(Discovery::Ptr discovery) override; void finished() override; private: void stop() override; void maybeFinish(); KDNSSD::ServiceBrowser m_browser {QStringLiteral("_smb._tcp")}; QList m_services; int m_resolvedCount = 0; bool m_disconnected = false; }; #endif // DNSSDDISCOVERER_H diff --git a/smb/kio_smb_browse.cpp b/smb/kio_smb_browse.cpp index c3451303..ecf7d560 100644 --- a/smb/kio_smb_browse.cpp +++ b/smb/kio_smb_browse.cpp @@ -1,660 +1,667 @@ ///////////////////////////////////////////////////////////////////////////// // // Project: SMB kioslave for KDE2 // // File: kio_smb_browse.cpp // // Abstract: member function implementations for SMBSlave that deal with // SMB browsing // // Author(s): Matthew Peterson // //--------------------------------------------------------------------------- // // Copyright (c) 2000 Caldera Systems, Inc. // Copyright (c) 2018-2020 Harald Sitter // // This program is free software; you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by the // Free Software Foundation; either version 2.1 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Lesser General Public License for more details. // // You should have received a copy of the GNU General Public License // along with this program; see the file COPYING. If not, please obtain // a copy from https://www.gnu.org/copyleft/gpl.html // ///////////////////////////////////////////////////////////////////////////// #include "kio_smb.h" #include "smburl.h" #include #include #include #include #include #include #include #include #include "dnssddiscoverer.h" #include "wsdiscoverer.h" #include using namespace KIO; int SMBSlave::cache_stat(const SMBUrl &url, struct stat *st) { int cacheStatErr; int result = smbc_stat(url.toSmbcUrl(), st); if (result == 0) { cacheStatErr = 0; } else { cacheStatErr = errno; } qCDebug(KIO_SMB_LOG) << "size " << static_cast(st->st_size); return cacheStatErr; } int SMBSlave::browse_stat_path(const SMBUrl &url, UDSEntry &udsentry) { int cacheStatErr = cache_stat(url, &st); if (cacheStatErr == 0) { return statToUDSEntry(url, st, udsentry); } return cacheStatErr; } int SMBSlave::statToUDSEntry(const QUrl &url, const struct stat &st, KIO::UDSEntry &udsentry) { if (!S_ISDIR(st.st_mode) && !S_ISREG(st.st_mode)) { qCDebug(KIO_SMB_LOG) << "mode: "<< st.st_mode; warning(i18n("%1:\n" "Unknown file type, neither directory or file.", url.toDisplayString())); return EINVAL; } if (!S_ISDIR(st.st_mode)) { // Awkwardly documented at // https://www.samba.org/samba/docs/using_samba/ch08.html // libsmb_stat.c assigns special meaning to +x permissions // (obviously only on files, all dirs are +x so this hacky representation // wouldn't work!): // - S_IXUSR = DOS archive: This file has been touched since the last DOS backup was performed on it. // - S_IXGRP = DOS system: This file has a specific purpose required by the operating system. // - S_IXOTH = DOS hidden: This file has been marked to be invisible to the user, unless the operating system is explicitly set to show it. // Only hiding has backing through KIO right now. if (st.st_mode & S_IXOTH) { // DOS hidden udsentry.fastInsert(KIO::UDSEntry::UDS_HIDDEN, true); } } // UID and GID **must** not be mapped. The values returned by libsmbclient are // simply the getuid/getgid of the process. They mean absolutely nothing. // Also see libsmb_stat.c. // Related: https://bugs.kde.org/show_bug.cgi?id=212801 // POSIX Access mode must not be mapped either! // It's meaningless for smb shares and downright disadvantagous. // The mode attributes outside the ones used and document above are // useless. The only one actively set is readonlyness. // // BUT the READONLY attribute does nothing on NT systems: // https://support.microsoft.com/en-us/help/326549/you-cannot-view-or-change-the-read-only-or-the-system-attributes-of-fo // The Read-only and System attributes is only used by Windows Explorer to determine // whether the folder is a special folder, such as a system folder that has its view // customized by Windows (for example, My Documents, Favorites, Fonts, Downloaded Program Files), // or a folder that you customized by using the Customize tab of the folder's Properties dialog box. // // As such respecting it on a KIO level is actually wrong as it doesn't indicate actual // readonlyness since the 90s and causes us to show readonly UI states when in fact // the directory is perfectly writable. // https://bugs.kde.org/show_bug.cgi?id=414482 // // Should we ever want to parse desktop.ini like we do .directory we'd only want to when a // dir is readonly as per the above microsoft support article. // Also see: // https://docs.microsoft.com/en-us/windows/win32/shell/how-to-customize-folders-with-desktop-ini udsentry.fastInsert(KIO::UDSEntry::UDS_FILE_TYPE, st.st_mode & S_IFMT); udsentry.fastInsert(KIO::UDSEntry::UDS_SIZE, st.st_size); udsentry.fastInsert(KIO::UDSEntry::UDS_MODIFICATION_TIME, st.st_mtime); udsentry.fastInsert(KIO::UDSEntry::UDS_ACCESS_TIME, st.st_atime); // No, st_ctime is not UDS_CREATION_TIME... return 0; } void SMBSlave::stat(const QUrl &kurl) { qCDebug(KIO_SMB_LOG) << kurl; // make a valid URL QUrl url = checkURL(kurl); // if URL is not valid we have to redirect to correct URL if (url != kurl) { qCDebug(KIO_SMB_LOG) << "redirection " << url; redirection(url); finished(); return; } m_current_url = url; UDSEntry udsentry; // Set name udsentry.fastInsert(KIO::UDSEntry::UDS_NAME, kurl.fileName()); switch (m_current_url.getType()) { case SMBURLTYPE_UNKNOWN: error(ERR_MALFORMED_URL, url.toDisplayString()); return; case SMBURLTYPE_ENTIRE_NETWORK: case SMBURLTYPE_WORKGROUP_OR_SERVER: udsentry.fastInsert(KIO::UDSEntry::UDS_FILE_TYPE, S_IFDIR); statEntry(udsentry); finished(); return; case SMBURLTYPE_SHARE_OR_PATH: { int ret = browse_stat_path(m_current_url, udsentry); if (ret == EPERM || ret == EACCES || workaroundEEXIST(ret)) { SMBUrl smbUrl(url); const int passwordError = checkPassword(smbUrl); if (passwordError == KJob::NoError) { redirection(smbUrl); finished(); } else if (passwordError == KIO::ERR_USER_CANCELED) { reportError(url, ret); } else { error(passwordError, url.toString()); } return; } else if (ret != 0) { qCDebug(KIO_SMB_LOG) << "stat() error" << ret << url; reportError(url, ret); return; } statEntry(udsentry); finished(); return; } } qCDebug(KIO_SMB_LOG) << "UNKNOWN " << url; finished(); } // TODO: complete checking <-- what does that even mean? // TODO: why is this not part of SMBUrl or at the very least URL validation should // be 100% shared between this and SMBUrl. Notably SMBUrl has code that looks // to do a similar thing but is much less complete. QUrl SMBSlave::checkURL(const QUrl &kurl_) const { qCDebug(KIO_SMB_LOG) << "checkURL " << kurl_; QUrl kurl(kurl_); // We treat cifs as an alias but need to translate it to smb. // https://bugs.kde.org/show_bug.cgi?id=327295 // It's not IANA registered and also libsmbc internally expects // smb URIs so we do very broadly coerce cifs to smb. // Also see SMBUrl. if (kurl.scheme() == "cifs") { kurl.setScheme("smb"); } QString surl = kurl.url(); // transform any links in the form smb:/ into smb:// if (surl.startsWith(QLatin1String("smb:/"))) { if (surl.length() == 5) { return QUrl("smb://"); } if (surl.at(5) != '/') { surl = "smb://" + surl.mid(5); qCDebug(KIO_SMB_LOG) << "checkURL return1 " << surl << " " << QUrl(surl); return QUrl(surl); } } if (surl == QLatin1String("smb://")) { return kurl; // unchanged } // smb:// normally have no userinfo // we must redirect ourself to remove the username and password if (surl.contains('@') && !surl.contains("smb://")) { QUrl url(kurl); url.setPath('/' + kurl.url().right(kurl.url().length() - kurl.url().indexOf('@') - 1)); QString userinfo = kurl.url().mid(5, kurl.url().indexOf('@') - 5); if (userinfo.contains(':')) { url.setUserName(userinfo.left(userinfo.indexOf(':'))); url.setPassword(userinfo.right(userinfo.length() - userinfo.indexOf(':') - 1)); } else { url.setUserName(userinfo); } qCDebug(KIO_SMB_LOG) << "checkURL return2 " << url; return url; } // if there's a valid host, don't have an empty path QUrl url(kurl); if (url.path().isEmpty()) url.setPath("/"); qCDebug(KIO_SMB_LOG) << "checkURL return3 " << url; return url; } SMBSlave::SMBError SMBSlave::errnumToKioError(const SMBUrl &url, const int errNum) { qCDebug(KIO_SMB_LOG) << "errNum" << errNum; switch (errNum) { case ENOENT: if (url.getType() == SMBURLTYPE_ENTIRE_NETWORK) return SMBError {ERR_SLAVE_DEFINED, i18n("Unable to find any workgroups in your local network. This might be caused by an enabled firewall.")}; else return SMBError {ERR_DOES_NOT_EXIST, url.toDisplayString()}; #ifdef ENOMEDIUM case ENOMEDIUM: return SMBError {ERR_SLAVE_DEFINED, i18n("No media in device for %1", url.toDisplayString())}; #endif #ifdef EHOSTDOWN case EHOSTDOWN: #endif case ECONNREFUSED: return SMBError {ERR_SLAVE_DEFINED, i18n("Could not connect to host for %1", url.toDisplayString())}; case ENOTDIR: return SMBError {ERR_CANNOT_ENTER_DIRECTORY, url.toDisplayString()}; case EFAULT: case EINVAL: return SMBError {ERR_DOES_NOT_EXIST, url.toDisplayString()}; case EPERM: case EACCES: return SMBError {ERR_ACCESS_DENIED, url.toDisplayString()}; case EIO: case ENETUNREACH: if (url.getType() == SMBURLTYPE_ENTIRE_NETWORK || url.getType() == SMBURLTYPE_WORKGROUP_OR_SERVER) return SMBError {ERR_SLAVE_DEFINED, i18n("Error while connecting to server responsible for %1", url.toDisplayString())}; else return SMBError {ERR_CONNECTION_BROKEN, url.toDisplayString()}; case ENOMEM: return SMBError {ERR_OUT_OF_MEMORY, url.toDisplayString()}; case ENODEV: return SMBError {ERR_SLAVE_DEFINED, i18n("Share could not be found on given server")}; case EBADF: return SMBError {ERR_INTERNAL, i18n("Bad file descriptor")}; case ETIMEDOUT: return SMBError {ERR_SERVER_TIMEOUT, url.host()}; case ENOTEMPTY: return SMBError {ERR_CANNOT_RMDIR, url.toDisplayString()}; #ifdef ENOTUNIQ case ENOTUNIQ: return SMBError {ERR_SLAVE_DEFINED, i18n("The given name could not be resolved to a unique server. " "Make sure your network is setup without any name conflicts " "between names used by Windows and by UNIX name resolution.")}; #endif case ECONNABORTED: return SMBError {ERR_CONNECTION_BROKEN, url.host()}; case EHOSTUNREACH: return SMBError {ERR_CANNOT_CONNECT, i18nc("@info:status smb failed to reach the server (e.g. server offline or network failure). %1 is an ip address or hostname", "%1: Host unreachable", url.host())}; case 0: // success return SMBError {ERR_INTERNAL, i18n("libsmbclient reported an error, but did not specify " "what the problem is. This might indicate a severe problem " "with your network - but also might indicate a problem with " "libsmbclient.\n" "If you want to help us, please provide a tcpdump of the " "network interface while you try to browse (be aware that " "it might contain private data, so do not post it if you are " "unsure about that - you can send it privately to the developers " "if they ask for it)")}; default: return SMBError {ERR_INTERNAL, i18n("Unknown error condition in stat: %1", QString::fromLocal8Bit(strerror(errNum)))}; } } void SMBSlave::reportError(const SMBUrl &url, const int errNum) { const SMBError smbErr = errnumToKioError(url, errNum); error(smbErr.kioErrorId, smbErr.errorString); } void SMBSlave::reportWarning(const SMBUrl &url, const int errNum) { const SMBError smbErr = errnumToKioError(url, errNum); const QString errorString = buildErrorString(smbErr.kioErrorId, smbErr.errorString); warning(xi18n("Error occurred while trying to access %1%2", url.url(), errorString)); } void SMBSlave::listDir(const QUrl &kurl) { qCDebug(KIO_SMB_LOG) << kurl; int errNum = 0; // check (correct) URL QUrl url = checkURL(kurl); // if URL is not valid we have to redirect to correct URL if (url != kurl) { redirection(url); finished(); return; } m_current_url = kurl; struct smbc_dirent *dirp = nullptr; UDSEntry udsentry; bool dir_is_root = true; int dirfd = smbc_opendir(m_current_url.toSmbcUrl()); if (dirfd > 0) { errNum = 0; } else { errNum = errno; } qCDebug(KIO_SMB_LOG) << "open " << m_current_url.toSmbcUrl() << " " << m_current_url.getType() << " " << dirfd; if (dirfd >= 0) { #ifdef HAVE_READDIRPLUS2 // readdirplus2 improves performance by giving us a stat without separate call (Samba>=4.12) while (const struct libsmb_file_info *fileInfo = smbc_readdirplus2(dirfd, &st)) { const QString name = QString::fromUtf8(fileInfo->name); if (name == ".") { continue; } else if (name == "..") { dir_is_root = false; continue; } udsentry.fastInsert(KIO::UDSEntry::UDS_NAME, name); m_current_url.addPath(name); statToUDSEntry(m_current_url, st, udsentry); // won't produce useful error listEntry(udsentry); m_current_url.cdUp(); udsentry.clear(); } #endif // HAVE_READDIRPLUS2 uint direntCount = 0; do { qCDebug(KIO_SMB_LOG) << "smbc_readdir "; dirp = smbc_readdir(dirfd); if (dirp == nullptr) break; ++direntCount; // Set name QString udsName; const QString dirpName = QString::fromUtf8(dirp->name); // We cannot trust dirp->commentlen has it might be with or without the NUL character // See KDE bug #111430 and Samba bug #3030 const QString comment = QString::fromUtf8(dirp->comment); if (dirp->smbc_type == SMBC_SERVER || dirp->smbc_type == SMBC_WORKGROUP) { udsName = dirpName.toLower(); udsName[0] = dirpName.at(0).toUpper(); if (!comment.isEmpty() && dirp->smbc_type == SMBC_SERVER) udsName += " (" + comment + ')'; } else udsName = dirpName; qCDebug(KIO_SMB_LOG) << "dirp->name " << dirp->name << " " << dirpName << " '" << comment << "'" << " " << dirp->smbc_type; udsentry.fastInsert(KIO::UDSEntry::UDS_NAME, udsName); udsentry.fastInsert(KIO::UDSEntry::UDS_COMMENT, QString::fromUtf8(dirp->comment)); // Mark all administrative shares, e.g ADMIN$, as hidden. #197903 if (dirpName.endsWith(QLatin1Char('$'))) { // qCDebug(KIO_SMB_LOG) << dirpName << "marked as hidden"; udsentry.fastInsert(KIO::UDSEntry::UDS_HIDDEN, 1); } if (udsName == ".") { // Skip the "." entry // Mind the way m_current_url is handled in the loop } else if (udsName == "..") { dir_is_root = false; // fprintf(stderr,"----------- hide: -%s-\n",dirp->name); // do nothing and hide the hidden shares #if !defined(HAVE_READDIRPLUS2) } else if (dirp->smbc_type == SMBC_FILE || dirp->smbc_type == SMBC_DIR) { // Set stat information m_current_url.addPath(dirpName); const int statErr = browse_stat_path(m_current_url, udsentry); if (statErr) { if (statErr == ENOENT || statErr == ENOTDIR) { reportWarning(m_current_url, statErr); } } else { // Call base class to list entry listEntry(udsentry); } m_current_url.cdUp(); #endif // HAVE_READDIRPLUS2 } else if (dirp->smbc_type == SMBC_SERVER || dirp->smbc_type == SMBC_FILE_SHARE) { // Set type udsentry.fastInsert(KIO::UDSEntry::UDS_FILE_TYPE, S_IFDIR); if (dirp->smbc_type == SMBC_SERVER) { udsentry.fastInsert(KIO::UDSEntry::UDS_ACCESS, (S_IRUSR | S_IXUSR | S_IRGRP | S_IXGRP | S_IROTH | S_IXOTH)); // QString workgroup = m_current_url.host().toUpper(); QUrl u("smb://"); u.setHost(dirpName); // when libsmbclient knows // u = QString("smb://%1?WORKGROUP=%2").arg(dirpName).arg(workgroup.toUpper()); qCDebug(KIO_SMB_LOG) << "list item " << u; udsentry.fastInsert(KIO::UDSEntry::UDS_URL, u.url()); udsentry.fastInsert(KIO::UDSEntry::UDS_MIME_TYPE, QString::fromLatin1("application/x-smb-server")); } else udsentry.fastInsert(KIO::UDSEntry::UDS_ACCESS, (S_IRWXU | S_IRWXG | S_IROTH | S_IXOTH)); // Call base class to list entry listEntry(udsentry); } else if (dirp->smbc_type == SMBC_WORKGROUP) { // Set type udsentry.fastInsert(KIO::UDSEntry::UDS_FILE_TYPE, S_IFDIR); // Set permissions udsentry.fastInsert(KIO::UDSEntry::UDS_ACCESS, (S_IRUSR | S_IRGRP | S_IROTH | S_IXUSR | S_IXGRP | S_IXOTH)); udsentry.fastInsert(KIO::UDSEntry::UDS_MIME_TYPE, QString::fromLatin1("application/x-smb-workgroup")); // QString workgroup = m_current_url.host().toUpper(); QUrl u("smb://"); u.setHost(dirpName); udsentry.fastInsert(KIO::UDSEntry::UDS_URL, u.url()); // Call base class to list entry listEntry(udsentry); } else { qCDebug(KIO_SMB_LOG) << "SMBC_UNKNOWN :" << dirpName; // TODO: we don't handle SMBC_IPC_SHARE, SMBC_PRINTER_SHARE // SMBC_LINK, SMBC_COMMS_SHARE // SlaveBase::error(ERR_INTERNAL, TEXT_UNSUPPORTED_FILE_TYPE); // continue; } udsentry.clear(); } while (dirp); // checked already in the head // Run service discovery if the path is root. This augments // "native" results from libsmbclient. auto normalizedUrl = url.adjusted(QUrl::NormalizePathSegments); if (normalizedUrl.path().isEmpty()) { qCDebug(KIO_SMB_LOG) << "Trying modern discovery (dnssd/wsdiscovery)"; QEventLoop e; UDSEntryList list; + QStringList discoveredNames; const auto flushEntries = [this, &list]() { if (list.isEmpty()) { return; } listEntries(list); list.clear(); }; const auto quitLoop = [&e, &flushEntries]() { flushEntries(); e.quit(); }; // Since slavebase has no eventloop it wont publish results // on a timer, since we do not know how long our discovery // will take this is super meh because we may appear // stuck for a while. Implement our own listing system // based on QTimer to mitigate. QTimer sendTimer; sendTimer.setInterval(300); connect(&sendTimer, &QTimer::timeout, this, flushEntries); sendTimer.start(); DNSSDDiscoverer d; WSDiscoverer w; const QList discoverers {&d, &w}; - auto appendDiscovery = [&](const Discovery::Ptr &discovery) { list.append(discovery->toEntry()); }; + auto appendDiscovery = [&](const Discovery::Ptr &discovery) { + if (discoveredNames.contains(discovery->udsName())) { + return; + } + discoveredNames << discovery->udsName(); + list.append(discovery->toEntry()); + }; auto maybeFinished = [&] { // finishes if all discoveries finished bool allFinished = true; for (auto discoverer : discoverers) { allFinished = allFinished && discoverer->isFinished(); } if (allFinished) { quitLoop(); } }; connect(&d, &DNSSDDiscoverer::newDiscovery, this, appendDiscovery); connect(&w, &WSDiscoverer::newDiscovery, this, appendDiscovery); connect(&d, &DNSSDDiscoverer::finished, this, maybeFinished); connect(&w, &WSDiscoverer::finished, this, maybeFinished); d.start(); w.start(); QTimer::singleShot(16000, &e, quitLoop); // max execution time! e.exec(); qCDebug(KIO_SMB_LOG) << "Modern discovery finished."; } if (dir_is_root) { udsentry.fastInsert(KIO::UDSEntry::UDS_FILE_TYPE, S_IFDIR); udsentry.fastInsert(KIO::UDSEntry::UDS_NAME, "."); udsentry.fastInsert(KIO::UDSEntry::UDS_ACCESS, (S_IRUSR | S_IRGRP | S_IROTH | S_IXUSR | S_IXGRP | S_IXOTH)); } else { udsentry.fastInsert(KIO::UDSEntry::UDS_NAME, "."); const int statErr = browse_stat_path(m_current_url, udsentry); if (statErr) { if (statErr == ENOENT || statErr == ENOTDIR) { reportWarning(m_current_url, statErr); } // Create a default UDSEntry if we could not stat the actual directory udsentry.fastInsert(KIO::UDSEntry::UDS_FILE_TYPE, S_IFDIR); udsentry.fastInsert(KIO::UDSEntry::UDS_ACCESS, (S_IRWXU | S_IRWXG | S_IROTH | S_IXOTH)); } } listEntry(udsentry); udsentry.clear(); // clean up smbc_closedir(dirfd); } else { if (errNum == EPERM || errNum == EACCES || workaroundEEXIST(errNum)) { const int passwordError = checkPassword(m_current_url); if (passwordError == KJob::NoError) { redirection(m_current_url); finished(); } else if (passwordError == KIO::ERR_USER_CANCELED) { reportError(m_current_url, errNum); } else { error(passwordError, m_current_url.toString()); } return; } reportError(m_current_url, errNum); return; } finished(); } void SMBSlave::fileSystemFreeSpace(const QUrl &url) { qCDebug(KIO_SMB_LOG) << url; // Avoid crashing in smbc_fstatvfs below when // requesting free space for smb:// which doesn't // make sense to do to begin with if (url.host().isEmpty()) { error(KIO::ERR_CANNOT_STAT, url.url()); return; } SMBUrl smbcUrl = url; struct statvfs dirStat { }; memset(&dirStat, 0, sizeof(struct statvfs)); const int err = smbc_statvfs(smbcUrl.toSmbcUrl().data(), &dirStat); if (err < 0) { error(KIO::ERR_CANNOT_STAT, url.url()); return; } // libsmb_stat.c has very awkward conditional branching that results // in data meaning different things based on context: // A samba host with unix extensions has f_frsize==0 and the f_bsize is // the actual block size. Any other server (such as windows) has a non-zero // f_frsize denoting the amount of sectors in a block and the f_bsize is // the amount of bytes in a sector. As such frsize*bsize is the actual // block size. // This was also broken in different ways throughout history, so depending // on the specific libsmbc versions the milage will vary. 4.7 to 4.11 are // at least behaving as described though. // https://bugs.kde.org/show_bug.cgi?id=298801 const auto frames = (dirStat.f_frsize == 0) ? 1 : dirStat.f_frsize; const auto blockSize = dirStat.f_bsize * frames; // Further more on older versions of samba f_bavail may not be set... const auto total = blockSize * dirStat.f_blocks; const auto available = blockSize * ((dirStat.f_bavail != 0) ? dirStat.f_bavail : dirStat.f_bfree); setMetaData("total", QString::number(total)); setMetaData("available", QString::number(available)); finished(); } bool SMBSlave::workaroundEEXIST(const int errNum) const { return (errNum == EEXIST) && m_enableEEXISTWorkaround; } diff --git a/smb/wsdiscoverer.cpp b/smb/wsdiscoverer.cpp index cdb644d4..b7cd421e 100644 --- a/smb/wsdiscoverer.cpp +++ b/smb/wsdiscoverer.cpp @@ -1,318 +1,323 @@ /* Copyright 2019-2020 Harald Sitter This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3 of the License or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of version 3 of the license. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include "wsdiscoverer.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include "kio_smb.h" // Publication service data resolver! // Specifically we'll ask the endpoint for PBSData via ws-transfer/Get. // The implementation is the bare minimum for our purposes! // https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-pbsd class PBSDResolver : public QObject { Q_OBJECT signals: void resolved(Discovery::Ptr discovery); public: /** * @param endpointUrl valid xaddr as advertised over ws-discovery (http://$ip/$referenceUuid) * @param destination endpoint reference urn as sent over ws-discovery ($referenceUuid) */ PBSDResolver(const QUrl &endpointUrl, const QString &destination, QObject *parent = nullptr) : QObject(parent) , m_endpointUrl(endpointUrl) , m_destination(destination) { } static QString nameFromComputerInfo(const QString &info) { // NB: spec says to use \ or / based on context, but in reality they are used // interchangibly in implementations. static QRegularExpression domainExpression("(?.+)[\\/]Domain:(?.+)"); static QRegularExpression workgroupExpression("(?.+)[\\/]Workgroup:(?.+)"); static QRegularExpression notJoinedExpression("(?.+)[\\/]NotJoined"); // We don't do anything with WG or domain info because windows10 doesn't seem to either. const auto joinedMatch = notJoinedExpression.match(info); if (joinedMatch.hasMatch()) { return joinedMatch.captured("name"); } const auto domainMatch = domainExpression.match(info); if (domainMatch.hasMatch()) { return domainMatch.captured("name"); } const auto workgroupMatch = workgroupExpression.match(info); if (workgroupMatch.hasMatch()) { return workgroupMatch.captured("name"); } return info; } // This must always set m_discovery and it must also time out on its own! void run() { // NB: when windows talks to windows they use lms:LargeMetadataSupport we probably don't // need this for the data we want, so it's left out. The actual messagse a windows // machine creates would be using "http://schemas.microsoft.com/windows/lms/2007/08" // as messageNamespace and set an additional header on the message. // https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-dpwssn/f700463d-cbbf-4545-ab47-b9a6fbf1ac7b KDSoapClientInterface client(m_endpointUrl.toString(), QStringLiteral("http://schemas.xmlsoap.org/ws/2004/09/transfer")); client.setSoapVersion(KDSoapClientInterface::SoapVersion::SOAP1_2); client.setTimeout(8000); KDSoapMessage message; KDSoapMessageAddressingProperties addressing; addressing.setAddressingNamespace(KDSoapMessageAddressingProperties::Addressing200408); addressing.setAction(QStringLiteral("http://schemas.xmlsoap.org/ws/2004/09/transfer/Get")); addressing.setMessageID(QStringLiteral("urn:uuid:") + QUuid::createUuid().toString(QUuid::WithoutBraces)); addressing.setDestination(m_destination); addressing.setReplyEndpointAddress(KDSoapMessageAddressingProperties::predefinedAddressToString( KDSoapMessageAddressingProperties::Anonymous, KDSoapMessageAddressingProperties::Addressing200408)); addressing.setSourceEndpointAddress(QStringLiteral("urn:uuid:") + QUuid::createUuid().toString(QUuid::WithoutBraces)); message.setMessageAddressingProperties(addressing); QString computer; KDSoapMessage response = client.call(QString(), message); if (response.isFault()) { qCDebug(KIO_SMB_LOG) << "Failed to obtain PBSD response" << m_endpointUrl.host() << m_destination << response.arguments() << response.faultAsString(); // No return! We'd disqualify systems that do not implement pbsd. } else { // The response xml would be nesting Metdata%1", m_endpointUrl.host()); } m_discovery.reset(new WSDiscovery(computer, m_endpointUrl.host())); emit resolved(m_discovery); } private: const QUrl m_endpointUrl; const QString m_destination; Discovery::Ptr m_discovery; }; // Utilizes WSDiscoveryClient to probe and resolve WSD services. WSDiscoverer::WSDiscoverer() : m_client(new WSDiscoveryClient(this)) { connect(m_client, &WSDiscoveryClient::probeMatchReceived, this, &WSDiscoverer::matchReceived); connect(m_client, &WSDiscoveryClient::resolveMatchReceived, this, &WSDiscoverer::resolveReceived); // If we haven't had a probematch in some seconds there's likely no more replies // coming and all hosts are known. Naturally resolvers may still be running and // get blocked on during stop(). Resolvers themselves have a timeout via // kdsoap. // NB: only started after first match! If we have no matches the slave will // stop us eventually anyway. m_probeMatchTimer.setInterval(2000); m_probeMatchTimer.setSingleShot(true); connect(&m_probeMatchTimer, &QTimer::timeout, this, &WSDiscoverer::stop); } void WSDiscoverer::start() { m_client->start(); // We only want devices. // We technically would probably also want to filter pub:Computer. // But! I am not sure if e.g. a NAS would publish itself as computer. // https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-pbsd KDQName type("wsdp:Device"); type.setNameSpace("http://schemas.xmlsoap.org/ws/2006/02/devprof"); m_client->sendProbe({type}, {}); } void WSDiscoverer::stop() { m_startedTimer = true; disconnect(&m_probeMatchTimer); m_probeMatchTimer.stop(); maybeFinish(); } bool WSDiscoverer::isFinished() const { return m_startedTimer && !m_probeMatchTimer.isActive() && m_resolvers.count() == m_resolvedCount; } void WSDiscoverer::matchReceived(const WSDiscoveryTargetService &matchedService) { // (re)start match timer to finish-early if at all possible. m_probeMatchTimer.start(); m_startedTimer = true; if (matchedService.xAddrList().isEmpty()) { // Has no addresses -> needs resolving still m_client->sendResolve(matchedService.endpointReference()); return; } resolveReceived(matchedService); } void WSDiscoverer::resolveReceived(const WSDiscoveryTargetService &service) { // (re)start match timer to finish-early if at all possible. m_probeMatchTimer.start(); m_startedTimer = true; if (m_seenEndpoints.contains(service.endpointReference())) { return; } m_seenEndpoints << service.endpointReference(); QUrl addr; for (const auto &xAddr : service.xAddrList()) { // https://docs.microsoft.com/en-us/windows/win32/wsdapi/xaddr-validation-rules // "At least one IP address included in the XAddrs (or IP address resolved from // a hostname included in the XAddrs) must be on the same subnet as the adapter // over which the ProbeMatches or ResolveMatches message was received." const auto hostInfo = QHostInfo::fromName(xAddr.host()); if (hostInfo.error() == QHostInfo::NoError) { addr = xAddr; break; } } if (addr.isEmpty()) { qCWarning(KIO_SMB_LOG) << "Failed to resolve any WS transport address." << "This suggests that DNS resolution may be broken." << service.xAddrList(); return; } PBSDResolver *resolver = new PBSDResolver(addr, service.endpointReference(), this); connect(resolver, &PBSDResolver::resolved, this, [this](Discovery::Ptr discovery) { ++m_resolvedCount; emit newDiscovery(discovery); maybeFinish(); }); QTimer::singleShot(0, resolver, &PBSDResolver::run); m_resolvers << resolver; } void WSDiscoverer::maybeFinish() { if (isFinished()) { emit finished(); } } WSDiscovery::WSDiscovery(const QString &computer, const QString &remote) : m_computer(computer) , m_remote(remote) { } +QString WSDiscovery::udsName() const +{ + return m_computer; +} + KIO::UDSEntry WSDiscovery::toEntry() const { KIO::UDSEntry entry; - entry.fastInsert(KIO::UDSEntry::UDS_NAME, m_computer); + entry.fastInsert(KIO::UDSEntry::UDS_NAME, udsName()); entry.fastInsert(KIO::UDSEntry::UDS_FILE_TYPE, S_IFDIR); entry.fastInsert(KIO::UDSEntry::UDS_ACCESS, (S_IRUSR | S_IXUSR | S_IRGRP | S_IXGRP | S_IROTH | S_IXOTH)); entry.fastInsert(KIO::UDSEntry::UDS_ICON_NAME, "network-server"); QUrl u; u.setScheme(QStringLiteral("smb")); u.setHost(m_remote); u.setPath("/"); // https://bugs.kde.org/show_bug.cgi?id=388922 entry.fastInsert(KIO::UDSEntry::UDS_URL, u.url()); entry.fastInsert(KIO::UDSEntry::UDS_MIME_TYPE, QStringLiteral("application/x-smb-server")); return entry; } #include "wsdiscoverer.moc" diff --git a/smb/wsdiscoverer.h b/smb/wsdiscoverer.h index 712398e1..840366c2 100644 --- a/smb/wsdiscoverer.h +++ b/smb/wsdiscoverer.h @@ -1,77 +1,77 @@ /* Copyright 2019-2020 Harald Sitter This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3 of the License or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of version 3 of the license. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #ifndef WSDISCOVERER_H #define WSDISCOVERER_H #include "discovery.h" #include #include class WSDiscoveryClient; class WSDiscoveryTargetService; class PBSDResolver; namespace KIO { class UDSEntry; } class WSDiscovery : public Discovery { const QString m_computer; const QString m_remote; public: WSDiscovery(const QString &computer, const QString &remote); - + QString udsName() const override; KIO::UDSEntry toEntry() const override; }; class WSDiscoverer : public QObject, public Discoverer { Q_OBJECT public: WSDiscoverer(); void start() override; bool isFinished() const override; signals: void newDiscovery(Discovery::Ptr discovery) override; void finished() override; private slots: void matchReceived(const WSDiscoveryTargetService &matchedService); void resolveReceived(const WSDiscoveryTargetService &matchedService); private: void stop() override; void maybeFinish(); WSDiscoveryClient *m_client = nullptr; bool m_startedTimer = false; QTimer m_probeMatchTimer; QStringList m_seenEndpoints; QList m_resolvers; int m_resolvedCount = 0; }; #endif // WSDISCOVERER_H