/* * Copyright (C) 2007 Justin Karneges * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser 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 library 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 Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * 02110-1301 USA * */ #include "irisnetplugin.h" #include #include #include "qdnssd.h" // for ntohl #ifdef Q_OS_WIN # include #else # include #endif static QByteArray nameToDottedString(const QByteArray &in) { QByteArray out; int at = 0; while(at < in.size()) { int len = in[at++]; if(len > 0) out += in.mid(at, len); out += '.'; at += len; } return out; } static QMap textsToAttribs(const QList &texts) { QMap out; foreach(const QByteArray &a, texts) { QString key; QByteArray value; int x = a.indexOf('='); if(x != -1) { key = QString::fromLatin1(a.mid(0, x)); value = a.mid(x + 1); } else { key = QString::fromLatin1(a); } out.insert(key, value); } return out; } static QByteArray attribsToTxtRecord(const QMap &attribs) { QList texts; QMapIterator it(attribs); while(it.hasNext()) { it.next(); QByteArray line = it.key().toLatin1() + '=' + it.value(); texts += line; } return QDnsSd::createTxtRecord(texts); } // returns a list of 3 items, or an empty list on error static QList nameToInstanceParts(const QByteArray &name) { // FIXME: improve this parsing... (what about escaping??) int at = name.indexOf('.'); QByteArray sname = name.mid(0, at); ++at; int next = name.indexOf('.', at); ++next; next = name.indexOf('.', next); QByteArray stype = name.mid(at, next - at); at = next + 1; QByteArray sdomain = name.mid(at); QList out; out += sname; out += stype; out += sdomain; return out; } static XMPP::NameRecord importQDnsSdRecord(const QDnsSd::Record &in) { XMPP::NameRecord out; switch(in.rrtype) { case 1: // A { quint32 *p = (quint32 *)in.rdata.data(); out.setAddress(QHostAddress(ntohl(*p))); } break; case 28: // AAAA { out.setAddress(QHostAddress((quint8 *)in.rdata.data())); } break; case 12: // PTR { out.setPtr(nameToDottedString(in.rdata)); } break; case 10: // NULL { out.setNull(in.rdata); } break; case 16: // TXT { QList txtEntries = QDnsSd::parseTxtRecord(in.rdata); if(txtEntries.isEmpty()) return out; out.setTxt(txtEntries); } break; default: // unsupported { return out; } } out.setOwner(in.name); out.setTtl(in.ttl); return out; } namespace { class QDnsSdDelegate { public: virtual ~QDnsSdDelegate() { } virtual void dns_queryResult(int id, const QDnsSd::QueryResult &result) { Q_UNUSED(id); Q_UNUSED(result); } virtual void dns_browseResult(int id, const QDnsSd::BrowseResult &result) { Q_UNUSED(id); Q_UNUSED(result); } virtual void dns_resolveResult(int id, const QDnsSd::ResolveResult &result) { Q_UNUSED(id); Q_UNUSED(result); } virtual void dns_regResult(int id, const QDnsSd::RegResult &result) { Q_UNUSED(id); Q_UNUSED(result); } }; class IdManager { private: QSet set; int at; inline void bump_at() { if(at == 0x7fffffff) at = 0; else ++at; } public: IdManager() : at(0) { } int reserveId() { while(1) { if(!set.contains(at)) { int id = at; set.insert(id); bump_at(); return id; } bump_at(); } } void releaseId(int id) { set.remove(id); } }; } //---------------------------------------------------------------------------- // AppleProvider //---------------------------------------------------------------------------- class AppleProvider : public XMPP::IrisNetProvider { Q_OBJECT Q_INTERFACES(XMPP::IrisNetProvider); public: QDnsSd dns; QHash delegateById; AppleProvider() : dns(this) { connect(&dns, SIGNAL(queryResult(int,QDnsSd::QueryResult)), SLOT(dns_queryResult(int,QDnsSd::QueryResult))); connect(&dns, SIGNAL(browseResult(int,QDnsSd::BrowseResult)), SLOT(dns_browseResult(int,QDnsSd::BrowseResult))); connect(&dns, SIGNAL(resolveResult(int,QDnsSd::ResolveResult)), SLOT(dns_resolveResult(int,QDnsSd::ResolveResult))); connect(&dns, SIGNAL(regResult(int,QDnsSd::RegResult)), SLOT(dns_regResult(int,QDnsSd::RegResult))); } virtual XMPP::NameProvider *createNameProviderInternet(); virtual XMPP::NameProvider *createNameProviderLocal(); virtual XMPP::ServiceProvider *createServiceProvider(); int query(QDnsSdDelegate *p, const QByteArray &name, int qType) { int id = dns.query(name, qType); delegateById[id] = p; return id; } int browse(QDnsSdDelegate *p, const QByteArray &serviceType, const QByteArray &domain) { int id = dns.browse(serviceType, domain); delegateById[id] = p; return id; } int resolve(QDnsSdDelegate *p, const QByteArray &serviceName, const QByteArray &serviceType, const QByteArray &domain) { int id = dns.resolve(serviceName, serviceType, domain); delegateById[id] = p; return id; } int reg(QDnsSdDelegate *p, const QByteArray &serviceName, const QByteArray &serviceType, const QByteArray &domain, int port, const QByteArray &txtRecord) { int id = dns.reg(serviceName, serviceType, domain, port, txtRecord); delegateById[id] = p; return id; } void stop(int id) { delegateById.remove(id); dns.stop(id); } void stop_all(QDnsSdDelegate *p) { QList ids; QHashIterator it(delegateById); while(it.hasNext()) { it.next(); if(it.value() == p) ids += it.key(); } foreach(int id, ids) stop(id); } private slots: void dns_queryResult(int id, const QDnsSd::QueryResult &result) { delegateById[id]->dns_queryResult(id, result); } void dns_browseResult(int id, const QDnsSd::BrowseResult &result) { delegateById[id]->dns_browseResult(id, result); } void dns_resolveResult(int id, const QDnsSd::ResolveResult &result) { delegateById[id]->dns_resolveResult(id, result); } void dns_regResult(int id, const QDnsSd::RegResult &result) { delegateById[id]->dns_regResult(id, result); } }; //---------------------------------------------------------------------------- // AppleBrowseSession //---------------------------------------------------------------------------- // only use this class for a single browse. if you want to browse again, // create a new object. class AppleBrowse : public QObject, public QDnsSdDelegate { Q_OBJECT public: AppleProvider *global; int browse_id; QList instances; QHash pendingByQueryId; // waiting for TXT AppleBrowse(AppleProvider *_global, QObject *parent = 0) : QObject(parent), global(_global), browse_id(-1) { connect(this, SIGNAL(unavailable_p(XMPP::ServiceInstance)), SIGNAL(unavailable(XMPP::ServiceInstance))); } ~AppleBrowse() { global->stop_all(this); } void browse(const QString &type, const QString &domain) { browse_id = global->browse(this, type.toUtf8(), domain.toUtf8()); } signals: void available(const XMPP::ServiceInstance &instance); void unavailable(const XMPP::ServiceInstance &instance); void error(); // emit delayed void unavailable_p(const XMPP::ServiceInstance &instance); protected: virtual void dns_browseResult(int id, const QDnsSd::BrowseResult &result) { Q_UNUSED(id); if(!result.success) { emit error(); return; } foreach(const QDnsSd::BrowseEntry &e, result.entries) { XMPP::ServiceInstance si(e.serviceName, e.serviceType, e.replyDomain, QMap()); if(e.added) { int query_id = global->query(this, si.name(), 16); // 16 == TXT pendingByQueryId[query_id] = si.name(); } else // removed { // emit these queued for SS. no worry of SR since // the browse operation is not cancellable. for(int n = 0; n < instances.count(); ++n) { const XMPP::ServiceInstance &i = instances[n]; if(i.name() == si.name()) { emit unavailable_p(i); instances.removeAt(n); --n; // adjust position } } } } } virtual void dns_queryResult(int id, const QDnsSd::QueryResult &result) { if(!result.success) { // if we get here, then it means we received a browse // entry, but could not fetch its TXT record. if // that happens, cancel the query and drop the // browse entry. global->stop(id); pendingByQueryId.remove(id); return; } // qdnssd guarantees at least one answer Q_ASSERT(!result.records.isEmpty()); // only the first entry matters, and it must be an added TXT if(!result.records[0].added || result.records[0].rrtype != 16) return; // we only care about one answer QByteArray name = pendingByQueryId[id]; QList parts = nameToInstanceParts(name); if(parts.isEmpty()) { // TODO: error Q_ASSERT(0); } global->stop(id); pendingByQueryId.remove(id); XMPP::NameRecord rec = importQDnsSdRecord(result.records[0]); // bad answer? if(rec.isNull()) return; QMap attribs = textsToAttribs(rec.texts()); // FIXME: conversion/escaping? XMPP::ServiceInstance si(QString::fromUtf8(parts[0]), QString::fromUtf8(parts[1]), QString::fromUtf8(parts[2]), attribs); // does qdnssd guarantee we won't receive dups? bool found = false; foreach(const XMPP::ServiceInstance &i, instances) { if(i.name() == si.name()) { found = true; break; } } Q_ASSERT(!found); instances += si; emit available(si); } }; //---------------------------------------------------------------------------- // AppleBrowseLookup //---------------------------------------------------------------------------- // only use this class for a single lookup. if you want to lookup again, // create a new object. class AppleBrowseLookup : public QObject, public QDnsSdDelegate { Q_OBJECT public: AppleProvider *global; int resolve_id; XMPP::NameResolver nameResolverAaaa; XMPP::NameResolver nameResolverA; bool activeAaaa; bool activeA; QTimer waitTimer; QByteArray host; QHostAddress addr4; QHostAddress addr6; int port; AppleBrowseLookup(AppleProvider *_global, QObject *parent = 0) : QObject(parent), global(_global), resolve_id(-1), nameResolverAaaa(this), nameResolverA(this), activeAaaa(false), activeA(false), waitTimer(this) { connect(&nameResolverAaaa, SIGNAL(resultsReady(QList)), SLOT(nameAaaa_resultsReady(QList))); connect(&nameResolverAaaa, SIGNAL(error(XMPP::NameResolver::Error)), SLOT(nameAaaa_error(XMPP::NameResolver::Error))); connect(&nameResolverA, SIGNAL(resultsReady(QList)), SLOT(nameA_resultsReady(QList))); connect(&nameResolverA, SIGNAL(error(XMPP::NameResolver::Error)), SLOT(nameA_error(XMPP::NameResolver::Error))); connect(&waitTimer, SIGNAL(timeout()), SLOT(waitTimer_timeout())); waitTimer.setSingleShot(true); } ~AppleBrowseLookup() { global->stop_all(this); } void resolve(const QByteArray &instance, const QByteArray &type, const QByteArray &domain) { resolve_id = global->resolve(this, instance, type, domain); } signals: // emits at least 1 and at most 2 void finished(const QList &addrs, int port); void error(); protected: void dns_resolveResult(int id, const QDnsSd::ResolveResult &result) { // there is only one response, so deregister global->stop(id); if(!result.success) { emit error(); return; } host = result.hostTarget; port = result.port; activeAaaa = true; activeA = true; nameResolverAaaa.start(host, XMPP::NameRecord::Aaaa); nameResolverA.start(host, XMPP::NameRecord::A); waitTimer.start(500); // 500ms cut-off time, take what we have and run } private slots: void nameAaaa_resultsReady(const QList &results) { // nameresolver guarantees at least one result, and we only // care about the first addr6 = results[0].address(); activeAaaa = false; tryDone(); } void nameAaaa_error(XMPP::NameResolver::Error e) { Q_UNUSED(e); activeAaaa = false; tryDone(); } void nameA_resultsReady(const QList &results) { // nameresolver guarantees at least one result, and we only // care about the first addr4 = results[0].address(); activeA = false; tryDone(); } void nameA_error(XMPP::NameResolver::Error e) { Q_UNUSED(e); activeA = false; tryDone(); } void waitTimer_timeout() { tryDone(); } private: void tryDone() { // we're done if both resolves are inactive and we have no // results, or if the wait timer ends and we have at least // one result if(!activeAaaa && !activeA && addr6.isNull() && addr4.isNull()) { nameResolverAaaa.stop(); nameResolverA.stop(); waitTimer.stop(); emit error(); return; } if(!waitTimer.isActive() && (!addr6.isNull() || !addr4.isNull())) { nameResolverAaaa.stop(); nameResolverA.stop(); QList out; if(!addr4.isNull()) out += addr4; if(!addr6.isNull()) out += addr6; emit finished(out, port); } } }; //---------------------------------------------------------------------------- // AppleNameProvider //---------------------------------------------------------------------------- class AppleNameProvider : public XMPP::NameProvider, public QDnsSdDelegate { Q_OBJECT public: AppleProvider *global; AppleNameProvider(AppleProvider *parent) : NameProvider(parent), global(parent) { } ~AppleNameProvider() { global->stop_all(this); } virtual bool supportsLongLived() const { return true; } virtual bool supportsRecordType(int type) const { // all record types supported Q_UNUSED(type); return true; } virtual int resolve_start(const QByteArray &name, int qType, bool longLived) { Q_UNUSED(longLived); // query is always long lived return global->query(this, name, qType); } virtual void resolve_stop(int id) { global->stop(id); } protected: virtual void dns_queryResult(int id, const QDnsSd::QueryResult &result) { if(!result.success) { emit resolve_error(id, XMPP::NameResolver::ErrorGeneric); return; } QList results; foreach(const QDnsSd::Record &rec, result.records) { XMPP::NameRecord nr = importQDnsSdRecord(rec); // unsupported type if(nr.isNull()) continue; // if removed, ensure ttl is 0 if(!rec.added) nr.setTtl(0); results += nr; } emit resolve_resultsReady(id, results); } }; //---------------------------------------------------------------------------- // AppleServiceProvider //---------------------------------------------------------------------------- class AppleServiceProvider : public XMPP::ServiceProvider, public QDnsSdDelegate { Q_OBJECT public: class Browse { public: AppleServiceProvider *parent; int id; AppleBrowse *browse; Browse(AppleServiceProvider *_parent) : parent(_parent), id(-1), browse(0) { } ~Browse() { delete browse; parent->idManager.releaseId(id); } }; class Resolve { public: AppleServiceProvider *parent; int id; AppleBrowseLookup *resolve; Resolve(AppleServiceProvider *_parent) : parent(_parent), id(-1), resolve(0) { } ~Resolve() { delete resolve; parent->idManager.releaseId(id); } }; AppleProvider *global; QList browseList; QList resolveList; IdManager idManager; AppleServiceProvider(AppleProvider *parent) : ServiceProvider(parent), global(parent) { } ~AppleServiceProvider() { qDeleteAll(resolveList); qDeleteAll(browseList); global->stop_all(this); } int indexOfBrowseByBrowse(AppleBrowse *browse) const { for(int n = 0; n < browseList.count(); ++n) { if(browseList[n]->browse == browse) return n; } return -1; } int indexOfBrowseById(int id) const { for(int n = 0; n < browseList.count(); ++n) { if(browseList[n]->id == id) return n; } return -1; } int indexOfResolveByResolve(AppleBrowseLookup *resolve) const { for(int n = 0; n < resolveList.count(); ++n) { if(resolveList[n]->resolve == resolve) return n; } return -1; } int indexOfResolveById(int id) const { for(int n = 0; n < resolveList.count(); ++n) { if(resolveList[n]->id == id) return n; } return -1; } virtual int browse_start(const QString &type, const QString &domain) { Browse *b = new Browse(this); b->id = idManager.reserveId(); b->browse = new AppleBrowse(global, this); connect(b->browse, SIGNAL(available(XMPP::ServiceInstance)), SLOT(browse_available(XMPP::ServiceInstance))); connect(b->browse, SIGNAL(unavailable(XMPP::ServiceInstance)), SLOT(browse_unavailable(XMPP::ServiceInstance))); connect(b->browse, SIGNAL(error()), SLOT(browse_error())); browseList += b; b->browse->browse(type, domain); return b->id; } virtual void browse_stop(int id) { int at = indexOfBrowseById(id); if(at == -1) return; Browse *b = browseList[at]; browseList.removeAt(at); delete b; } virtual int resolve_start(const QByteArray &name) { QList parts = nameToInstanceParts(name); if(parts.isEmpty()) { // TODO: signal error rather than die Q_ASSERT(0); } Resolve *r = new Resolve(this); r->id = idManager.reserveId(); r->resolve = new AppleBrowseLookup(global, this); connect(r->resolve, SIGNAL(finished(QList)), SLOT(resolve_finished(QList))); connect(r->resolve, SIGNAL(error()), SLOT(resolve_error())); resolveList += r; r->resolve->resolve(parts[0], parts[1], parts[2]); return r->id; } virtual void resolve_stop(int id) { int at = indexOfResolveById(id); if(at == -1) return; Resolve *r = resolveList[at]; resolveList.removeAt(at); delete r; } virtual int publish_start(const QString &instance, const QString &type, int port, const QMap &attributes) { QByteArray txtRecord = attribsToTxtRecord(attributes); if(txtRecord.isEmpty()) { // TODO: signal error rather than die Q_ASSERT(0); } QString domain = "local"; // FIXME: conversion/escaping is probably wrong? return global->reg(this, instance.toUtf8(), type.toUtf8(), domain.toUtf8(), port, txtRecord); } virtual void publish_update(int id, const QMap &attributes) { // TODO: verify 'id' is valid. if not valid, then assert/return (don't do anything or signal error) QByteArray txtRecord = attribsToTxtRecord(attributes); if(txtRecord.isEmpty()) { // TODO: signal error rather than die Q_ASSERT(0); } if(global->dns.recordUpdateTxt(id, txtRecord, 4500)) { // FIXME: SR QMetaObject::invokeMethod(this, "publish_published", Qt::QueuedConnection, Q_ARG(int, id)); } else { // TODO: unpublish // FIXME: register meta type, SR QMetaObject::invokeMethod(this, "publish_error", Qt::QueuedConnection, Q_ARG(int, id), Q_ARG(XMPP::ServiceLocalPublisher::Error, XMPP::ServiceLocalPublisher::ErrorGeneric)); } } virtual void publish_stop(int id) { global->stop(id); } virtual int publish_extra_start(int pub_id, const XMPP::NameRecord &name) { // TODO Q_UNUSED(pub_id); Q_UNUSED(name); return 0; } virtual void publish_extra_update(int id, const XMPP::NameRecord &name) { // TODO Q_UNUSED(id); Q_UNUSED(name); } virtual void publish_extra_stop(int id) { // TODO Q_UNUSED(id); } // called by AppleProvider void dns_regResult(int id, const QDnsSd::RegResult &result) { // TODO Q_UNUSED(id); Q_UNUSED(result); } private slots: void browse_available(const XMPP::ServiceInstance &instance) { int at = indexOfBrowseByBrowse((AppleBrowse *)sender()); Q_ASSERT(at != -1); emit browse_instanceAvailable(browseList[at]->id, instance); } void browse_unavailable(const XMPP::ServiceInstance &instance) { int at = indexOfBrowseByBrowse((AppleBrowse *)sender()); Q_ASSERT(at != -1); emit browse_instanceUnavailable(browseList[at]->id, instance); } void browse_error() { int at = indexOfBrowseByBrowse((AppleBrowse *)sender()); Q_ASSERT(at != -1); Browse *b = browseList[at]; browseList.removeAt(at); int id = b->id; delete b; // FIXME: this looks weird, we should probably rename our // local function emit ServiceProvider::browse_error(id, XMPP::ServiceBrowser::ErrorGeneric); } void resolve_finished(const QList &addrs, int port) { int at = indexOfResolveByResolve((AppleBrowseLookup *)sender()); Q_ASSERT(at != -1); Resolve *r = resolveList[at]; resolveList.removeAt(at); int id = r->id; delete r; QList results; foreach(const QHostAddress &addr, addrs) { ResolveResult r; r.address = addr; r.port = port; results += r; } emit resolve_resultsReady(id, results); } void resolve_error() { int at = indexOfResolveByResolve((AppleBrowseLookup *)sender()); Q_ASSERT(at != -1); Resolve *r = resolveList[at]; resolveList.removeAt(at); int id = r->id; delete r; // FIXME: this looks weird, we should probably rename our // local function emit ServiceProvider::resolve_error(id, XMPP::ServiceResolver::ErrorGeneric); } }; // AppleProvider XMPP::NameProvider *AppleProvider::createNameProviderInternet() { return new AppleNameProvider(this); } XMPP::NameProvider *AppleProvider::createNameProviderLocal() { return new AppleNameProvider(this); } XMPP::ServiceProvider *AppleProvider::createServiceProvider() { return new AppleServiceProvider(this); } #ifdef APPLEDNS_STATIC XMPP::IrisNetProvider *irisnet_createAppleProvider() { return new AppleProvider; } #else Q_EXPORT_PLUGIN2(appledns, AppleProvider) #endif #include "appledns.moc"