pidgin/pidgin

Create PurplePerson.

19 months ago, Gary Kramlich
c22448f50d5d
Create PurplePerson.

This automatically manages the sorting of contacts and will report the highest
ranked one as the priority contact.

Testing Done:
Ran the unit tests

Bugs closed: PIDGIN-17668

Reviewed at https://reviews.imfreedom.org/r/1838/
/* purple
*
* Purple is the legal property of its developers, whose names are too numerous
* to list here. Please refer to the COPYRIGHT file distributed with this
* source distribution.
*
* 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 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 <gio/gio.h>
#include <libgupnp/gupnp-context-manager.h>
#include <libgupnp/gupnp-control-point.h>
#include <libgupnp/gupnp-service-info.h>
#include <libgupnp/gupnp-service-proxy.h>
#include <libgupnp-igd/gupnp-simple-igd.h>
#include "upnp.h"
#include "glibcompat.h"
#include "soupcompat.h"
#include "debug.h"
#include "eventloop.h"
#include "network.h"
#include "proxy.h"
#include "purplegio.h"
#include "signals.h"
#include "util.h"
#include "xmlnode.h"
/***************************************************************
** General Defines *
****************************************************************/
#define PORT_MAPPING_LEASE_TIME 0
#define PORT_MAPPING_DESCRIPTION "PURPLE_UPNP_PORT_FORWARD"
typedef enum {
PURPLE_UPNP_STATUS_UNDISCOVERED = -1,
PURPLE_UPNP_STATUS_UNABLE_TO_DISCOVER,
PURPLE_UPNP_STATUS_DISCOVERING,
PURPLE_UPNP_STATUS_DISCOVERED
} PurpleUPnPStatus;
typedef struct {
PurpleUPnPStatus status;
gchar *control_url;
gchar *service_type;
char publicip[16];
char internalip[16];
gint64 lookup_time;
} PurpleUPnPControlInfo;
typedef struct {
unsigned short portmap;
gchar protocol[4];
gboolean add;
PurpleUPnPCallback cb;
gpointer cb_data;
gboolean success;
guint tima; /* g_timeout_add handle */
} PurpleUPnPMappingAddRemove;
static PurpleUPnPControlInfo control_info = {
PURPLE_UPNP_STATUS_UNDISCOVERED,
NULL, "\0", "\0", "\0", 0};
static GUPnPContextManager *manager = NULL;
static GUPnPSimpleIgd *simple_igd = NULL;
static GSList *discovery_callbacks = NULL;
static void lookup_internal_ip(void);
static gboolean
fire_ar_cb_async_and_free(gpointer data)
{
PurpleUPnPMappingAddRemove *ar = data;
if (ar) {
if (ar->cb)
ar->cb(ar->success, ar->cb_data);
g_free(ar);
}
return FALSE;
}
static void
fire_discovery_callbacks(gboolean success)
{
while(discovery_callbacks) {
gpointer data;
PurpleUPnPCallback cb = discovery_callbacks->data;
discovery_callbacks = g_slist_delete_link(discovery_callbacks, discovery_callbacks);
data = discovery_callbacks->data;
discovery_callbacks = g_slist_delete_link(discovery_callbacks, discovery_callbacks);
cb(success, data);
}
}
static void
upnp_service_proxy_call_action_cb(GObject *source, GAsyncResult *result,
G_GNUC_UNUSED gpointer data)
{
GError *error = NULL;
char *ip = NULL;
GUPnPServiceProxyAction *action = NULL;
action = gupnp_service_proxy_call_action_finish(GUPNP_SERVICE_PROXY(source),
result, &error);
if (error != NULL) {
control_info.publicip[0] = '\0';
purple_debug_error("upnp",
"Failed to call GetExternalIPAddress action: %s",
error->message);
g_error_free(error);
return;
}
gupnp_service_proxy_action_get_result(action, &error,
"NewExternalIPAddress",
G_TYPE_STRING, &ip, NULL);
if (error == NULL) {
g_strlcpy(control_info.publicip, ip, sizeof(control_info.publicip));
purple_debug_info("upnp", "NAT Returned IP: %s", control_info.publicip);
g_free(ip);
} else {
control_info.publicip[0] = '\0';
purple_debug_error("upnp",
"Failed to get result from GetExternalIPAddress: %s",
error->message);
g_error_free(error);
}
gupnp_service_proxy_action_unref(action);
}
static void
upnp_service_proxy_available_cb(G_GNUC_UNUSED GUPnPControlPoint *cp,
GUPnPServiceProxy *proxy,
G_GNUC_UNUSED gpointer data)
{
gchar *control_url = NULL;
const gchar *service_type = NULL;
control_url = gupnp_service_info_get_control_url(GUPNP_SERVICE_INFO(proxy));
service_type = gupnp_service_info_get_service_type(GUPNP_SERVICE_INFO(proxy));
purple_debug_info("upnp",
"Service proxy available for %s on control URL %s",
service_type,
control_url);
control_info.lookup_time = g_get_monotonic_time();
control_info.control_url = control_url;
control_info.service_type = g_strdup(service_type);
if(control_url) {
GUPnPServiceProxyAction *action = NULL;
control_info.status = PURPLE_UPNP_STATUS_DISCOVERED;
fire_discovery_callbacks(TRUE);
lookup_internal_ip();
action = gupnp_service_proxy_action_new("GetExternalIPAddress", NULL);
gupnp_service_proxy_call_action_async(proxy, action, NULL,
upnp_service_proxy_call_action_cb,
NULL);
} else {
control_info.status = PURPLE_UPNP_STATUS_UNABLE_TO_DISCOVER;
fire_discovery_callbacks(FALSE);
}
}
static void
upnp_context_unavailable_cb(G_GNUC_UNUSED GUPnPContextManager *manager,
GUPnPContext *context, G_GNUC_UNUSED gpointer data)
{
purple_debug_info("upnp",
"UPnP context no longer available for interface %s",
gssdp_client_get_interface(GSSDP_CLIENT(context)));
/* Delete these to prevent them owning refs back on the context. */
g_object_set_data(G_OBJECT(context), "WANIPConnection", NULL);
g_object_set_data(G_OBJECT(context), "WANPPPConnection", NULL);
}
static void
upnp_context_available_cb(G_GNUC_UNUSED GUPnPContextManager *manager,
GUPnPContext *context, G_GNUC_UNUSED gpointer data)
{
GUPnPControlPoint *cp;
purple_debug_info("upnp", "UPnP context now available for interface %s",
gssdp_client_get_interface(GSSDP_CLIENT(context)));
cp = gupnp_control_point_new(context,
"urn:schemas-upnp-org:service:WANIPConnection:1");
g_signal_connect(cp, "service-proxy-available",
G_CALLBACK(upnp_service_proxy_available_cb), NULL);
gssdp_resource_browser_set_active(GSSDP_RESOURCE_BROWSER(cp), TRUE);
g_object_set_data_full(G_OBJECT(context), "WANIPConnection", cp,
g_object_unref);
cp = gupnp_control_point_new(context,
"urn:schemas-upnp-org:service:WANPPPConnection:1");
g_signal_connect(cp, "service-proxy-available",
G_CALLBACK(upnp_service_proxy_available_cb), NULL);
gssdp_resource_browser_set_active(GSSDP_RESOURCE_BROWSER(cp), TRUE);
g_object_set_data_full(G_OBJECT(context), "WANPPPConnection", cp,
g_object_unref);
}
void
purple_upnp_discover(PurpleUPnPCallback cb, gpointer cb_data)
{
if (cb) {
discovery_callbacks = g_slist_append(discovery_callbacks, cb);
discovery_callbacks = g_slist_append(discovery_callbacks, cb_data);
}
if (control_info.status == PURPLE_UPNP_STATUS_DISCOVERING) {
return;
}
purple_debug_info("upnp",
"Starting discovery on all available interfaces");
g_clear_object(&manager);
manager = gupnp_context_manager_create(0);
g_signal_connect(manager, "context-available",
G_CALLBACK(upnp_context_available_cb), NULL);
g_signal_connect(manager, "context-unavailable",
G_CALLBACK(upnp_context_unavailable_cb), NULL);
control_info.status = PURPLE_UPNP_STATUS_DISCOVERING;
}
const gchar *
purple_upnp_get_public_ip()
{
if (control_info.status == PURPLE_UPNP_STATUS_DISCOVERED
&& *control_info.publicip)
return control_info.publicip;
/* Trigger another UPnP discovery if 5 minutes have elapsed since the
* last one, and it wasn't successful */
if (control_info.status < PURPLE_UPNP_STATUS_DISCOVERING &&
(g_get_monotonic_time() - control_info.lookup_time) >
300 * G_USEC_PER_SEC) {
purple_upnp_discover(NULL, NULL);
}
return NULL;
}
/* TODO: This could be exported */
static const gchar *
purple_upnp_get_internal_ip(void)
{
if (control_info.status == PURPLE_UPNP_STATUS_DISCOVERED
&& *control_info.internalip)
return control_info.internalip;
/* Trigger another UPnP discovery if 5 minutes have elapsed since the
* last one, and it wasn't successful */
if (control_info.status < PURPLE_UPNP_STATUS_DISCOVERING &&
(g_get_monotonic_time() - control_info.lookup_time) >
300 * G_USEC_PER_SEC) {
purple_upnp_discover(NULL, NULL);
}
return NULL;
}
static void
looked_up_internal_ip_cb(GObject *source, GAsyncResult *result,
G_GNUC_UNUSED gpointer user_data)
{
GSocketConnection *conn;
GSocketAddress *addr;
GInetSocketAddress *inetsockaddr;
GError *error = NULL;
conn = g_socket_client_connect_to_host_finish(G_SOCKET_CLIENT(source),
result, &error);
if (conn == NULL) {
purple_debug_error("upnp", "Unable to look up local IP: %s",
error->message);
g_clear_error(&error);
return;
}
g_strlcpy(control_info.internalip, "0.0.0.0",
sizeof(control_info.internalip));
addr = g_socket_connection_get_local_address(conn, &error);
if ((inetsockaddr = G_INET_SOCKET_ADDRESS(addr)) != NULL) {
GInetAddress *inetaddr =
g_inet_socket_address_get_address(inetsockaddr);
if (g_inet_address_get_family(inetaddr) == G_SOCKET_FAMILY_IPV4 &&
!g_inet_address_get_is_loopback(inetaddr))
{
gchar *ip = g_inet_address_to_string(inetaddr);
g_strlcpy(control_info.internalip, ip,
sizeof(control_info.internalip));
g_free(ip);
}
} else {
purple_debug_error(
"upnp", "Unable to get local address of connection: %s",
error ? error->message : "unknown socket address type");
g_clear_error(&error);
}
g_object_unref(addr);
purple_debug_info("upnp", "Local IP: %s", control_info.internalip);
g_object_unref(conn);
}
static void
lookup_internal_ip(void)
{
gchar *host;
gint port;
GSocketClient *client;
GError *error = NULL;
if (!g_uri_split_network(control_info.control_url, G_URI_FLAGS_NONE, NULL,
&host, &port, &error))
{
purple_debug_error("upnp",
"lookup_internal_ip(): Failed In Parse URL: %s",
error->message);
return;
}
client = purple_gio_socket_client_new(NULL, &error);
if (client == NULL) {
purple_debug_error("upnp", "Get Local IP Connect to %s:%d Failed: %s",
host, port, error->message);
g_clear_error(&error);
g_free(host);
return;
}
purple_debug_info("upnp", "Attempting connection to %s:%u\n", host, port);
g_socket_client_connect_to_host_async(client, host, port, NULL,
looked_up_internal_ip_cb, NULL);
g_object_unref(client);
g_free(host);
}
static void
upnp_mapped_external_port_cb(GUPnPSimpleIgd *igd, const gchar *protocol,
G_GNUC_UNUSED const gchar *external_ip,
G_GNUC_UNUSED const gchar *replaces_external_ip,
guint16 external_port,
G_GNUC_UNUSED const gchar *local_ip,
guint16 local_port,
G_GNUC_UNUSED const gchar *description,
gpointer data)
{
PurpleUPnPMappingAddRemove *ar = data;
if(!purple_strequal(ar->protocol, protocol)) {
return;
}
if(external_port != ar->portmap) {
return;
}
purple_debug_info("upnp", "Successfully completed port mapping operation");
ar->success = TRUE;
ar->tima = g_timeout_add(0, fire_ar_cb_async_and_free, ar);
g_signal_handlers_disconnect_by_data(igd, ar);
}
static void
upnp_error_mapping_port_cb(GUPnPSimpleIgd *igd, GError *error,
const gchar *protocol,
guint16 external_port,
G_GNUC_UNUSED const gchar *local_ip,
G_GNUC_UNUSED guint16 local_port,
G_GNUC_UNUSED const gchar *description,
gpointer data)
{
PurpleUPnPMappingAddRemove *ar = data;
if(!purple_strequal(ar->protocol, protocol)) {
return;
}
if(external_port != ar->portmap) {
return;
}
purple_debug_error("upnp", "purple_upnp_set_port_mapping(): Failed: %s",
error->message);
ar->success = FALSE;
ar->tima = g_timeout_add(0, fire_ar_cb_async_and_free, ar);
g_signal_handlers_disconnect_by_data(igd, ar);
}
static void
do_port_mapping_cb(gboolean has_control_mapping, gpointer data)
{
PurpleUPnPMappingAddRemove *ar = data;
if (!has_control_mapping) {
ar->success = FALSE;
ar->tima = g_timeout_add(0, fire_ar_cb_async_and_free, ar);
return;
}
if(ar->add) {
const gchar *internal_ip;
/* get the internal IP */
if(!(internal_ip = purple_upnp_get_internal_ip())) {
purple_debug_error("upnp",
"purple_upnp_set_port_mapping(): couldn't get local ip\n");
ar->success = FALSE;
ar->tima = g_timeout_add(0, fire_ar_cb_async_and_free, ar);
return;
}
if(simple_igd == NULL) {
simple_igd = gupnp_simple_igd_new();
}
gupnp_simple_igd_add_port(simple_igd, ar->protocol, ar->portmap,
internal_ip, ar->portmap,
PORT_MAPPING_LEASE_TIME,
PORT_MAPPING_DESCRIPTION);
} else {
if(simple_igd == NULL) {
simple_igd = gupnp_simple_igd_new();
}
gupnp_simple_igd_remove_port(simple_igd, ar->protocol, ar->portmap);
}
g_signal_connect(simple_igd, "mapped-external-port",
G_CALLBACK(upnp_mapped_external_port_cb), ar);
g_signal_connect(simple_igd, "error-mapping-port",
G_CALLBACK(upnp_error_mapping_port_cb), ar);
}
static gboolean
fire_port_mapping_failure_cb(gpointer data)
{
PurpleUPnPMappingAddRemove *ar = data;
ar->tima = 0;
do_port_mapping_cb(FALSE, data);
return FALSE;
}
void
purple_upnp_set_port_mapping(unsigned short portmap, const gchar *protocol,
PurpleUPnPCallback cb, gpointer cb_data)
{
PurpleUPnPMappingAddRemove *ar;
ar = g_new0(PurpleUPnPMappingAddRemove, 1);
ar->cb = cb;
ar->cb_data = cb_data;
ar->add = TRUE;
ar->portmap = portmap;
g_strlcpy(ar->protocol, protocol, sizeof(ar->protocol));
switch(control_info.status) {
case PURPLE_UPNP_STATUS_UNABLE_TO_DISCOVER:
if (g_get_monotonic_time() - control_info.lookup_time >
300 * G_USEC_PER_SEC) {
/* If we haven't had a successful UPnP discovery, check if 5 minutes
* has elapsed since the last try, try again */
purple_upnp_discover(do_port_mapping_cb, ar);
} else if (cb) {
/* Asynchronously trigger a failed response */
ar->tima = g_timeout_add(10, fire_port_mapping_failure_cb, ar);
} else {
/* No need to do anything if nobody expects a response*/
g_free(ar);
ar = NULL;
}
break;
case PURPLE_UPNP_STATUS_UNDISCOVERED:
case PURPLE_UPNP_STATUS_DISCOVERING:
purple_upnp_discover(do_port_mapping_cb, ar);
break;
case PURPLE_UPNP_STATUS_DISCOVERED:
default:
do_port_mapping_cb(TRUE, ar);
break;
}
}
void
purple_upnp_remove_port_mapping(unsigned short portmap, const char *protocol,
PurpleUPnPCallback cb, gpointer cb_data)
{
PurpleUPnPMappingAddRemove *ar;
ar = g_new0(PurpleUPnPMappingAddRemove, 1);
ar->cb = cb;
ar->cb_data = cb_data;
ar->add = FALSE;
ar->portmap = portmap;
g_strlcpy(ar->protocol, protocol, sizeof(ar->protocol));
switch(control_info.status) {
case PURPLE_UPNP_STATUS_UNABLE_TO_DISCOVER:
if (g_get_monotonic_time() - control_info.lookup_time >
300 * G_USEC_PER_SEC) {
/* If we haven't had a successful UPnP discovery, check if 5 minutes
* has elapsed since the last try, try again */
purple_upnp_discover(do_port_mapping_cb, ar);
} else if (cb) {
/* Asynchronously trigger a failed response */
ar->tima = g_timeout_add(10, fire_port_mapping_failure_cb, ar);
} else {
/* No need to do anything if nobody expects a response*/
g_free(ar);
ar = NULL;
}
break;
case PURPLE_UPNP_STATUS_DISCOVERING:
case PURPLE_UPNP_STATUS_UNDISCOVERED:
purple_upnp_discover(do_port_mapping_cb, ar);
break;
case PURPLE_UPNP_STATUS_DISCOVERED:
default:
do_port_mapping_cb(TRUE, ar);
break;
}
}
static void
purple_upnp_network_config_changed_cb(GNetworkMonitor *monitor, gboolean available, gpointer data)
{
/* Reset the control_info to default values */
control_info.status = PURPLE_UPNP_STATUS_UNDISCOVERED;
g_clear_pointer(&control_info.control_url, g_free);
g_clear_pointer(&control_info.service_type, g_free);
control_info.publicip[0] = '\0';
control_info.internalip[0] = '\0';
control_info.lookup_time = 0;
}
void
purple_upnp_init()
{
g_signal_connect(g_network_monitor_get_default(),
"network-changed",
G_CALLBACK(purple_upnp_network_config_changed_cb),
NULL);
}
void
purple_upnp_uninit(void)
{
g_clear_object(&simple_igd);
g_clear_pointer(&control_info.control_url, g_free);
g_clear_pointer(&control_info.service_type, g_free);
g_clear_object(&manager);
}