583 lines
16 KiB
C++
583 lines
16 KiB
C++
|
|
#include "qtavahi.h"
|
|
|
|
#include <avahi-client/client.h>
|
|
#include <avahi-client/lookup.h>
|
|
#include <avahi-client/publish.h>
|
|
|
|
#include <avahi-common/alternative.h>
|
|
#include <avahi-common/error.h>
|
|
#include <avahi-common/malloc.h>
|
|
|
|
#include <avahi-qt4/qt-watch.h>
|
|
|
|
#include <QSize>
|
|
#include <QtEndian>
|
|
|
|
extern "C" {
|
|
#include <netinet/in.h>
|
|
#include <net/if.h>
|
|
}
|
|
|
|
static QString ifidx2name(unsigned int ifindex) {
|
|
char buf[IF_NAMESIZE];
|
|
char *n = ::if_indextoname(ifindex, buf);
|
|
if (!n) return QString();
|
|
return QString::fromUtf8(n, qstrnlen(n, IF_NAMESIZE));
|
|
}
|
|
|
|
namespace Avahi {
|
|
|
|
static QHostAddress fromAvahiAddress(const AvahiAddress *address, AvahiIfIndex interface) {
|
|
switch (address->proto) {
|
|
case AVAHI_PROTO_INET:
|
|
return QHostAddress(qFromBigEndian((quint32) address->data.ipv4.address));
|
|
case AVAHI_PROTO_INET6: {
|
|
QHostAddress addr((quint8*) address->data.ipv6.address);
|
|
if (interface >= 0) addr.setScopeId(ifidx2name(interface));
|
|
return addr;
|
|
}
|
|
case AVAHI_PROTO_UNSPEC:
|
|
return QHostAddress();
|
|
}
|
|
return QHostAddress();
|
|
}
|
|
|
|
namespace priv {
|
|
|
|
static void browsep_client_callback(AvahiClient *c, AvahiClientState state, void * userdata);
|
|
static void browsep_browse_callback(AvahiServiceBrowser *b, AvahiIfIndex interface, AvahiProtocol protocol, AvahiBrowserEvent event,
|
|
const char *name, const char *type, const char *domain, AvahiLookupResultFlags flags, void* userdata);
|
|
static void browsep_resolve_callback(AvahiServiceResolver *r, AvahiIfIndex interface, AvahiProtocol protocol, AvahiResolverEvent event,
|
|
const char *name, const char *type, const char *domain, const char *host_name, const AvahiAddress *address,
|
|
uint16_t port, AvahiStringList *txt, AvahiLookupResultFlags flags, void* userdata);
|
|
}
|
|
}
|
|
|
|
namespace Avahi {
|
|
namespace priv {
|
|
|
|
class BrowseP {
|
|
public:
|
|
static const ::AvahiPoll *m_qtpoll;
|
|
|
|
QString m_serviceType;
|
|
|
|
bool m_initialized;
|
|
bool m_running;
|
|
|
|
::AvahiClient *m_client;
|
|
::AvahiServiceBrowser *m_browser;
|
|
|
|
QByteArray m_service_type;
|
|
|
|
Avahi::Browse *m_parent;
|
|
|
|
QHash<ServiceKey, Service> m_list;
|
|
|
|
BrowseP(QString servicetype, Browse *parent)
|
|
: m_serviceType(servicetype),
|
|
m_initialized(false), m_running(false), m_client(0), m_browser(0),
|
|
m_service_type(servicetype.toUtf8()), m_parent(parent) {
|
|
int err;
|
|
|
|
if (!m_qtpoll) {
|
|
if (!(m_qtpoll = avahi_qt_poll_get())) {
|
|
qWarning("avahi_qt_poll_get failed");
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (!(m_client = avahi_client_new(m_qtpoll, (AvahiClientFlags) 0, browsep_client_callback, this, &err))) {
|
|
qWarning("avahi_client_new failed: %s", avahi_strerror(err));
|
|
return;
|
|
}
|
|
|
|
m_initialized = true;
|
|
}
|
|
|
|
void clearList() {
|
|
QList<Service> l = m_list.values();
|
|
m_list.clear();
|
|
for (int i = 0; i < l.size(); ++i) {
|
|
m_parent->lostService(l.at(i));
|
|
}
|
|
}
|
|
|
|
void release() {
|
|
stop();
|
|
|
|
if (!m_initialized) return;
|
|
|
|
if (m_client) {
|
|
avahi_client_free(m_client);
|
|
m_client = 0;
|
|
}
|
|
|
|
m_initialized = false;
|
|
}
|
|
|
|
~BrowseP() {
|
|
release();
|
|
}
|
|
|
|
void client_callback(AvahiClient *c, AvahiClientState state) {
|
|
switch (state) {
|
|
case AVAHI_CLIENT_S_RUNNING:
|
|
break;
|
|
case AVAHI_CLIENT_FAILURE:
|
|
qWarning("avahi client failure: %s", avahi_strerror(avahi_client_errno(c)));
|
|
release();
|
|
break;
|
|
case AVAHI_CLIENT_S_COLLISION:
|
|
case AVAHI_CLIENT_S_REGISTERING:
|
|
break;
|
|
case AVAHI_CLIENT_CONNECTING:
|
|
break;
|
|
}
|
|
}
|
|
|
|
void browse_callback(AvahiServiceBrowser *b, AvahiIfIndex interface, AvahiProtocol protocol, AvahiBrowserEvent event,
|
|
const char *name, const char *type, const char *domain, AvahiLookupResultFlags /* flags */) {
|
|
if (!m_browser) m_browser = b;
|
|
Q_ASSERT(m_browser == b);
|
|
|
|
if (protocol != AVAHI_PROTO_INET && protocol != AVAHI_PROTO_INET6) return; /* ignore non IPv[46] for now */
|
|
|
|
/* Called whenever a new services becomes available on the LAN or is removed from the LAN */
|
|
|
|
switch (event) {
|
|
case AVAHI_BROWSER_FAILURE:
|
|
qWarning("(Browser) %s", avahi_strerror(avahi_client_errno(m_client)));
|
|
release();
|
|
return;
|
|
|
|
case AVAHI_BROWSER_NEW:
|
|
qDebug("(Browser) NEW: service '%s' of type '%s' in domain '%s'", name, type, domain);
|
|
|
|
/* We ignore the returned resolver object. In the callback
|
|
* function we free it. If the server is terminated before
|
|
* the callback function is called the server will free
|
|
* the resolver for us. */
|
|
|
|
if (!(avahi_service_resolver_new(m_client, interface, protocol, name, type, domain, protocol, (AvahiLookupFlags) 0, browsep_resolve_callback, this))) {
|
|
qWarning("Failed to resolve service '%s' on '%s': %s", name, domain, avahi_strerror(avahi_client_errno(m_client)));
|
|
}
|
|
|
|
break;
|
|
|
|
case AVAHI_BROWSER_REMOVE: {
|
|
ServiceKey k(interface, protocol, name, domain);
|
|
Service s = m_list.value(k);
|
|
if (s.valid()) {
|
|
m_list.remove(k);
|
|
m_parent->lostService(s);
|
|
}
|
|
}
|
|
break;
|
|
|
|
case AVAHI_BROWSER_ALL_FOR_NOW:
|
|
break;
|
|
case AVAHI_BROWSER_CACHE_EXHAUSTED:
|
|
break;
|
|
}
|
|
}
|
|
|
|
void resolve_callback(AvahiServiceResolver *r, AvahiIfIndex interface, AvahiProtocol protocol, AvahiResolverEvent event,
|
|
const char *name, const char *type, const char *domain, const char *host_name, const AvahiAddress *address,
|
|
uint16_t port, AvahiStringList *txt, AvahiLookupResultFlags /* flags */) {
|
|
Q_ASSERT(r);
|
|
|
|
// if (protocol != AVAHI_PROTO_INET) goto done; /* ignore non IPv4 for now */
|
|
if (protocol != AVAHI_PROTO_INET && protocol != AVAHI_PROTO_INET6) goto done; /* ignore non IPv[46] for now */
|
|
|
|
/* Called whenever a service has been resolved successfully or timed out */
|
|
|
|
switch (event) {
|
|
case AVAHI_RESOLVER_FAILURE:
|
|
qDebug("(Resolver) Failed to resolve service '%s' of type '%s' at '%s' in domain '%s': %s", name, type, host_name, domain, avahi_strerror(avahi_client_errno(m_client)));
|
|
break;
|
|
|
|
case AVAHI_RESOLVER_FOUND: {
|
|
qDebug("(Resolver) found service '%s' of type '%s' at '%s' in domain '%s'", name, type, host_name, domain);
|
|
QHash<QString, QString> qtxt;
|
|
AvahiStringList *psl;
|
|
for (psl = txt ; psl ; psl = psl->next) {
|
|
QString e = QString::fromUtf8((const char*) psl->text, psl->size), key, value;
|
|
int eqPos = e.indexOf(QChar('='));
|
|
if (eqPos == 0) continue; /* ignore empty keys */
|
|
if (eqPos < 0) { /* "boolean" */
|
|
key = e;
|
|
} else {
|
|
key = e.left(eqPos);
|
|
value = e.mid(eqPos + 1);
|
|
}
|
|
key = key.toLower();
|
|
// qDebug() << "Resolver: key = " << key << ", value = " << value;
|
|
qtxt.insert(key, value);
|
|
}
|
|
|
|
ServiceKey k(interface, protocol, name, domain);
|
|
Service s = m_list.value(k);
|
|
if (s.valid()) {
|
|
m_list.remove(k);
|
|
m_parent->lostService(s);
|
|
}
|
|
|
|
s = Service(QString::fromUtf8(name), QString::fromUtf8(host_name), QString::fromUtf8(domain), fromAvahiAddress(address, interface), interface, protocol, port, qtxt);
|
|
m_list.insert(k, s);
|
|
m_parent->foundService(s);
|
|
}
|
|
}
|
|
|
|
done:
|
|
avahi_service_resolver_free(r);
|
|
}
|
|
|
|
bool start() {
|
|
if (m_running) return true;
|
|
if (!m_initialized) return false;
|
|
|
|
clearList();
|
|
|
|
if (NULL == (m_browser = avahi_service_browser_new(m_client, AVAHI_IF_UNSPEC, AVAHI_PROTO_UNSPEC, m_service_type.constData(), NULL, (AvahiLookupFlags) 0, browsep_browse_callback, this))) {
|
|
qWarning("Failed to create service browser: %s", avahi_strerror(avahi_client_errno(m_client)));
|
|
return false;
|
|
}
|
|
|
|
emit m_parent->started();
|
|
m_running = true;
|
|
|
|
return true;
|
|
}
|
|
|
|
void stop() {
|
|
clearList();
|
|
if (m_running) {
|
|
emit m_parent->stopped();
|
|
m_running = false;
|
|
}
|
|
if (m_browser) {
|
|
avahi_service_browser_free(m_browser);
|
|
m_browser = 0;
|
|
}
|
|
}
|
|
};
|
|
const AvahiPoll *BrowseP::m_qtpoll = 0;
|
|
|
|
static void browsep_client_callback(AvahiClient *c, AvahiClientState state, void * userdata) {
|
|
BrowseP *p = static_cast<BrowseP*>(userdata);
|
|
p->client_callback(c, state);
|
|
}
|
|
|
|
static void browsep_browse_callback(AvahiServiceBrowser *b, AvahiIfIndex interface, AvahiProtocol protocol, AvahiBrowserEvent event,
|
|
const char *name, const char *type, const char *domain, AvahiLookupResultFlags flags, void* userdata) {
|
|
BrowseP *p = static_cast<BrowseP*>(userdata);
|
|
p->browse_callback(b, interface, protocol, event, name, type, domain, flags);
|
|
}
|
|
|
|
static void browsep_resolve_callback(AvahiServiceResolver *r, AvahiIfIndex interface, AvahiProtocol protocol, AvahiResolverEvent event,
|
|
const char *name, const char *type, const char *domain, const char *host_name, const AvahiAddress *address,
|
|
uint16_t port, AvahiStringList *txt, AvahiLookupResultFlags flags, void* userdata) {
|
|
BrowseP *p = static_cast<BrowseP*>(userdata);
|
|
p->resolve_callback(r, interface, protocol, event, name, type, domain, host_name, address, port, txt, flags);
|
|
}
|
|
|
|
static void announcep_client_callback(AvahiClient *c, AvahiClientState state, void * userdata);
|
|
static void announcep_entry_group_callback(AvahiEntryGroup *g, AvahiEntryGroupState state, void *userdata);
|
|
|
|
class AnnounceP {
|
|
public:
|
|
static const AvahiPoll *m_qtpoll;
|
|
|
|
QString m_serviceType, m_name;
|
|
quint16 m_port;
|
|
QHash<QString,QString> m_txt;
|
|
|
|
bool m_initialized;
|
|
bool m_running;
|
|
|
|
AvahiClient *m_client;
|
|
AvahiEntryGroup *m_group;
|
|
|
|
QByteArray m_service_type;
|
|
|
|
Announce *m_parent;
|
|
|
|
char *m_raw_name; /* avahi string */
|
|
QList<QByteArray> m_txt_list;
|
|
::AvahiStringList *m_raw_txt;
|
|
|
|
AnnounceP(QString serviceType, QString name, quint16 port, QHash<QString,QString> txt, Announce *parent)
|
|
: m_serviceType(serviceType), m_name(name), m_port(port), m_txt(txt),
|
|
m_initialized(false), m_running(false), m_client(0), m_group(0),
|
|
m_service_type(serviceType.toUtf8()), m_parent(parent),
|
|
m_raw_name(avahi_strdup(name.toUtf8().constData())), m_raw_txt(0) {
|
|
int err;
|
|
|
|
QHashIterator<QString, QString> i(m_txt);
|
|
while (i.hasNext()) {
|
|
QByteArray a;
|
|
i.next();
|
|
if (i.value().isNull()) {
|
|
m_txt_list.append(a = i.key().toUtf8());
|
|
} else {
|
|
m_txt_list.append(a = QString("%1=%2").arg(i.key(), i.value()).toUtf8());
|
|
}
|
|
AvahiStringList *l = avahi_string_list_new(a.constData(), NULL);
|
|
l->next = m_raw_txt;
|
|
m_raw_txt = l;
|
|
}
|
|
|
|
if (!m_qtpoll) {
|
|
if (!(m_qtpoll = avahi_qt_poll_get())) {
|
|
qWarning("avahi_qt_poll_get failed");
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (!(m_client = avahi_client_new(m_qtpoll, (AvahiClientFlags) 0, announcep_client_callback, this, &err))) {
|
|
qWarning("avahi_client_new failed: %s", avahi_strerror(err));
|
|
return;
|
|
}
|
|
|
|
m_initialized = true;
|
|
|
|
start();
|
|
}
|
|
|
|
void release() {
|
|
if (m_running) {
|
|
m_running = false;
|
|
}
|
|
if (!m_initialized) return;
|
|
|
|
if (m_group) {
|
|
qDebug("avahi unregister service");
|
|
avahi_entry_group_reset(m_group);
|
|
avahi_entry_group_free(m_group);
|
|
m_group = 0;
|
|
}
|
|
|
|
if (m_client) {
|
|
avahi_client_free(m_client);
|
|
m_client = 0;
|
|
}
|
|
|
|
m_initialized = false;
|
|
|
|
emit m_parent->stopped();
|
|
}
|
|
|
|
~AnnounceP() {
|
|
release();
|
|
if (m_raw_name) {
|
|
avahi_free(m_raw_name);
|
|
m_raw_name = 0;
|
|
}
|
|
if (m_raw_txt) {
|
|
avahi_string_list_free(m_raw_txt);
|
|
m_raw_txt = 0;
|
|
}
|
|
}
|
|
|
|
void client_callback(AvahiClient *c, AvahiClientState state) {
|
|
m_client = c;
|
|
switch (state) {
|
|
case AVAHI_CLIENT_S_RUNNING:
|
|
create_services();
|
|
break;
|
|
case AVAHI_CLIENT_FAILURE:
|
|
qDebug("avahi client failure");
|
|
release();
|
|
break;
|
|
case AVAHI_CLIENT_S_COLLISION:
|
|
case AVAHI_CLIENT_S_REGISTERING:
|
|
if (m_group) {
|
|
avahi_entry_group_reset(m_group);
|
|
}
|
|
break;
|
|
case AVAHI_CLIENT_CONNECTING:
|
|
break;
|
|
}
|
|
}
|
|
|
|
void entry_group_callback(AvahiEntryGroup *g, AvahiEntryGroupState state) {
|
|
m_group = g;
|
|
|
|
switch (state) {
|
|
case AVAHI_ENTRY_GROUP_ESTABLISHED :
|
|
emit m_parent->established();
|
|
break;
|
|
|
|
case AVAHI_ENTRY_GROUP_COLLISION :
|
|
{
|
|
char *n = avahi_alternative_service_name(m_raw_name);
|
|
avahi_free(m_raw_name);
|
|
m_raw_name = n;
|
|
m_name = QString::fromUtf8(m_raw_name);
|
|
}
|
|
|
|
/* And recreate the services */
|
|
avahi_entry_group_reset(m_group);
|
|
create_services();
|
|
/* same name should result in AVAHI_ERR_COLLISION in create_services, emit collision there. */
|
|
break;
|
|
|
|
case AVAHI_ENTRY_GROUP_FAILURE :
|
|
qDebug("Entry group failure: %s", avahi_strerror(avahi_client_errno(m_client)));
|
|
|
|
release();
|
|
break;
|
|
|
|
case AVAHI_ENTRY_GROUP_UNCOMMITED:
|
|
case AVAHI_ENTRY_GROUP_REGISTERING:
|
|
break;
|
|
}
|
|
}
|
|
|
|
bool create_services() {
|
|
int ret;
|
|
|
|
if (!m_group) {
|
|
if (!(m_group = avahi_entry_group_new(m_client, announcep_entry_group_callback, this))) {
|
|
qDebug("avahi_entry_group_new failed: %s", avahi_strerror(avahi_client_errno(m_client)));
|
|
release();
|
|
return false;
|
|
}
|
|
}
|
|
|
|
again:
|
|
if (avahi_entry_group_is_empty(m_group)) {
|
|
if ((ret = avahi_entry_group_add_service_strlst(m_group, AVAHI_IF_UNSPEC, AVAHI_PROTO_UNSPEC, (AvahiPublishFlags) 0, m_raw_name, m_service_type.constData(), NULL, NULL, m_port, m_raw_txt)) < 0) {
|
|
|
|
if (ret == AVAHI_ERR_COLLISION)
|
|
goto collision;
|
|
|
|
qDebug("Failed to add '%s': %s", m_service_type.constData(), avahi_strerror(ret));
|
|
release();
|
|
return false;
|
|
}
|
|
if ((ret = avahi_entry_group_commit(m_group)) < 0) {
|
|
qDebug("Failed to commit entry group: %s", avahi_strerror(ret));
|
|
release();
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
|
|
collision:
|
|
qDebug("avahi name collision: %s, trying a new one", m_raw_name);
|
|
{
|
|
char *n = avahi_alternative_service_name(m_raw_name);
|
|
avahi_free(m_raw_name);
|
|
m_raw_name = n;
|
|
m_name = QString::fromUtf8(m_raw_name);
|
|
}
|
|
avahi_entry_group_reset(m_group);
|
|
emit m_parent->collision();
|
|
goto again;
|
|
}
|
|
|
|
bool start() {
|
|
if (m_running) return true;
|
|
if (!m_initialized) return false;
|
|
|
|
m_running = true;
|
|
emit m_parent->started();
|
|
|
|
return true;
|
|
}
|
|
|
|
void stop() {
|
|
release();
|
|
}
|
|
};
|
|
const AvahiPoll *AnnounceP::m_qtpoll = 0;
|
|
|
|
static void announcep_client_callback(AvahiClient *c, AvahiClientState state, void * userdata) {
|
|
AnnounceP *p = static_cast<AnnounceP*>(userdata);
|
|
p->client_callback(c, state);
|
|
}
|
|
static void announcep_entry_group_callback(AvahiEntryGroup *g, AvahiEntryGroupState state, void *userdata) {
|
|
AnnounceP *p = static_cast<AnnounceP*>(userdata);
|
|
p->entry_group_callback(g, state);
|
|
}
|
|
}
|
|
|
|
/* public interfaces */
|
|
|
|
Browse::Browse(QString serviceType, QObject *parent)
|
|
: QObject(parent), m_priv(new priv::BrowseP(serviceType, this)) {
|
|
}
|
|
|
|
Browse::~Browse() {
|
|
delete m_priv;
|
|
m_priv = 0;
|
|
}
|
|
|
|
bool Browse::start() {
|
|
return m_priv->start();
|
|
}
|
|
|
|
void Browse::stop() {
|
|
m_priv->stop();
|
|
}
|
|
|
|
QString Browse::serviceType() const {
|
|
return m_priv->m_serviceType;
|
|
}
|
|
|
|
QList<Service> Browse::currentList() const {
|
|
return m_priv->m_list.values();
|
|
}
|
|
|
|
void Browse::foundService(Avahi::Service s) {
|
|
emit found(s);
|
|
}
|
|
|
|
void Browse::lostService(Avahi::Service s) {
|
|
emit lost(s);
|
|
if (s.m_priv && s.m_priv->m_watcher) {
|
|
emit s.m_priv->m_watcher->lost(s);
|
|
}
|
|
}
|
|
|
|
|
|
Announce::Announce(QString serviceType, QString name, quint16 port, QHash<QString,QString> txt, QObject *parent)
|
|
: QObject(parent), m_priv(new priv::AnnounceP(serviceType, name, port, txt, this)) {
|
|
}
|
|
|
|
Announce::~Announce() {
|
|
delete m_priv;
|
|
m_priv = 0;
|
|
}
|
|
|
|
bool Announce::start() {
|
|
return m_priv->start();
|
|
}
|
|
|
|
void Announce::stop() {
|
|
m_priv->stop();
|
|
}
|
|
|
|
QString Announce::serviceType() const {
|
|
return m_priv->m_serviceType;
|
|
}
|
|
|
|
QString Announce::name() const {
|
|
return m_priv->m_name;
|
|
}
|
|
quint16 Announce::port() const {
|
|
return m_priv->m_port;
|
|
}
|
|
QHash<QString, QString> Announce::txt() const {
|
|
return m_priv->m_txt;
|
|
}
|
|
QString Announce::txt(QString key) const {
|
|
return m_priv->m_txt.value(key);
|
|
}
|
|
|
|
}
|