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 §ion : 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"