pidgin/pidgin

closing merged branch
port-changes-from-branch-2.x.y-to-default
2020-02-03, Gary Kramlich
2f836435c33c
closing merged branch
/*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program 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 Library General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02111-1301, USA.
*/
#include "internal.h"
#include <purple.h>
#include "mdns_interface.h"
#include "buddy.h"
#include "bonjour.h"
#include <avahi-client/client.h>
#include <avahi-client/lookup.h>
#include <avahi-client/publish.h>
#include <avahi-common/address.h>
#include <avahi-common/malloc.h>
#include <avahi-common/error.h>
#include <avahi-common/strlst.h>
#include <avahi-glib/glib-malloc.h>
#include <avahi-glib/glib-watch.h>
/* Avahi only defines the types that it actually uses (which at this time doesn't include NULL) */
#ifndef AVAHI_DNS_TYPE_NULL
#define AVAHI_DNS_TYPE_NULL 0x0A
#endif
/* data used by avahi bonjour implementation */
typedef struct {
AvahiClient *client;
AvahiGLibPoll *glib_poll;
AvahiServiceBrowser *sb;
AvahiEntryGroup *group;
AvahiEntryGroup *buddy_icon_group;
} AvahiSessionImplData;
typedef struct {
AvahiServiceResolver *resolver;
AvahiIfIndex interface;
AvahiProtocol protocol;
gchar *name;
gchar *type;
gchar *domain;
/* This is a reference to the entry in BonjourBuddy->ips */
const char *ip;
} AvahiSvcResolverData;
typedef struct {
GSList *resolvers;
AvahiRecordBrowser *buddy_icon_rec_browser;
} AvahiBuddyImplData;
static gint
_find_resolver_data(gconstpointer a, gconstpointer b) {
const AvahiSvcResolverData *rd_a = a;
const AvahiSvcResolverData *rd_b = b;
gint ret = 1;
if(rd_a->interface == rd_b->interface
&& rd_a->protocol == rd_b->protocol
&& purple_strequal(rd_a->name, rd_b->name)
&& purple_strequal(rd_a->type, rd_b->type)
&& purple_strequal(rd_a->domain, rd_b->domain)) {
ret = 0;
}
return ret;
}
static gint
_find_resolver_data_by_resolver(gconstpointer a, gconstpointer b) {
const AvahiSvcResolverData *rd_a = a;
const AvahiServiceResolver *resolver = b;
gint ret = 1;
if(rd_a->resolver == resolver)
ret = 0;
return ret;
}
static void
_cleanup_resolver_data(AvahiSvcResolverData *rd) {
if (rd->resolver)
avahi_service_resolver_free(rd->resolver);
g_free(rd->name);
g_free(rd->type);
g_free(rd->domain);
g_free(rd);
}
static void
_resolver_callback(AvahiServiceResolver *r, AvahiIfIndex interface, AvahiProtocol protocol,
AvahiResolverEvent event, const char *name, const char *type, const char *domain,
const char *host_name, const AvahiAddress *a, uint16_t port, AvahiStringList *txt,
AvahiLookupResultFlags flags, void *userdata) {
PurpleBuddy *pb;
BonjourBuddy *bb;
PurpleAccount *account = userdata;
AvahiStringList *l;
size_t size;
char *key, *value;
char ip[AVAHI_ADDRESS_STR_MAX];
AvahiBuddyImplData *b_impl;
AvahiSvcResolverData *rd;
GSList *res;
g_return_if_fail(r != NULL);
pb = purple_blist_find_buddy(account, name);
bb = (pb != NULL) ? purple_buddy_get_protocol_data(pb) : NULL;
switch (event) {
case AVAHI_RESOLVER_FAILURE:
purple_debug_error("bonjour", "_resolve_callback - Failure: %s\n",
avahi_strerror(avahi_client_errno(avahi_service_resolver_get_client(r))));
avahi_service_resolver_free(r);
if (bb != NULL) {
b_impl = bb->mdns_impl_data;
res = g_slist_find_custom(b_impl->resolvers, r, _find_resolver_data_by_resolver);
if (res != NULL) {
rd = res->data;
b_impl->resolvers = g_slist_delete_link(b_impl->resolvers, res);
/* We've already freed the resolver */
rd->resolver = NULL;
_cleanup_resolver_data(rd);
/* If this was the last resolver, remove the buddy */
if (b_impl->resolvers == NULL)
bonjour_buddy_signed_off(pb);
}
}
break;
case AVAHI_RESOLVER_FOUND:
purple_debug_info("bonjour", "_resolve_callback - name:%s account:%p bb:%p\n",
name, account, bb);
/* create a buddy record */
if (bb == NULL)
bb = bonjour_buddy_new(name, account);
b_impl = bb->mdns_impl_data;
/* If we're reusing an existing buddy, it may be a new resolver or an existing one. */
res = g_slist_find_custom(b_impl->resolvers, r, _find_resolver_data_by_resolver);
if (res != NULL)
rd = res->data;
else {
rd = g_new0(AvahiSvcResolverData, 1);
rd->resolver = r;
rd->interface = interface;
rd->protocol = protocol;
rd->name = g_strdup(name);
rd->type = g_strdup(type);
rd->domain = g_strdup(domain);
b_impl->resolvers = g_slist_prepend(b_impl->resolvers, rd);
}
/* Get the ip as a string */
ip[0] = '\0';
avahi_address_snprint(ip, AVAHI_ADDRESS_STR_MAX, a);
if (protocol == AVAHI_PROTO_INET6)
append_iface_if_linklocal(ip, interface);
purple_debug_info("bonjour", "_resolve_callback - name:%s ip:%s prev_ip:%s\n",
name, ip, rd->ip);
if (rd->ip == NULL || !purple_strequal(rd->ip, ip)) {
/* We store duplicates in bb->ips, so we always remove the one */
if (rd->ip != NULL) {
bb->ips = g_slist_remove(bb->ips, rd->ip);
g_free((gchar *) rd->ip);
}
/* IPv6 goes at the front of the list and IPv4 at the end so that we "prefer" IPv6, if present */
if (protocol == AVAHI_PROTO_INET6) {
rd->ip = g_strdup_printf("%s", ip);
bb->ips = g_slist_prepend(bb->ips, (gchar *) rd->ip);
} else {
rd->ip = g_strdup(ip);
bb->ips = g_slist_append(bb->ips, (gchar *) rd->ip);
}
}
bb->port_p2pj = port;
/* Obtain the parameters from the text_record */
clear_bonjour_buddy_values(bb);
for(l = txt; l != NULL; l = l->next) {
if (avahi_string_list_get_pair(l, &key, &value, &size) < 0)
continue;
set_bonjour_buddy_value(bb, key, value, size);
/* TODO: Since we're using the glib allocator, I think we
* can use the values instead of re-copying them */
avahi_free(key);
avahi_free(value);
}
if (!bonjour_buddy_check(bb)) {
b_impl->resolvers = g_slist_remove(b_impl->resolvers, rd);
_cleanup_resolver_data(rd);
/* If this was the last resolver, remove the buddy */
if (b_impl->resolvers == NULL) {
if (pb != NULL)
bonjour_buddy_signed_off(pb);
else
bonjour_buddy_delete(bb);
}
} else
/* Add or update the buddy in our buddy list */
bonjour_buddy_add_to_purple(bb, pb);
break;
default:
purple_debug_info("bonjour", "Unrecognized Service Resolver event: %d.\n", event);
}
}
static void
_browser_callback(AvahiServiceBrowser *b, AvahiIfIndex interface,
AvahiProtocol protocol, AvahiBrowserEvent event,
const char *name, const char *type, const char *domain,
AvahiLookupResultFlags flags, void *userdata) {
PurpleAccount *account = userdata;
PurpleBuddy *pb = NULL;
switch (event) {
case AVAHI_BROWSER_FAILURE:
purple_debug_error("bonjour", "_browser_callback - Failure: %s\n",
avahi_strerror(avahi_client_errno(avahi_service_browser_get_client(b))));
/* TODO: This is an error that should be handled. */
break;
case AVAHI_BROWSER_NEW:
/* A new peer has joined the network and uses iChat bonjour */
purple_debug_info("bonjour", "_browser_callback - new service\n");
/* Make sure it isn't us */
if (purple_utf8_strcasecmp(name, bonjour_get_jid(account)) != 0) {
if (!avahi_service_resolver_new(avahi_service_browser_get_client(b),
interface, protocol, name, type, domain, protocol,
0, _resolver_callback, account)) {
purple_debug_warning("bonjour", "_browser_callback -- Error initiating resolver: %s\n",
avahi_strerror(avahi_client_errno(avahi_service_browser_get_client(b))));
}
}
break;
case AVAHI_BROWSER_REMOVE:
purple_debug_info("bonjour", "_browser_callback - Remove service\n");
pb = purple_blist_find_buddy(account, name);
if (pb != NULL) {
BonjourBuddy *bb = purple_buddy_get_protocol_data(pb);
AvahiBuddyImplData *b_impl;
GSList *l;
AvahiSvcResolverData *rd_search;
g_return_if_fail(bb != NULL);
b_impl = bb->mdns_impl_data;
/* There may be multiple presences, we should only get rid of this one */
rd_search = g_new0(AvahiSvcResolverData, 1);
rd_search->interface = interface;
rd_search->protocol = protocol;
rd_search->name = (gchar *) name;
rd_search->type = (gchar *) type;
rd_search->domain = (gchar *) domain;
l = g_slist_find_custom(b_impl->resolvers, rd_search, _find_resolver_data);
g_free(rd_search);
if (l != NULL) {
AvahiSvcResolverData *rd = l->data;
b_impl->resolvers =
g_slist_delete_link(b_impl->resolvers, l);
/* This IP is no longer available */
if (rd->ip != NULL) {
bb->ips = g_slist_remove(bb->ips, rd->ip);
g_free((gchar *) rd->ip);
}
_cleanup_resolver_data(rd);
/* If this was the last resolver, remove the buddy */
if (b_impl->resolvers == NULL)
bonjour_buddy_signed_off(pb);
}
}
break;
case AVAHI_BROWSER_ALL_FOR_NOW:
case AVAHI_BROWSER_CACHE_EXHAUSTED:
break;
default:
purple_debug_info("bonjour", "Unrecognized Service browser event: %d.\n", event);
}
}
static void
_buddy_icon_group_cb(AvahiEntryGroup *g, AvahiEntryGroupState state, void *userdata) {
BonjourDnsSd *data = userdata;
AvahiSessionImplData *idata = data->mdns_impl_data;
g_return_if_fail(g == idata->buddy_icon_group || idata->buddy_icon_group == NULL);
switch(state) {
case AVAHI_ENTRY_GROUP_ESTABLISHED:
purple_debug_info("bonjour", "Successfully registered buddy icon data.\n");
break;
case AVAHI_ENTRY_GROUP_COLLISION:
purple_debug_error("bonjour", "Collision registering buddy icon data.\n");
break;
case AVAHI_ENTRY_GROUP_FAILURE:
purple_debug_error("bonjour", "Error registering buddy icon data: %s.\n",
avahi_strerror(avahi_client_errno(avahi_entry_group_get_client(g))));
break;
case AVAHI_ENTRY_GROUP_UNCOMMITED:
case AVAHI_ENTRY_GROUP_REGISTERING:
break;
}
}
static void
_entry_group_cb(AvahiEntryGroup *g, AvahiEntryGroupState state, void *userdata) {
AvahiSessionImplData *idata = userdata;
g_return_if_fail(g == idata->group || idata->group == NULL);
switch(state) {
case AVAHI_ENTRY_GROUP_ESTABLISHED:
purple_debug_info("bonjour", "Successfully registered service.\n");
break;
case AVAHI_ENTRY_GROUP_COLLISION:
purple_debug_error("bonjour", "Collision registering entry group.\n");
/* TODO: Handle error - this should log out the account. (Possibly with "wants to die")*/
break;
case AVAHI_ENTRY_GROUP_FAILURE:
purple_debug_error("bonjour", "Error registering entry group: %s\n.",
avahi_strerror(avahi_client_errno(avahi_entry_group_get_client(g))));
/* TODO: Handle error - this should log out the account.*/
break;
case AVAHI_ENTRY_GROUP_UNCOMMITED:
case AVAHI_ENTRY_GROUP_REGISTERING:
break;
}
}
static void
_buddy_icon_record_cb(AvahiRecordBrowser *b, AvahiIfIndex interface, AvahiProtocol protocol,
AvahiBrowserEvent event, const char *name, uint16_t clazz, uint16_t type,
const void *rdata, size_t size, AvahiLookupResultFlags flags, void *userdata) {
BonjourBuddy *buddy = userdata;
AvahiBuddyImplData *idata = buddy->mdns_impl_data;
switch (event) {
case AVAHI_BROWSER_CACHE_EXHAUSTED:
case AVAHI_BROWSER_ALL_FOR_NOW:
/* Ignore these "meta" informational events */
return;
case AVAHI_BROWSER_NEW:
bonjour_buddy_got_buddy_icon(buddy, rdata, size);
break;
case AVAHI_BROWSER_REMOVE:
case AVAHI_BROWSER_FAILURE:
purple_debug_error("bonjour", "Error retrieving buddy icon record: %s\n",
avahi_strerror(avahi_client_errno(avahi_record_browser_get_client(b))));
break;
}
/* Stop listening */
avahi_record_browser_free(b);
if (idata->buddy_icon_rec_browser == b) {
idata->buddy_icon_rec_browser = NULL;
}
}
/****************************
* mdns_interface functions *
****************************/
gboolean _mdns_init_session(BonjourDnsSd *data) {
AvahiSessionImplData *idata = g_new0(AvahiSessionImplData, 1);
const AvahiPoll *poll_api;
int error;
/* Tell avahi to use g_malloc and g_free */
avahi_set_allocator (avahi_glib_allocator ());
/* This currently depends on the glib mainloop,
* we should make it use the libpurple abstraction */
idata->glib_poll = avahi_glib_poll_new(NULL, G_PRIORITY_DEFAULT);
poll_api = avahi_glib_poll_get(idata->glib_poll);
idata->client = avahi_client_new(poll_api, 0, NULL, data, &error);
if (idata->client == NULL) {
purple_debug_error("bonjour", "Error initializing Avahi: %s\n", avahi_strerror(error));
avahi_glib_poll_free(idata->glib_poll);
g_free(idata);
return FALSE;
}
data->mdns_impl_data = idata;
bonjour_dns_sd_set_jid(data->account, avahi_client_get_host_name(idata->client));
return TRUE;
}
gboolean _mdns_publish(BonjourDnsSd *data, PublishType type, GSList *records) {
int publish_result = 0;
AvahiSessionImplData *idata = data->mdns_impl_data;
AvahiStringList *lst = NULL;
g_return_val_if_fail(idata != NULL, FALSE);
if (!idata->group) {
idata->group = avahi_entry_group_new(idata->client,
_entry_group_cb, idata);
if (!idata->group) {
purple_debug_error("bonjour",
"Unable to initialize the data for the mDNS (%s).\n",
avahi_strerror(avahi_client_errno(idata->client)));
return FALSE;
}
}
while (records) {
PurpleKeyValuePair *kvp = records->data;
lst = avahi_string_list_add_pair(lst, kvp->key, kvp->value);
records = records->next;
}
/* Publish the service */
switch (type) {
case PUBLISH_START:
publish_result = avahi_entry_group_add_service_strlst(
idata->group, AVAHI_IF_UNSPEC,
AVAHI_PROTO_UNSPEC, 0,
bonjour_get_jid(data->account),
LINK_LOCAL_RECORD_NAME, NULL, NULL, data->port_p2pj, lst);
break;
case PUBLISH_UPDATE:
publish_result = avahi_entry_group_update_service_txt_strlst(
idata->group, AVAHI_IF_UNSPEC,
AVAHI_PROTO_UNSPEC, 0,
bonjour_get_jid(data->account),
LINK_LOCAL_RECORD_NAME, NULL, lst);
break;
}
/* Free the memory used by temp data */
avahi_string_list_free(lst);
if (publish_result < 0) {
purple_debug_error("bonjour",
"Failed to add the " LINK_LOCAL_RECORD_NAME " service. Error: %s\n",
avahi_strerror(publish_result));
return FALSE;
}
if (type == PUBLISH_START
&& (publish_result = avahi_entry_group_commit(idata->group)) < 0) {
purple_debug_error("bonjour",
"Failed to commit " LINK_LOCAL_RECORD_NAME " service. Error: %s\n",
avahi_strerror(publish_result));
return FALSE;
}
return TRUE;
}
gboolean _mdns_browse(BonjourDnsSd *data) {
AvahiSessionImplData *idata = data->mdns_impl_data;
g_return_val_if_fail(idata != NULL, FALSE);
idata->sb = avahi_service_browser_new(idata->client, AVAHI_IF_UNSPEC, AVAHI_PROTO_UNSPEC, LINK_LOCAL_RECORD_NAME, NULL, 0, _browser_callback, data->account);
if (!idata->sb) {
purple_debug_error("bonjour",
"Unable to initialize service browser. Error: %s.\n",
avahi_strerror(avahi_client_errno(idata->client)));
return FALSE;
}
return TRUE;
}
gboolean _mdns_set_buddy_icon_data(BonjourDnsSd *data, gconstpointer avatar_data, gsize avatar_len) {
AvahiSessionImplData *idata = data->mdns_impl_data;
if (idata == NULL || idata->client == NULL)
return FALSE;
if (avatar_data != NULL) {
gboolean new_group = FALSE;
gchar *svc_name;
int ret;
AvahiPublishFlags flags = 0;
if (idata->buddy_icon_group == NULL) {
purple_debug_info("bonjour", "Setting new buddy icon.\n");
new_group = TRUE;
idata->buddy_icon_group = avahi_entry_group_new(idata->client,
_buddy_icon_group_cb, data);
} else {
purple_debug_info("bonjour", "Updating existing buddy icon.\n");
flags |= AVAHI_PUBLISH_UPDATE;
}
if (idata->buddy_icon_group == NULL) {
purple_debug_error("bonjour",
"Unable to initialize the buddy icon group (%s).\n",
avahi_strerror(avahi_client_errno(idata->client)));
return FALSE;
}
svc_name = g_strdup_printf("%s." LINK_LOCAL_RECORD_NAME "local",
bonjour_get_jid(data->account));
ret = avahi_entry_group_add_record(idata->buddy_icon_group, AVAHI_IF_UNSPEC,
AVAHI_PROTO_UNSPEC, flags, svc_name,
AVAHI_DNS_CLASS_IN, AVAHI_DNS_TYPE_NULL, 120, avatar_data, avatar_len);
g_free(svc_name);
if (ret < 0) {
purple_debug_error("bonjour",
"Failed to register buddy icon. Error: %s\n", avahi_strerror(ret));
if (new_group) {
avahi_entry_group_free(idata->buddy_icon_group);
idata->buddy_icon_group = NULL;
}
return FALSE;
}
if (new_group && (ret = avahi_entry_group_commit(idata->buddy_icon_group)) < 0) {
purple_debug_error("bonjour",
"Failed to commit buddy icon group. Error: %s\n", avahi_strerror(ret));
avahi_entry_group_free(idata->buddy_icon_group);
idata->buddy_icon_group = NULL;
return FALSE;
}
} else if (idata->buddy_icon_group != NULL) {
purple_debug_info("bonjour", "Removing existing buddy icon.\n");
avahi_entry_group_free(idata->buddy_icon_group);
idata->buddy_icon_group = NULL;
}
return TRUE;
}
void _mdns_stop(BonjourDnsSd *data) {
AvahiSessionImplData *idata = data->mdns_impl_data;
if (idata == NULL || idata->client == NULL)
return;
if (idata->sb != NULL)
avahi_service_browser_free(idata->sb);
avahi_client_free(idata->client);
avahi_glib_poll_free(idata->glib_poll);
g_free(idata);
data->mdns_impl_data = NULL;
}
void _mdns_init_buddy(BonjourBuddy *buddy) {
buddy->mdns_impl_data = g_new0(AvahiBuddyImplData, 1);
}
void _mdns_delete_buddy(BonjourBuddy *buddy) {
AvahiBuddyImplData *idata = buddy->mdns_impl_data;
g_return_if_fail(idata != NULL);
if (idata->buddy_icon_rec_browser != NULL)
avahi_record_browser_free(idata->buddy_icon_rec_browser);
g_slist_free_full(idata->resolvers, (GDestroyNotify)_cleanup_resolver_data);
g_free(idata);
buddy->mdns_impl_data = NULL;
}
void _mdns_retrieve_buddy_icon(BonjourBuddy* buddy) {
PurpleConnection *conn = purple_account_get_connection(buddy->account);
BonjourData *bd = purple_connection_get_protocol_data(conn);
AvahiSessionImplData *session_idata = bd->dns_sd_data->mdns_impl_data;
AvahiBuddyImplData *idata = buddy->mdns_impl_data;
gchar *name;
g_return_if_fail(idata != NULL);
if (idata->buddy_icon_rec_browser != NULL)
avahi_record_browser_free(idata->buddy_icon_rec_browser);
purple_debug_info("bonjour", "Retrieving buddy icon for '%s'.\n", buddy->name);
name = g_strdup_printf("%s." LINK_LOCAL_RECORD_NAME "local", buddy->name);
idata->buddy_icon_rec_browser = avahi_record_browser_new(session_idata->client, AVAHI_IF_UNSPEC,
AVAHI_PROTO_UNSPEC, name, AVAHI_DNS_CLASS_IN, AVAHI_DNS_TYPE_NULL, 0,
_buddy_icon_record_cb, buddy);
g_free(name);
if (!idata->buddy_icon_rec_browser) {
purple_debug_error("bonjour",
"Unable to initialize buddy icon record browser. Error: %s.\n",
avahi_strerror(avahi_client_errno(session_idata->client)));
}
}