diff --git a/smb/dnssddiscoverer.h b/smb/dnssddiscoverer.h --- a/smb/dnssddiscoverer.h +++ b/smb/dnssddiscoverer.h @@ -31,11 +31,12 @@ class DNSSDDiscovery : public Discovery { public: - DNSSDDiscovery(KDNSSD::RemoteService::Ptr service); + DNSSDDiscovery(KDNSSD::RemoteService::Ptr service, const QString &deviceModel); KIO::UDSEntry toEntry() const override; private: KDNSSD::RemoteService::Ptr m_service; + QString m_deviceModel; }; class DNSSDDiscoverer : public QObject, public Discoverer diff --git a/smb/dnssddiscoverer.cpp b/smb/dnssddiscoverer.cpp --- a/smb/dnssddiscoverer.cpp +++ b/smb/dnssddiscoverer.cpp @@ -21,19 +21,41 @@ #include "dnssddiscoverer.h" #include "kio_smb.h" -DNSSDDiscovery::DNSSDDiscovery(KDNSSD::RemoteService::Ptr service) +#include +#include +#include +#include + +DNSSDDiscovery::DNSSDDiscovery(KDNSSD::RemoteService::Ptr service, const QString &deviceModel) : m_service(service) + , m_deviceModel(deviceModel) { } KIO::UDSEntry DNSSDDiscovery::toEntry() const { KIO::UDSEntry entry; - entry.fastInsert(KIO::UDSEntry::UDS_NAME, m_service->serviceName()); +#pragma message "FIXME for debugging name is set to model!" + if (m_deviceModel.isEmpty()) { + entry.fastInsert(KIO::UDSEntry::UDS_NAME, m_service->serviceName()); + } else { + entry.fastInsert(KIO::UDSEntry::UDS_NAME, m_deviceModel); + } 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"); + QString icon = "network-server"; +#pragma message "this wont work the model may include versioning at the end MacBookPro13,1" + if (!m_deviceModel.isEmpty()) { + icon = "network-server-model-" + m_deviceModel.toLower(); + } +#pragma message "FIXME manual map hack" + // Known models https://www.tumfatig.net/20170607/let-mac-os-auto-discover-your-smb-shares/ + if (m_deviceModel.startsWith("MacBook")) { // Manual map for missing icons! + icon = "computer-laptop"; + } +#pragma message "FIXME should map MacSamba to something neat" + entry.fastInsert(KIO::UDSEntry::UDS_ICON_NAME, icon); // 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 @@ -57,16 +79,151 @@ return entry; } +/** + * Resolves the model of a dnssd remote. + * OSX has this in the TXT DNS entries which we do not find as + * ordinary services. This is a huge hack to model avahi's RecordBrowser + * and get access to the raw data underneath ServiceBrowser + */ +class DeviceInfoResolver : public QObject +{ + Q_OBJECT + + /** DNS record types, see RFC 1035 */ + enum { + AVAHI_DNS_TYPE_A = 0x01, + AVAHI_DNS_TYPE_NS = 0x02, + AVAHI_DNS_TYPE_CNAME = 0x05, + AVAHI_DNS_TYPE_SOA = 0x06, + AVAHI_DNS_TYPE_PTR = 0x0C, + AVAHI_DNS_TYPE_HINFO = 0x0D, + AVAHI_DNS_TYPE_MX = 0x0F, + AVAHI_DNS_TYPE_TXT = 0x10, + AVAHI_DNS_TYPE_AAAA = 0x1C, + AVAHI_DNS_TYPE_SRV = 0x21 + }; + + /** DNS record classes, see RFC 1035 */ + enum { + AVAHI_DNS_CLASS_IN = 0x01 /**< Probably the only class we will ever use */ + }; + +public: + explicit DeviceInfoResolver(KDNSSD::RemoteService::Ptr service, QObject *parent = nullptr) + : QObject(parent) + , m_service(service) + { + // Hack adopted from kdnssd: avahi has severe reentrancy problems and fires events + // as soon as it created the browser, before we even had a chance to listen to it. + // Instead listen to all events and filter them on our end. + QDBusConnection::systemBus() + .connect("org.freedesktop.Avahi", + "", + "org.freedesktop.Avahi.RecordBrowser", + "ItemNew", + this, + SLOT(gotGlobalItemRemove(int,int,QString,ushort,ushort,QByteArray,uint,QDBusMessage))); + QDBusConnection::systemBus() + .connect("org.freedesktop.Avahi", + "", + "org.freedesktop.Avahi.RecordBrowser", + "AllForNow", + this, + SLOT(gotGlobalAllForNow(QDBusMessage))); + + } + + void start() + { + QDBusInterface interface(QStringLiteral("org.freedesktop.Avahi"), + QStringLiteral("/"), + QStringLiteral("org.freedesktop.Avahi.Server"), + QDBusConnection::systemBus()); + + // OSX carriers the device-information in TXT DNS records for $prettyName + // of the format "me's iMac._device-info._tcp.local" + // Linuxes and OSX user overrides carry the information as regular services of + // the type _device-info._tcp as well, but we can access that thorugh the record browser + // the same way. + QDBusReply reply = interface.call("RecordBrowserNew", -1, -1, + m_service->serviceName() + "._device-info._tcp." + m_service->domain(), + QVariant::fromValue(AVAHI_DNS_CLASS_IN), + QVariant::fromValue(AVAHI_DNS_TYPE_TXT), + QVariant::fromValue(0)); + if (reply.isValid()) { + m_path = reply.value().path(); + } + } + +public slots: + void gotGlobalItemRemove(int iface, int proto, + QString name, ushort clazz, ushort type, + QByteArray recordData, uint flags, QDBusMessage msg) + { + qDebug() << "record" + << iface << proto << name << clazz << type + << recordData + << flags; + if (m_path.isEmpty() || m_path != msg.path()) { + return; + } + + QMap map; + if (!recordData.isEmpty()) { + // Most excitedly we get TXT data back. That is: the first 8bit + // contains the amount of bits comprising the field, followed + // by those bits. Rinse and repeat. + // e.g. '(SO)model=iMac10,1(LF)osxvers=18' + // (0xE = len 14) + // model=iMac10,1 + // (0xA = len 10) + // osxvers=18 + const char *data = recordData.constData(); + int offset = 0; + while (offset < recordData.size()) { + char len = data[offset]; + QString str = QString::fromUtf8(data + offset + 1, len); + const int separatorIndex = str.indexOf('='); + if (separatorIndex < 0) { + continue; + } + map[str.mid(0, separatorIndex)] = str.mid(separatorIndex + 1); + offset += len + 1; + } + } + + m_path.clear(); // Makes further signals noop. + deleteLater(); + emit resolved(Discovery::Ptr(new DNSSDDiscovery(m_service, map.value("model")))); + } + + void gotGlobalAllForNow(QDBusMessage msg) + { + // Possibly don't need this, if we get a resolution we'll resolved(), + // otherwise we'll hit a hard timeout eventually and get killed. + Q_UNUSED(msg) + } + +signals: + void resolved(Discovery::Ptr); + +private: + QString m_path; + KDNSSD::RemoteService::Ptr m_service; +}; + DNSSDDiscoverer::DNSSDDiscoverer() { + qDebug() << "DISCO"; 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(); + << service->port() + << service->textData(); // 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 @@ -79,9 +236,10 @@ connect(service.data(), &KDNSSD::RemoteService::resolved, this, [=] { - ++m_resolvedCount; - emit newDiscovery(Discovery::Ptr(new DNSSDDiscovery(service))); - maybeFinish(); +#pragma message "FIXME: needs additional timeouts? if the resolver gets stuck for whatever reason we'll lose the discovery" + auto resolver = new DeviceInfoResolver(service); + connect(resolver, &DeviceInfoResolver::resolved, this, &DNSSDDiscoverer::newDiscovery); + resolver->start(); }); // Schedule resolution of hostname. We'll later call resolve @@ -116,3 +274,5 @@ emit finished(); } } + +#include "dnssddiscoverer.moc"