File indexing completed on 2024-04-28 04:58:02

0001 /*
0002     SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
0003     SPDX-FileCopyrightText: 2019-2021 Harald Sitter <sitter@kde.org>
0004 */
0005 
0006 #include "wsdiscoverer.h"
0007 
0008 #include <QCoreApplication>
0009 #include <QDebug>
0010 #include <QHostInfo>
0011 #include <QRegularExpression>
0012 #include <QUuid>
0013 
0014 #include <WSDiscoveryClient>
0015 #include <WSDiscoveryTargetService>
0016 
0017 #include <KDSoapClient/KDSoapClientInterface>
0018 #include <KDSoapClient/KDSoapMessage>
0019 #include <KDSoapClient/KDSoapMessageAddressingProperties>
0020 #include <KDSoapClient/KDSoapNamespaceManager>
0021 
0022 #include <KIO/UDSEntry>
0023 #include <KLocalizedString>
0024 
0025 #include <chrono>
0026 
0027 #include "kio_smb.h"
0028 
0029 using namespace std::chrono_literals;
0030 
0031 // http://docs.oasis-open.org/ws-dd/dpws/wsdd-dpws-1.1-spec.html
0032 // WSD itself defines shorter timeouts. We follow DPWS instead because Windows 10 actually speaks DPWS, so it seems
0033 // prudent to follow its presumed internal limits.
0034 // - discard Probe & ResolveMatch N seconds after corresponding Probe:
0035 constexpr auto MATCH_TIMEOUT = 10s;
0036 
0037 // Not specified default value for HTTP timeouts. We could go with a default 120s or 600s timeout but that seems
0038 // a bit excessive. We only use SOAP over HTTP for device PBSD resolution.
0039 constexpr auto HTTP_TIMEOUT = 20s;
0040 
0041 // Publication service data resolver!
0042 // Specifically we'll ask the endpoint for PBSData via ws-transfer/Get.
0043 // The implementation is the bare minimum for our purposes!
0044 // https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-pbsd
0045 class PBSDResolver : public QObject
0046 {
0047     Q_OBJECT
0048 
0049 Q_SIGNALS:
0050     void resolved(Discovery::Ptr discovery);
0051 
0052 public:
0053     /**
0054      * @param endpointUrl valid xaddr as advertised over ws-discovery (http://$ip/$referenceUuid)
0055      * @param destination endpoint reference urn as sent over ws-discovery ($referenceUuid)
0056      */
0057     PBSDResolver(const QUrl &endpointUrl, const QString &destination, QObject *parent = nullptr)
0058         : QObject(parent)
0059         , m_endpointUrl(endpointUrl)
0060         , m_destination(destination)
0061     {
0062     }
0063 
0064     static QString nameFromComputerInfo(const QString &info)
0065     {
0066         // NB: spec says to use \ or / based on context, but in reality they are used
0067         //    interchangeably in implementations.
0068         static QRegularExpression domainExpression("(?<name>.+)[\\/]Domain:(?<domain>.+)");
0069         static QRegularExpression workgroupExpression("(?<name>.+)[\\/]Workgroup:(?<workgroup>.+)");
0070         static QRegularExpression notJoinedExpression("(?<name>.+)[\\/]NotJoined");
0071 
0072         // We don't do anything with WG or domain info because windows10 doesn't seem to either.
0073         const auto joinedMatch = notJoinedExpression.match(info);
0074         if (joinedMatch.hasMatch()) {
0075             return joinedMatch.captured("name");
0076         }
0077 
0078         const auto domainMatch = domainExpression.match(info);
0079         if (domainMatch.hasMatch()) {
0080             return domainMatch.captured("name");
0081         }
0082 
0083         const auto workgroupMatch = workgroupExpression.match(info);
0084         if (workgroupMatch.hasMatch()) {
0085             return workgroupMatch.captured("name");
0086         }
0087 
0088         return info;
0089     }
0090 
0091     // This must always set m_discovery and it must also time out on its own!
0092     void run()
0093     {
0094         // NB: when windows talks to windows they use lms:LargeMetadataSupport we probably don't
0095         // need this for the data we want, so it's left out. The actual messages a windows
0096         // machine creates would be using "http://schemas.microsoft.com/windows/lms/2007/08"
0097         // as messageNamespace and set an additional header <LargeMetadataSupport/> on the message.
0098         // https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-dpwssn/f700463d-cbbf-4545-ab47-b9a6fbf1ac7b
0099 
0100         KDSoapClientInterface client(m_endpointUrl.toString(), QStringLiteral("http://schemas.xmlsoap.org/ws/2004/09/transfer"));
0101         client.setSoapVersion(KDSoapClientInterface::SoapVersion::SOAP1_2);
0102         client.setTimeout(std::chrono::milliseconds(HTTP_TIMEOUT).count());
0103 
0104         KDSoapMessage message;
0105         KDSoapMessageAddressingProperties addressing;
0106         addressing.setAddressingNamespace(KDSoapMessageAddressingProperties::Addressing200408);
0107         addressing.setAction(QStringLiteral("http://schemas.xmlsoap.org/ws/2004/09/transfer/Get"));
0108         addressing.setMessageID(QStringLiteral("urn:uuid:") + QUuid::createUuid().toString(QUuid::WithoutBraces));
0109         addressing.setDestination(m_destination);
0110         addressing.setReplyEndpointAddress(KDSoapMessageAddressingProperties::predefinedAddressToString(KDSoapMessageAddressingProperties::Anonymous,
0111                                                                                                         KDSoapMessageAddressingProperties::Addressing200408));
0112         addressing.setSourceEndpointAddress(QStringLiteral("urn:uuid:") + QUuid::createUuid().toString(QUuid::WithoutBraces));
0113         message.setMessageAddressingProperties(addressing);
0114 
0115         QString computer;
0116 
0117         KDSoapMessage response = client.call(QString(), message);
0118         if (response.isFault()) {
0119             qCDebug(KIO_SMB_LOG) << "Failed to obtain PBSD response" << m_endpointUrl.host() << m_destination << response.arguments()
0120                                  << response.faultAsString();
0121             // No return! We'd disqualify systems that do not implement pbsd.
0122         } else {
0123             // The response xml would be nesting Metdata<MetadataSection<Relationship<Host<Computer
0124             // https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-pbsd/ec0810ba-2427-46f5-8d47-cc94919ee4c1
0125             // The value therein is $netbiosname/Domain:$domain or $netbiosname\Workgroup:$workgroup or $netbiosname\NotJoined
0126             // (yes, that is either a forward or backward slash!). In practice everyone uses a / for everything though!
0127             // For simplicity's sake we'll manually pop the value (or empty) out, if we get a name it's grand
0128             // otherwise we'll attempt reverse resolution from the IP (which ideally would yield results
0129             // over systemd-resolved's llmnr).
0130 
0131             const auto childValues = response.childValues();
0132             for (const auto &section : qAsConst(childValues)) {
0133                 computer = section.childValues().child("Relationship").childValues().child("Host").childValues().child("Computer").value().toString();
0134                 if (!computer.isEmpty()) {
0135                     break;
0136                 }
0137             }
0138 
0139             computer = nameFromComputerInfo(computer);
0140         }
0141 
0142         if (computer.isEmpty()) {
0143             // Chances are if we get here the remote doesn't implement PBSD.
0144             // Shouldn't really happen. Seeing as we got a return message
0145             // for our PBSD request however we'll assume the implementation is
0146             // just misbehaving and will assume it supports SMB all the same.
0147 
0148             // As a shot in the dark try to resolver via QHostInfo (which ideally
0149             // does a LMNR lookup via libc/systemd)
0150             auto hostInfo = QHostInfo::fromName(m_endpointUrl.host());
0151             if (hostInfo.error() == QHostInfo::NoError) {
0152                 computer = hostInfo.hostName();
0153             } else {
0154                 qCWarning(KIO_SMB_LOG) << "failed to resolve host for endpoint url:" << m_endpointUrl;
0155             }
0156         }
0157 
0158         // Default the host to the IP unless we have a prettyName in which case we use
0159         // the presumed DNSSD name prettyName.local. listDir will redirect this to the
0160         // LLMNR variant (without .local) should it turn out not resolvable.
0161         QString host = m_endpointUrl.host();
0162         if (computer.isEmpty()) {
0163             computer = xi18nc("host entry when no pretty name is available. %1 likely is an IP address", "Unknown Device @ <resource>%1</resource>", host);
0164         } else {
0165             // If we got a DNSSD name, use that, otherwise redirect to on-demand resolution.
0166             host = computer.endsWith(".local") ? computer : computer + ".kio-discovery-wsd";
0167         }
0168 
0169         m_discovery.reset(new WSDiscovery(computer, host));
0170         Q_EMIT resolved(m_discovery);
0171     }
0172 
0173 private:
0174     const QUrl m_endpointUrl;
0175     const QString m_destination;
0176     Discovery::Ptr m_discovery;
0177 };
0178 
0179 // Wraps a /Resolve request. Resolves are subject to timeouts which is implemented by only waiting for a reply until
0180 // the internal timeout is hit and then deleting the resolver.
0181 class WSDResolver : public QObject
0182 {
0183     Q_OBJECT
0184 public:
0185     explicit WSDResolver(const QString &endpoint, QObject *parent = nullptr)
0186         : QObject(parent)
0187         , m_endpoint(endpoint)
0188     {
0189         connect(&m_client, &WSDiscoveryClient::resolveMatchReceived, this, [this](const WSDiscoveryTargetService &service) {
0190             Q_ASSERT(service.endpointReference() == m_endpoint);
0191             Q_EMIT resolved(service);
0192             stop();
0193         });
0194 
0195         m_stopTimer.setInterval(MATCH_TIMEOUT); // R4066 of DPWS spec
0196         m_stopTimer.setSingleShot(true);
0197         connect(&m_stopTimer, &QTimer::timeout, this, &WSDResolver::stop);
0198     }
0199 
0200 public Q_SLOTS:
0201     void start()
0202     {
0203         m_client.sendResolve(m_endpoint);
0204         m_stopTimer.start();
0205     }
0206 
0207     void stop()
0208     {
0209         m_stopTimer.stop();
0210         disconnect(&m_stopTimer);
0211         Q_EMIT stopped();
0212     }
0213 
0214 Q_SIGNALS:
0215     void resolved(const WSDiscoveryTargetService &service);
0216     void stopped();
0217 
0218 private:
0219     const QString m_endpoint;
0220     WSDiscoveryClient m_client;
0221     QTimer m_stopTimer;
0222 };
0223 
0224 // Utilizes WSDiscoveryClient to probe and resolve WSD services.
0225 WSDiscoverer::WSDiscoverer()
0226     : m_client(new WSDiscoveryClient(this))
0227 {
0228     connect(m_client, &WSDiscoveryClient::probeMatchReceived, this, &WSDiscoverer::matchReceived);
0229 
0230     // Matches may only arrive within a given time period afterwards we no
0231     // longer care as per the spec. stopping is further contigent on all
0232     // resolvers having finished though (they each have timeouts as well).
0233     m_probeMatchTimer.setInterval(MATCH_TIMEOUT);
0234     m_probeMatchTimer.setSingleShot(true);
0235     connect(&m_probeMatchTimer, &QTimer::timeout, this, &WSDiscoverer::stop);
0236 }
0237 
0238 WSDiscoverer::~WSDiscoverer()
0239 {
0240     qDeleteAll(m_resolvers);
0241     qDeleteAll(m_endpointResolvers);
0242 }
0243 
0244 void WSDiscoverer::start()
0245 {
0246     m_client->start();
0247 
0248     // We only want devices.
0249     // We technically would probably also want to filter pub:Computer.
0250     // But! I am not sure if e.g. a NAS would publish itself as computer.
0251     // https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-pbsd
0252     KDQName type("wsdp:Device");
0253     type.setNameSpace("http://schemas.xmlsoap.org/ws/2006/02/devprof");
0254     m_client->sendProbe({type}, {});
0255 
0256     // (re)start match timer to finish-early if at all possible.
0257     m_probeMatchTimer.start();
0258     m_startedTimer = true;
0259 }
0260 
0261 void WSDiscoverer::stop()
0262 {
0263     m_startedTimer = true;
0264     disconnect(&m_probeMatchTimer);
0265     m_probeMatchTimer.stop();
0266     maybeFinish();
0267 }
0268 
0269 bool WSDiscoverer::isFinished() const
0270 {
0271     const bool notProbing = !m_probeMatchTimer.isActive();
0272     const bool notWaitingForWSDResolve = m_endpointResolvers.isEmpty();
0273     const bool notWaitingForPBSD = m_resolvers.count() == m_resolvedCount;
0274     return m_startedTimer && (notProbing && notWaitingForWSDResolve && notWaitingForPBSD);
0275 }
0276 
0277 void WSDiscoverer::matchReceived(const WSDiscoveryTargetService &matchedService)
0278 {
0279     if (!m_probeMatchTimer.isActive()) { // R4065 of DPWS spec
0280         qCWarning(KIO_SMB_LOG) << "match received too late" << matchedService.endpointReference();
0281         return;
0282     }
0283 
0284     if (matchedService.xAddrList().isEmpty()) { // Has no addresses -> needs resolving still
0285         const QString endpoint = matchedService.endpointReference();
0286         if (m_seenEndpoints.contains(endpoint) || m_endpointResolvers.contains(endpoint)) {
0287             return;
0288         }
0289 
0290         auto resolver = new WSDResolver(endpoint, this);
0291         connect(resolver, &WSDResolver::resolved, this, &WSDiscoverer::resolveReceived);
0292         connect(resolver, &WSDResolver::stopped, this, [this, endpoint] {
0293             if (m_endpointResolvers.contains(endpoint)) {
0294                 m_endpointResolvers.take(endpoint)->deleteLater();
0295             }
0296             maybeFinish();
0297         });
0298         m_endpointResolvers.insert(endpoint, resolver);
0299         resolver->start();
0300 
0301         return;
0302     }
0303 
0304     resolveReceived(matchedService);
0305 }
0306 
0307 void WSDiscoverer::resolveReceived(const WSDiscoveryTargetService &service)
0308 {
0309     if (m_seenEndpoints.contains(service.endpointReference())) {
0310         return;
0311     }
0312     m_seenEndpoints << service.endpointReference();
0313 
0314     QUrl addr;
0315     const QList<QUrl> xAddrList = service.xAddrList();
0316     for (const auto &xAddr : xAddrList) {
0317         // https://docs.microsoft.com/en-us/windows/win32/wsdapi/xaddr-validation-rules
0318         // "At least one IP address included in the XAddrs (or IP address resolved from
0319         // a hostname included in the XAddrs) must be on the same subnet as the adapter
0320         // over which the ProbeMatches or ResolveMatches message was received."
0321         const auto hostInfo = QHostInfo::fromName(xAddr.host());
0322         if (hostInfo.error() == QHostInfo::NoError) {
0323             addr = xAddr;
0324             break;
0325         }
0326     }
0327 
0328     if (addr.isEmpty()) {
0329         qCWarning(KIO_SMB_LOG) << "Failed to resolve any WS transport address."
0330                                << "This suggests that DNS resolution may be broken." << service.xAddrList();
0331         return;
0332     }
0333 
0334     auto *resolver = new PBSDResolver(addr, service.endpointReference(), this);
0335     connect(resolver, &PBSDResolver::resolved, this, [this](Discovery::Ptr discovery) {
0336         ++m_resolvedCount;
0337         Q_EMIT newDiscovery(discovery);
0338         maybeFinish();
0339     });
0340     QTimer::singleShot(0, resolver, &PBSDResolver::run);
0341     m_resolvers << resolver;
0342 }
0343 
0344 void WSDiscoverer::maybeFinish()
0345 {
0346     if (isFinished()) {
0347         Q_EMIT finished();
0348     }
0349 }
0350 
0351 WSDiscovery::WSDiscovery(const QString &computer, const QString &remote)
0352     : m_computer(computer)
0353     , m_remote(remote)
0354 {
0355 }
0356 
0357 QString WSDiscovery::udsName() const
0358 {
0359     return m_computer;
0360 }
0361 
0362 KIO::UDSEntry WSDiscovery::toEntry() const
0363 {
0364     KIO::UDSEntry entry;
0365     const int fastInsertCount = 6;
0366     entry.reserve(fastInsertCount);
0367     entry.fastInsert(KIO::UDSEntry::UDS_NAME, udsName());
0368 
0369     entry.fastInsert(KIO::UDSEntry::UDS_FILE_TYPE, S_IFDIR);
0370     entry.fastInsert(KIO::UDSEntry::UDS_ACCESS, (S_IRUSR | S_IXUSR | S_IRGRP | S_IXGRP | S_IROTH | S_IXOTH));
0371     entry.fastInsert(KIO::UDSEntry::UDS_ICON_NAME, "network-server");
0372 
0373     QUrl u;
0374     u.setScheme(QStringLiteral("smb"));
0375     u.setHost(m_remote);
0376     u.setPath("/"); // https://bugs.kde.org/show_bug.cgi?id=388922
0377 
0378     entry.fastInsert(KIO::UDSEntry::UDS_URL, u.url());
0379     entry.fastInsert(KIO::UDSEntry::UDS_MIME_TYPE, QStringLiteral("application/x-smb-server"));
0380     return entry;
0381 }
0382 
0383 #include "moc_wsdiscoverer.cpp"
0384 #include "wsdiscoverer.moc"