diff libpurple/upnp.c @ 15373:5fe8042783c1

Rename gtk/ and libgaim/ to pidgin/ and libpurple/
author Sean Egan <seanegan@gmail.com>
date Sat, 20 Jan 2007 02:32:10 +0000
parents
children 56a2a0bb290a
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libpurple/upnp.c	Sat Jan 20 02:32:10 2007 +0000
@@ -0,0 +1,993 @@
+/**
+ * @file upnp.c UPnP Implementation
+ * @ingroup core
+ *
+ * gaim
+ *
+ * Gaim 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., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ */
+#include "internal.h"
+
+#include "debug.h"
+#include "util.h"
+#include "proxy.h"
+#include "xmlnode.h"
+#include "network.h"
+#include "eventloop.h"
+#include "upnp.h"
+
+
+/***************************************************************
+** General Defines                                             *
+****************************************************************/
+#define HTTP_OK "200 OK"
+#define DEFAULT_HTTP_PORT 80
+#define DISCOVERY_TIMEOUT 1000
+
+/***************************************************************
+** Discovery/Description Defines                               *
+****************************************************************/
+#define NUM_UDP_ATTEMPTS 2
+
+/* Address and port of an SSDP request used for discovery */
+#define HTTPMU_HOST_ADDRESS "239.255.255.250"
+#define HTTPMU_HOST_PORT 1900
+
+#define SEARCH_REQUEST_DEVICE "urn:schemas-upnp-org:service:%s"
+
+#define SEARCH_REQUEST_STRING \
+	"M-SEARCH * HTTP/1.1\r\n" \
+	"MX: 2\r\n" \
+	"HOST: 239.255.255.250:1900\r\n" \
+	"MAN: \"ssdp:discover\"\r\n" \
+	"ST: urn:schemas-upnp-org:service:%s\r\n" \
+	"\r\n"
+
+#define WAN_IP_CONN_SERVICE "WANIPConnection:1"
+#define WAN_PPP_CONN_SERVICE "WANPPPConnection:1"
+
+/******************************************************************
+** Action Defines                                                 *
+*******************************************************************/
+#define HTTP_HEADER_ACTION \
+	"POST /%s HTTP/1.1\r\n" \
+	"HOST: %s:%d\r\n" \
+	"SOAPACTION: \"urn:schemas-upnp-org:service:%s#%s\"\r\n" \
+	"CONTENT-TYPE: text/xml ; charset=\"utf-8\"\r\n" \
+	"CONTENT-LENGTH: %" G_GSIZE_FORMAT "\r\n\r\n"
+
+#define SOAP_ACTION \
+	"<?xml version=\"1.0\" encoding=\"utf-8\"?>\r\n" \
+	"<s:Envelope xmlns:s=\"http://schemas.xmlsoap.org/soap/envelope/\" " \
+		"s:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\">\r\n" \
+	  "<s:Body>\r\n" \
+	    "<u:%s xmlns:u=\"urn:schemas-upnp-org:service:%s\">\r\n" \
+	      "%s" \
+	    "</u:%s>\r\n" \
+	  "</s:Body>\r\n" \
+	"</s:Envelope>"
+
+#define PORT_MAPPING_LEASE_TIME "0"
+#define PORT_MAPPING_DESCRIPTION "GAIM_UPNP_PORT_FORWARD"
+
+#define ADD_PORT_MAPPING_PARAMS \
+	"<NewRemoteHost></NewRemoteHost>\r\n" \
+	"<NewExternalPort>%i</NewExternalPort>\r\n" \
+	"<NewProtocol>%s</NewProtocol>\r\n" \
+	"<NewInternalPort>%i</NewInternalPort>\r\n" \
+	"<NewInternalClient>%s</NewInternalClient>\r\n" \
+	"<NewEnabled>1</NewEnabled>\r\n" \
+	"<NewPortMappingDescription>" \
+	PORT_MAPPING_DESCRIPTION \
+	"</NewPortMappingDescription>\r\n" \
+	"<NewLeaseDuration>" \
+	PORT_MAPPING_LEASE_TIME \
+	"</NewLeaseDuration>\r\n"
+
+#define DELETE_PORT_MAPPING_PARAMS \
+	"<NewRemoteHost></NewRemoteHost>\r\n" \
+	"<NewExternalPort>%i</NewExternalPort>\r\n" \
+	"<NewProtocol>%s</NewProtocol>\r\n"
+
+typedef enum {
+	GAIM_UPNP_STATUS_UNDISCOVERED = -1,
+	GAIM_UPNP_STATUS_UNABLE_TO_DISCOVER,
+	GAIM_UPNP_STATUS_DISCOVERING,
+	GAIM_UPNP_STATUS_DISCOVERED
+} GaimUPnPStatus;
+
+typedef struct {
+	GaimUPnPStatus status;
+	gchar* control_url;
+	gchar service_type[20];
+	char publicip[16];
+	char internalip[16];
+	time_t lookup_time;
+} GaimUPnPControlInfo;
+
+typedef struct {
+	guint inpa;	/* gaim_input_add handle */
+	guint tima;	/* gaim_timeout_add handle */
+	int fd;
+	struct sockaddr_in server;
+	gchar service_type[25];
+	int retry_count;
+	gchar *full_url;
+} UPnPDiscoveryData;
+
+typedef struct {
+	unsigned short portmap;
+	gchar protocol[4];
+	gboolean add;
+	GaimUPnPCallback cb;
+	gpointer cb_data;
+} UPnPMappingAddRemove;
+
+static GaimUPnPControlInfo control_info = {
+	GAIM_UPNP_STATUS_UNDISCOVERED,
+	NULL, "\0", "\0", "\0", 0};
+
+static GSList *discovery_callbacks = NULL;
+
+static void gaim_upnp_discover_send_broadcast(UPnPDiscoveryData *dd);
+static void lookup_public_ip(void);
+static void lookup_internal_ip(void);
+
+static void
+fire_discovery_callbacks(gboolean success)
+{
+	while(discovery_callbacks) {
+		gpointer data;
+		GaimUPnPCallback cb = discovery_callbacks->data;
+		discovery_callbacks = g_slist_remove(discovery_callbacks, cb);
+		data = discovery_callbacks->data;
+		discovery_callbacks = g_slist_remove(discovery_callbacks, data);
+		cb(success, data);
+	}
+}
+
+static gboolean
+gaim_upnp_compare_device(const xmlnode* device, const gchar* deviceType)
+{
+	xmlnode* deviceTypeNode = xmlnode_get_child(device, "deviceType");
+	char *tmp;
+	gboolean ret;
+
+	if(deviceTypeNode == NULL) {
+		return FALSE;
+	}
+
+	tmp = xmlnode_get_data(deviceTypeNode);
+	ret = !g_ascii_strcasecmp(tmp, deviceType);
+	g_free(tmp);
+
+	return ret;
+}
+
+static gboolean
+gaim_upnp_compare_service(const xmlnode* service, const gchar* serviceType)
+{
+	xmlnode* serviceTypeNode;
+	char *tmp;
+	gboolean ret;
+
+	if(service == NULL) {
+		return FALSE;
+	}
+
+	serviceTypeNode = xmlnode_get_child(service, "serviceType");
+
+	if(serviceTypeNode == NULL) {
+		return FALSE;
+	}
+
+	tmp = xmlnode_get_data(serviceTypeNode);
+	ret = !g_ascii_strcasecmp(tmp, serviceType);
+	g_free(tmp);
+
+	return ret;
+}
+
+static gchar*
+gaim_upnp_parse_description_response(const gchar* httpResponse, gsize len,
+	const gchar* httpURL, const gchar* serviceType)
+{
+	gchar *xmlRoot, *baseURL, *controlURL, *service;
+	xmlnode *xmlRootNode, *serviceTypeNode, *controlURLNode, *baseURLNode;
+	char *tmp;
+
+	/* make sure we have a valid http response */
+	if(g_strstr_len(httpResponse, len, HTTP_OK) == NULL) {
+		gaim_debug_error("upnp",
+			"parse_description_response(): Failed In HTTP_OK\n");
+		return NULL;
+	}
+
+	/* find the root of the xml document */
+	if((xmlRoot = g_strstr_len(httpResponse, len, "<root")) == NULL) {
+		gaim_debug_error("upnp",
+			"parse_description_response(): Failed finding root\n");
+		return NULL;
+	}
+
+	/* create the xml root node */
+	if((xmlRootNode = xmlnode_from_str(xmlRoot,
+			len - (xmlRoot - httpResponse))) == NULL) {
+		gaim_debug_error("upnp",
+			"parse_description_response(): Could not parse xml root node\n");
+		return NULL;
+	}
+
+	/* get the baseURL of the device */
+	if((baseURLNode = xmlnode_get_child(xmlRootNode, "URLBase")) != NULL) {
+		baseURL = xmlnode_get_data(baseURLNode);
+	} else {
+		baseURL = g_strdup(httpURL);
+	}
+
+	/* get the serviceType child that has the service type as its data */
+
+	/* get urn:schemas-upnp-org:device:InternetGatewayDevice:1 and its devicelist */
+	serviceTypeNode = xmlnode_get_child(xmlRootNode, "device");
+	while(!gaim_upnp_compare_device(serviceTypeNode,
+			"urn:schemas-upnp-org:device:InternetGatewayDevice:1") &&
+			serviceTypeNode != NULL) {
+		serviceTypeNode = xmlnode_get_next_twin(serviceTypeNode);
+	}
+	if(serviceTypeNode == NULL) {
+		gaim_debug_error("upnp",
+			"parse_description_response(): could not get serviceTypeNode 1\n");
+		g_free(baseURL);
+		xmlnode_free(xmlRootNode);
+		return NULL;
+	}
+	serviceTypeNode = xmlnode_get_child(serviceTypeNode, "deviceList");
+	if(serviceTypeNode == NULL) {
+		gaim_debug_error("upnp",
+			"parse_description_response(): could not get serviceTypeNode 2\n");
+		g_free(baseURL);
+		xmlnode_free(xmlRootNode);
+		return NULL;
+	}
+
+	/* get urn:schemas-upnp-org:device:WANDevice:1 and its devicelist */
+	serviceTypeNode = xmlnode_get_child(serviceTypeNode, "device");
+	while(!gaim_upnp_compare_device(serviceTypeNode,
+			"urn:schemas-upnp-org:device:WANDevice:1") &&
+			serviceTypeNode != NULL) {
+		serviceTypeNode = xmlnode_get_next_twin(serviceTypeNode);
+	}
+	if(serviceTypeNode == NULL) {
+		gaim_debug_error("upnp",
+			"parse_description_response(): could not get serviceTypeNode 3\n");
+		g_free(baseURL);
+		xmlnode_free(xmlRootNode);
+		return NULL;
+	}
+	serviceTypeNode = xmlnode_get_child(serviceTypeNode, "deviceList");
+	if(serviceTypeNode == NULL) {
+		gaim_debug_error("upnp",
+			"parse_description_response(): could not get serviceTypeNode 4\n");
+		g_free(baseURL);
+		xmlnode_free(xmlRootNode);
+		return NULL;
+	}
+
+	/* get urn:schemas-upnp-org:device:WANConnectionDevice:1 and its servicelist */
+	serviceTypeNode = xmlnode_get_child(serviceTypeNode, "device");
+	while(serviceTypeNode && !gaim_upnp_compare_device(serviceTypeNode,
+			"urn:schemas-upnp-org:device:WANConnectionDevice:1")) {
+		serviceTypeNode = xmlnode_get_next_twin(serviceTypeNode);
+	}
+	if(serviceTypeNode == NULL) {
+		gaim_debug_error("upnp",
+			"parse_description_response(): could not get serviceTypeNode 5\n");
+		g_free(baseURL);
+		xmlnode_free(xmlRootNode);
+		return NULL;
+	}
+	serviceTypeNode = xmlnode_get_child(serviceTypeNode, "serviceList");
+	if(serviceTypeNode == NULL) {
+		gaim_debug_error("upnp",
+			"parse_description_response(): could not get serviceTypeNode 6\n");
+		g_free(baseURL);
+		xmlnode_free(xmlRootNode);
+		return NULL;
+	}
+
+	/* get the serviceType variable passed to this function */
+	service = g_strdup_printf(SEARCH_REQUEST_DEVICE, serviceType);
+	serviceTypeNode = xmlnode_get_child(serviceTypeNode, "service");
+	while(!gaim_upnp_compare_service(serviceTypeNode, service) &&
+			serviceTypeNode != NULL) {
+		serviceTypeNode = xmlnode_get_next_twin(serviceTypeNode);
+	}
+
+	g_free(service);
+	if(serviceTypeNode == NULL) {
+		gaim_debug_error("upnp",
+			"parse_description_response(): could not get serviceTypeNode 7\n");
+		g_free(baseURL);
+		xmlnode_free(xmlRootNode);
+		return NULL;
+	}
+
+	/* get the controlURL of the service */
+	if((controlURLNode = xmlnode_get_child(serviceTypeNode,
+			"controlURL")) == NULL) {
+		gaim_debug_error("upnp",
+			"parse_description_response(): Could not find controlURL\n");
+		g_free(baseURL);
+		xmlnode_free(xmlRootNode);
+		return NULL;
+	}
+
+	tmp = xmlnode_get_data(controlURLNode);
+	if(baseURL && !gaim_str_has_prefix(tmp, "http://") &&
+	   !gaim_str_has_prefix(tmp, "HTTP://")) {
+		/* Handle absolute paths in a relative URL.  This probably
+		 * belongs in util.c. */
+		if (tmp[0] == '/') {
+			size_t length;
+			const char *path, *start = strstr(baseURL, "://");
+			start = start ? start + 3 : baseURL;
+			path = strchr(start, '/');
+			length = path ? path - baseURL : strlen(baseURL);
+			controlURL = g_strdup_printf("%.*s%s", length, baseURL, tmp);
+		} else {
+			controlURL = g_strdup_printf("%s%s", baseURL, tmp);
+		}
+		g_free(tmp);
+	}else{
+		controlURL = tmp;
+	}
+	g_free(baseURL);
+	xmlnode_free(xmlRootNode);
+
+	return controlURL;
+}
+
+static void
+upnp_parse_description_cb(GaimUtilFetchUrlData *url_data, gpointer user_data,
+		const gchar *httpResponse, gsize len, const gchar *error_message)
+{
+	UPnPDiscoveryData *dd = user_data;
+	gchar *control_url = NULL;
+
+	if (len > 0)
+		control_url = gaim_upnp_parse_description_response(
+			httpResponse, len, dd->full_url, dd->service_type);
+
+	g_free(dd->full_url);
+
+	if(control_url == NULL) {
+		gaim_debug_error("upnp",
+			"gaim_upnp_parse_description(): control URL is NULL\n");
+	}
+
+	control_info.status = control_url ? GAIM_UPNP_STATUS_DISCOVERED
+		: GAIM_UPNP_STATUS_UNABLE_TO_DISCOVER;
+	control_info.lookup_time = time(NULL);
+	control_info.control_url = control_url;
+	strncpy(control_info.service_type, dd->service_type,
+		sizeof(control_info.service_type));
+
+	fire_discovery_callbacks(control_url != NULL);
+
+	/* Look up the public and internal IPs */
+	if(control_url != NULL) {
+		lookup_public_ip();
+		lookup_internal_ip();
+	}
+
+	g_free(dd);
+}
+
+static void
+gaim_upnp_parse_description(const gchar* descriptionURL, UPnPDiscoveryData *dd)
+{
+	gchar* httpRequest;
+	gchar* descriptionXMLAddress;
+	gchar* descriptionAddress;
+	int port = 0;
+
+	/* parse the 4 above variables out of the descriptionURL
+	   example description URL: http://192.168.1.1:5678/rootDesc.xml */
+
+	/* parse the url into address, port, path variables */
+	if(!gaim_url_parse(descriptionURL, &descriptionAddress,
+			&port, &descriptionXMLAddress, NULL, NULL)) {
+		return;
+	}
+	if(port == 0 || port == -1) {
+		port = DEFAULT_HTTP_PORT;
+	}
+
+	/* for example...
+	   GET /rootDesc.xml HTTP/1.1\r\nHost: 192.168.1.1:5678\r\n\r\n */
+	httpRequest = g_strdup_printf(
+		"GET /%s HTTP/1.1\r\n"
+		"Connection: close\r\n"
+		"Host: %s:%d\r\n\r\n",
+		descriptionXMLAddress, descriptionAddress, port);
+
+	g_free(descriptionXMLAddress);
+
+	dd->full_url = g_strdup_printf("http://%s:%d",
+			descriptionAddress, port);
+	g_free(descriptionAddress);
+
+	/* Remove the timeout because everything it is waiting for has
+	 * successfully completed */
+	gaim_timeout_remove(dd->tima);
+	dd->tima = 0;
+
+	gaim_util_fetch_url_request(descriptionURL, TRUE, NULL, TRUE, httpRequest,
+			TRUE, upnp_parse_description_cb, dd);
+
+	g_free(httpRequest);
+
+}
+
+static void
+gaim_upnp_parse_discover_response(const gchar* buf, unsigned int buf_len,
+	UPnPDiscoveryData *dd)
+{
+	gchar* startDescURL;
+	gchar* endDescURL;
+	gchar* descURL;
+
+	if(g_strstr_len(buf, buf_len, HTTP_OK) == NULL) {
+		gaim_debug_error("upnp",
+			"parse_discover_response(): Failed In HTTP_OK\n");
+		return;
+	}
+
+	if((startDescURL = g_strstr_len(buf, buf_len, "http://")) == NULL) {
+		gaim_debug_error("upnp",
+			"parse_discover_response(): Failed In finding http://\n");
+		return;
+	}
+
+	endDescURL = g_strstr_len(startDescURL, buf_len - (startDescURL - buf),
+			"\r");
+	if(endDescURL == NULL) {
+		endDescURL = g_strstr_len(startDescURL,
+				buf_len - (startDescURL - buf), "\n");
+		if(endDescURL == NULL) {
+			gaim_debug_error("upnp",
+				"parse_discover_response(): Failed In endDescURL\n");
+			return;
+		}
+	}
+
+	/* XXX: I'm not sure how this could ever happen */
+	if(endDescURL == startDescURL) {
+		gaim_debug_error("upnp",
+			"parse_discover_response(): endDescURL == startDescURL\n");
+		return;
+	}
+
+	descURL = g_strndup(startDescURL, endDescURL - startDescURL);
+
+	gaim_upnp_parse_description(descURL, dd);
+
+	g_free(descURL);
+
+}
+
+static gboolean
+gaim_upnp_discover_timeout(gpointer data)
+{
+	UPnPDiscoveryData* dd = data;
+
+	if (dd->inpa)
+		gaim_input_remove(dd->inpa);
+	dd->inpa = 0;
+	dd->tima = 0;
+
+	if (dd->retry_count < NUM_UDP_ATTEMPTS) {
+		/* TODO: We probably shouldn't be incrementing retry_count in two places */
+		dd->retry_count++;
+		gaim_upnp_discover_send_broadcast(dd);
+	} else {
+		if (dd->fd)
+			close(dd->fd);
+
+		control_info.status = GAIM_UPNP_STATUS_UNABLE_TO_DISCOVER;
+		control_info.lookup_time = time(NULL);
+		control_info.service_type[0] = '\0';
+		g_free(control_info.control_url);
+		control_info.control_url = NULL;
+
+		fire_discovery_callbacks(FALSE);
+
+		g_free(dd);
+	}
+
+	return FALSE;
+}
+
+static void
+gaim_upnp_discover_udp_read(gpointer data, gint sock, GaimInputCondition cond)
+{
+	int len;
+	UPnPDiscoveryData *dd = data;
+	gchar buf[65536];
+
+	do {
+		len = recv(dd->fd, buf,
+			sizeof(buf) - 1, 0);
+
+		if(len > 0) {
+			buf[len] = '\0';
+			break;
+		} else if(errno != EINTR) {
+			/* We'll either get called again, or time out */
+			return;
+		}
+	} while (errno == EINTR);
+
+	gaim_input_remove(dd->inpa);
+	dd->inpa = 0;
+
+	close(dd->fd);
+	dd->fd = 0;
+
+	/* parse the response, and see if it was a success */
+	gaim_upnp_parse_discover_response(buf, len, dd);
+
+	/* We'll either time out or continue successfully */
+}
+
+static void
+gaim_upnp_discover_send_broadcast(UPnPDiscoveryData *dd)
+{
+	gchar *sendMessage = NULL;
+	gsize totalSize;
+	gboolean sentSuccess;
+
+	/* because we are sending over UDP, if there is a failure
+	   we should retry the send NUM_UDP_ATTEMPTS times. Also,
+	   try different requests for WANIPConnection and WANPPPConnection*/
+	for(; dd->retry_count < NUM_UDP_ATTEMPTS; dd->retry_count++) {
+		sentSuccess = FALSE;
+
+		if((dd->retry_count % 2) == 0) {
+			strncpy(dd->service_type, WAN_IP_CONN_SERVICE, sizeof(dd->service_type));
+		} else {
+			strncpy(dd->service_type, WAN_PPP_CONN_SERVICE, sizeof(dd->service_type));
+		}
+
+		sendMessage = g_strdup_printf(SEARCH_REQUEST_STRING, dd->service_type);
+
+		totalSize = strlen(sendMessage);
+
+		do {
+			if(sendto(dd->fd, sendMessage, totalSize, 0,
+					(struct sockaddr*) &(dd->server),
+					sizeof(struct sockaddr_in)
+					) == totalSize) {
+				sentSuccess = TRUE;
+				break;
+			}
+		} while (errno == EINTR || errno == EAGAIN);
+
+		g_free(sendMessage);
+
+		if(sentSuccess) {
+			dd->tima = gaim_timeout_add(DISCOVERY_TIMEOUT,
+				gaim_upnp_discover_timeout, dd);
+			dd->inpa = gaim_input_add(dd->fd, GAIM_INPUT_READ,
+				gaim_upnp_discover_udp_read, dd);
+
+			return;
+		}
+	}
+
+	/* We have already done all our retries. Make sure that the callback
+	 * doesn't get called before the original function returns */
+	gaim_timeout_add(10, gaim_upnp_discover_timeout, dd);
+}
+
+void
+gaim_upnp_discover(GaimUPnPCallback cb, gpointer cb_data)
+{
+	/* Socket Setup Variables */
+	int sock;
+	struct hostent* hp;
+
+	/* UDP RECEIVE VARIABLES */
+	UPnPDiscoveryData *dd;
+
+	if (control_info.status == GAIM_UPNP_STATUS_DISCOVERING) {
+		if (cb) {
+			discovery_callbacks = g_slist_append(
+					discovery_callbacks, cb);
+			discovery_callbacks = g_slist_append(
+					discovery_callbacks, cb_data);
+		}
+		return;
+	}
+
+	dd = g_new0(UPnPDiscoveryData, 1);
+	if (cb) {
+		discovery_callbacks = g_slist_append(discovery_callbacks, cb);
+		discovery_callbacks = g_slist_append(discovery_callbacks,
+				cb_data);
+	}
+
+	/* Set up the sockets */
+	sock = socket(AF_INET, SOCK_DGRAM, 0);
+	if(sock == -1) {
+		gaim_debug_error("upnp",
+			"gaim_upnp_discover(): Failed In sock creation\n");
+		/* Short circuit the retry attempts */
+		dd->retry_count = NUM_UDP_ATTEMPTS;
+		gaim_timeout_add(10, gaim_upnp_discover_timeout, dd);
+		return;
+	}
+
+	dd->fd = sock;
+
+	/* TODO: Non-blocking! */
+	if((hp = gethostbyname(HTTPMU_HOST_ADDRESS)) == NULL) {
+		gaim_debug_error("upnp",
+			"gaim_upnp_discover(): Failed In gethostbyname\n");
+		/* Short circuit the retry attempts */
+		dd->retry_count = NUM_UDP_ATTEMPTS;
+		gaim_timeout_add(10, gaim_upnp_discover_timeout, dd);
+		return;
+	}
+
+	memset(&(dd->server), 0, sizeof(struct sockaddr));
+	dd->server.sin_family = AF_INET;
+	memcpy(&(dd->server.sin_addr), hp->h_addr_list[0], hp->h_length);
+	dd->server.sin_port = htons(HTTPMU_HOST_PORT);
+
+	control_info.status = GAIM_UPNP_STATUS_DISCOVERING;
+
+	gaim_upnp_discover_send_broadcast(dd);
+}
+
+static void
+gaim_upnp_generate_action_message_and_send(const gchar* actionName,
+		const gchar* actionParams, GaimUtilFetchUrlCallback cb,
+		gpointer cb_data)
+{
+
+	gchar* soapMessage;
+	gchar* totalSendMessage;
+	gchar* pathOfControl;
+	gchar* addressOfControl;
+	int port = 0;
+
+	/* parse the url into address, port, path variables */
+	if(!gaim_url_parse(control_info.control_url, &addressOfControl,
+			&port, &pathOfControl, NULL, NULL)) {
+		gaim_debug_error("upnp",
+			"generate_action_message_and_send(): Failed In Parse URL\n");
+		/* XXX: This should probably be async */
+		if(cb)
+			cb(NULL, cb_data, NULL, 0, NULL);
+	}
+	if(port == 0 || port == -1) {
+		port = DEFAULT_HTTP_PORT;
+	}
+
+	/* set the soap message */
+	soapMessage = g_strdup_printf(SOAP_ACTION, actionName,
+		control_info.service_type, actionParams, actionName);
+
+	/* set the HTTP Header, and append the body to it */
+	totalSendMessage = g_strdup_printf(HTTP_HEADER_ACTION "%s",
+		pathOfControl, addressOfControl, port,
+		control_info.service_type, actionName,
+		strlen(soapMessage), soapMessage);
+	g_free(pathOfControl);
+	g_free(soapMessage);
+
+	gaim_util_fetch_url_request(control_info.control_url, FALSE, NULL, TRUE,
+			totalSendMessage, TRUE, cb, cb_data);
+
+	g_free(totalSendMessage);
+	g_free(addressOfControl);
+}
+
+const gchar *
+gaim_upnp_get_public_ip()
+{
+	if (control_info.status == GAIM_UPNP_STATUS_DISCOVERED
+			&& control_info.publicip
+			&& strlen(control_info.publicip) > 0)
+		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 < GAIM_UPNP_STATUS_DISCOVERING
+			&& (time(NULL) - control_info.lookup_time) > 300)
+		gaim_upnp_discover(NULL, NULL);
+
+	return NULL;
+}
+
+static void
+looked_up_public_ip_cb(GaimUtilFetchUrlData *url_data, gpointer user_data,
+		const gchar *httpResponse, gsize len, const gchar *error_message)
+{
+	gchar* temp, *temp2;
+
+	if ((error_message != NULL) || (httpResponse == NULL))
+		return;
+
+	/* extract the ip, or see if there is an error */
+	if((temp = g_strstr_len(httpResponse, len,
+			"<NewExternalIPAddress")) == NULL) {
+		gaim_debug_error("upnp",
+			"looked_up_public_ip_cb(): Failed Finding <NewExternalIPAddress\n");
+		return;
+	}
+	if(!(temp = g_strstr_len(temp, len - (temp - httpResponse), ">"))) {
+		gaim_debug_error("upnp",
+			"looked_up_public_ip_cb(): Failed In Finding >\n");
+		return;
+	}
+	if(!(temp2 = g_strstr_len(temp, len - (temp - httpResponse), "<"))) {
+		gaim_debug_error("upnp",
+			"looked_up_public_ip_cb(): Failed In Finding <\n");
+		return;
+	}
+	*temp2 = '\0';
+
+	strncpy(control_info.publicip, temp + 1,
+			sizeof(control_info.publicip));
+
+	gaim_debug_info("upnp", "NAT Returned IP: %s\n", control_info.publicip);
+}
+
+static void
+lookup_public_ip()
+{
+	gaim_upnp_generate_action_message_and_send("GetExternalIPAddress", "",
+			looked_up_public_ip_cb, NULL);
+}
+
+/* TODO: This could be exported */
+static const gchar *
+gaim_upnp_get_internal_ip()
+{
+	if (control_info.status == GAIM_UPNP_STATUS_DISCOVERED
+			&& control_info.internalip
+			&& strlen(control_info.internalip) > 0)
+		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 < GAIM_UPNP_STATUS_DISCOVERING
+			&& (time(NULL) - control_info.lookup_time) > 300)
+		gaim_upnp_discover(NULL, NULL);
+
+	return NULL;
+}
+
+static void
+looked_up_internal_ip_cb(gpointer data, gint source, const gchar *error_message)
+{
+	if (source) {
+		strncpy(control_info.internalip,
+			gaim_network_get_local_system_ip(source),
+			sizeof(control_info.internalip));
+		gaim_debug_info("upnp", "Local IP: %s\n",
+				control_info.internalip);
+		close(source);
+	} else
+		gaim_debug_info("upnp", "Unable to look up local IP\n");
+
+}
+
+static void
+lookup_internal_ip()
+{
+	gchar* addressOfControl;
+	int port = 0;
+
+	if(!gaim_url_parse(control_info.control_url, &addressOfControl, &port,
+			NULL, NULL, NULL)) {
+		gaim_debug_error("upnp",
+			"lookup_internal_ip(): Failed In Parse URL\n");
+		return;
+	}
+	if(port == 0 || port == -1) {
+		port = DEFAULT_HTTP_PORT;
+	}
+
+	if(gaim_proxy_connect(NULL, NULL, addressOfControl, port,
+			looked_up_internal_ip_cb, NULL) == NULL)
+	{
+		gaim_debug_error("upnp", "Get Local IP Connect Failed: Address: %s @@@ Port %d\n",
+			addressOfControl, port);
+	}
+
+	g_free(addressOfControl);
+}
+
+static void
+done_port_mapping_cb(GaimUtilFetchUrlData *url_data, gpointer user_data,
+		const gchar *httpResponse, gsize len, const gchar *error_message)
+{
+	UPnPMappingAddRemove *ar = user_data;
+
+	gboolean success = TRUE;
+
+	/* determine if port mapping was a success */
+	if ((error_message != NULL) || (httpResponse == NULL) ||
+		(g_strstr_len(httpResponse, len, HTTP_OK) == NULL))
+	{
+		gaim_debug_error("upnp",
+			"gaim_upnp_set_port_mapping(): Failed HTTP_OK\n%s\n",
+			httpResponse ? httpResponse : "(null)");
+		success =  FALSE;
+	} else
+		gaim_debug_info("upnp", "Successfully completed port mapping operation\n");
+
+	if (ar->cb)
+		ar->cb(success, ar->cb_data);
+	g_free(ar);
+}
+
+static void
+do_port_mapping_cb(gboolean has_control_mapping, gpointer data)
+{
+	UPnPMappingAddRemove *ar = data;
+
+	if (has_control_mapping) {
+		gchar action_name[25];
+		gchar *action_params;
+		if(ar->add) {
+			const gchar *internal_ip;
+			/* get the internal IP */
+			if(!(internal_ip = gaim_upnp_get_internal_ip())) {
+				gaim_debug_error("upnp",
+					"gaim_upnp_set_port_mapping(): couldn't get local ip\n");
+				/* UGLY */
+				if (ar->cb)
+					ar->cb(FALSE, ar->cb_data);
+				g_free(ar);
+				return;
+			}
+			strncpy(action_name, "AddPortMapping",
+					sizeof(action_name));
+			action_params = g_strdup_printf(
+					ADD_PORT_MAPPING_PARAMS,
+					ar->portmap, ar->protocol, ar->portmap,
+					internal_ip);
+		} else {
+			strncpy(action_name, "DeletePortMapping", sizeof(action_name));
+			action_params = g_strdup_printf(
+				DELETE_PORT_MAPPING_PARAMS,
+				ar->portmap, ar->protocol);
+		}
+
+		gaim_upnp_generate_action_message_and_send(action_name,
+				action_params, done_port_mapping_cb, ar);
+
+		g_free(action_params);
+		return;
+	}
+
+
+	if (ar->cb)
+		ar->cb(FALSE, ar->cb_data);
+	g_free(ar);
+}
+
+static gboolean
+fire_port_mapping_failure_cb(gpointer data)
+{
+	do_port_mapping_cb(FALSE, data);
+	return FALSE;
+}
+
+void
+gaim_upnp_set_port_mapping(unsigned short portmap, const gchar* protocol,
+		GaimUPnPCallback cb, gpointer cb_data)
+{
+	UPnPMappingAddRemove *ar;
+
+	ar = g_new0(UPnPMappingAddRemove, 1);
+	ar->cb = cb;
+	ar->cb_data = cb_data;
+	ar->add = TRUE;
+	ar->portmap = portmap;
+	strncpy(ar->protocol, protocol, sizeof(ar->protocol));
+
+	/* If we're waiting for a discovery, add to the callbacks list */
+	if(control_info.status == GAIM_UPNP_STATUS_DISCOVERING) {
+		/* TODO: This will fail because when this cb is triggered,
+		 * the internal IP lookup won't be complete */
+		discovery_callbacks = g_slist_append(
+				discovery_callbacks, do_port_mapping_cb);
+		discovery_callbacks = g_slist_append(
+				discovery_callbacks, ar);
+		return;
+	}
+
+	/* If we haven't had a successful UPnP discovery, check if 5 minutes has
+	 * elapsed since the last try, try again */
+	if(control_info.status == GAIM_UPNP_STATUS_UNDISCOVERED ||
+			(control_info.status == GAIM_UPNP_STATUS_UNABLE_TO_DISCOVER
+			 && (time(NULL) - control_info.lookup_time) > 300)) {
+		gaim_upnp_discover(do_port_mapping_cb, ar);
+		return;
+	} else if(control_info.status == GAIM_UPNP_STATUS_UNABLE_TO_DISCOVER) {
+		if (cb) {
+			/* Asynchronously trigger a failed response */
+			gaim_timeout_add(10, fire_port_mapping_failure_cb, ar);
+		} else {
+			/* No need to do anything if nobody expects a response*/
+			g_free(ar);
+		}
+		return;
+	}
+
+	do_port_mapping_cb(TRUE, ar);
+}
+
+void
+gaim_upnp_remove_port_mapping(unsigned short portmap, const char* protocol,
+		GaimUPnPCallback cb, gpointer cb_data)
+{
+	UPnPMappingAddRemove *ar;
+
+	ar = g_new0(UPnPMappingAddRemove, 1);
+	ar->cb = cb;
+	ar->cb_data = cb_data;
+	ar->add = FALSE;
+	ar->portmap = portmap;
+	strncpy(ar->protocol, protocol, sizeof(ar->protocol));
+
+	/* If we're waiting for a discovery, add to the callbacks list */
+	if(control_info.status == GAIM_UPNP_STATUS_DISCOVERING) {
+		discovery_callbacks = g_slist_append(
+				discovery_callbacks, do_port_mapping_cb);
+		discovery_callbacks = g_slist_append(
+				discovery_callbacks, ar);
+		return;
+	}
+
+	/* If we haven't had a successful UPnP discovery, check if 5 minutes has
+	 * elapsed since the last try, try again */
+	if(control_info.status == GAIM_UPNP_STATUS_UNDISCOVERED ||
+			(control_info.status == GAIM_UPNP_STATUS_UNABLE_TO_DISCOVER
+			 && (time(NULL) - control_info.lookup_time) > 300)) {
+		gaim_upnp_discover(do_port_mapping_cb, ar);
+		return;
+	} else if(control_info.status == GAIM_UPNP_STATUS_UNABLE_TO_DISCOVER) {
+		if (cb) {
+			/* Asynchronously trigger a failed response */
+			gaim_timeout_add(10, fire_port_mapping_failure_cb, ar);
+		} else {
+			/* No need to do anything if nobody expects a response*/
+			g_free(ar);
+		}
+		return;
+	}
+
+	do_port_mapping_cb(TRUE, ar);
+}