changeset 27635:7fbf964c6c6c

Move the IDN support into the DNS routines. This turned out to be cleaner than I first thought, so here we go. All libpurple DNS routines support IDN when built against GNU libidn.
author Paul Aurich <paul@darkrain42.org>
date Sun, 19 Jul 2009 23:45:13 +0000
parents 91333c6c16ef
children e8ec4a3e6706 577bf7ea0395
files ChangeLog ChangeLog.API libpurple/Makefile.am libpurple/dnsquery.c libpurple/dnssrv.c libpurple/network.c libpurple/network.h libpurple/protocols/jabber/jabber.c libpurple/protocols/jabber/jutil.c libpurple/protocols/jabber/jutil.h
diffstat 10 files changed, 196 insertions(+), 82 deletions(-) [+]
line wrap: on
line diff
--- a/ChangeLog	Sun Jul 19 22:40:10 2009 +0000
+++ b/ChangeLog	Sun Jul 19 23:45:13 2009 +0000
@@ -38,6 +38,8 @@
 	* Better handling of corrupt certificates in the TLS Peers cache.
 	* More efficient purple_find_buddies() and purple_find_group() functions.
 	  (Jan Kaluza and Aman Gupta)
+	* Internationalized Domain Names are supported when libpurple is compiled
+	  against the GNU IDN library.
 
 	AIM and ICQ:
 	* Preliminary support for a new authentication scheme called
@@ -106,9 +108,8 @@
 	* Removed support for obsoleted XEP-0022 (Message Events) and XEP-0091
 	  (Legacy Entity Time).
 	* When the GNU IDN library (libidn) is available, it is used for
-	  normalization of Jabber IDs and support for internationalized domain
-	  names (IDNA).  When unavailable, internal routines are used (as in
-	  previous versions) for normalization and IDNA is unavailable.
+	  normalization of Jabber IDs.  When unavailable, internal routines are
+	  used (as in previous versions).
 
 	Yahoo!/Yahoo! JAPAN:
 	* P2P file transfers.  (Sulabh Mahajan)
--- a/ChangeLog.API	Sun Jul 19 22:40:10 2009 +0000
+++ b/ChangeLog.API	Sun Jul 19 23:45:13 2009 +0000
@@ -42,6 +42,7 @@
 		* purple_log_get_activity_score
 		* purple_markup_is_rtl
 		* purple_markup_escape_text
+		* purple_network_convert_idn_to_ascii
 		* purple_network_force_online
 		* purple_network_set_stun_server
 		* purple_network_set_turn_server
@@ -83,6 +84,8 @@
 		* purple_find_buddies is now more efficient in the case where
 		  it is enumerating all the buddies for an account.
 		* purple_find_group is now more efficient for large numbers of groups.
+		* All DNS routines support internationalized domain names (IDNs) when
+		  libpurple is compiled with GNU libidn.
 
 		Deprecated:
 		* buddy-added and buddy-removed blist signals
--- a/libpurple/Makefile.am	Sun Jul 19 22:40:10 2009 +0000
+++ b/libpurple/Makefile.am	Sun Jul 19 23:45:13 2009 +0000
@@ -293,6 +293,7 @@
 	$(FARSIGHT_LIBS) \
 	$(GSTREAMER_LIBS) \
 	$(GSTINTERFACES_LIBS) \
+	$(IDN_LIBS) \
 	-lm
 
 AM_CPPFLAGS = \
@@ -308,6 +309,7 @@
 	$(FARSIGHT_CFLAGS) \
 	$(GSTREAMER_CFLAGS) \
 	$(GSTINTERFACES_CFLAGS) \
+	$(IDN_CFLAGS) \
 	$(NETWORKMANAGER_CFLAGS)
 
 # INSTALL_SSL_CERTIFICATES is true when SSL_CERTIFICATES_DIR is empty.
--- a/libpurple/dnsquery.c	Sun Jul 19 22:40:10 2009 +0000
+++ b/libpurple/dnsquery.c	Sun Jul 19 23:45:13 2009 +0000
@@ -28,6 +28,7 @@
 #include "internal.h"
 #include "debug.h"
 #include "dnsquery.h"
+#include "network.h"
 #include "notify.h"
 #include "prefs.h"
 #include "util.h"
@@ -171,6 +172,18 @@
 	return FALSE;
 }
 
+static gboolean
+dns_str_is_ascii(const char *name)
+{
+	guchar *c;
+	for (c = (guchar *)name; c && *c; ++c) {
+		if (*c > 0x7f)
+			return FALSE;
+	}
+
+	return TRUE;
+}
+
 #if defined(PURPLE_DNSQUERY_USE_FORK)
 
 /*
@@ -230,6 +243,7 @@
 	struct sockaddr_in sin;
 	const size_t addrlen = sizeof(sin);
 #endif
+	char *hostname;
 
 #ifdef HAVE_SIGNAL_H
 	purple_restore_default_signal_handlers();
@@ -274,6 +288,22 @@
 			_exit(1);
 		}
 
+#ifdef USE_IDN
+		if (!dns_str_is_ascii(dns_params.hostname)) {
+			rc = purple_network_convert_idn_to_ascii(dns_params.hostname, &hostname);
+			if (rc != 0) {
+				write_to_parent(child_out, &rc, sizeof(rc));
+				close(child_out);
+				if (show_debug)
+					fprintf(stderr, "dns[%d] Error: IDN conversion returned "
+							"%d\n", getpid(), rc);
+				dns_params.hostname[0] = '\0';
+				continue;
+			}
+		} else /* intentional to execute the g_strdup */
+#endif
+		hostname = g_strdup(dns_params.hostname);
+
 		/* We have the hostname and port, now resolve the IP */
 
 #ifdef HAVE_GETADDRINFO
@@ -290,7 +320,7 @@
 #ifdef AI_ADDRCONFIG
 		hints.ai_flags |= AI_ADDRCONFIG;
 #endif /* AI_ADDRCONFIG */
-		rc = getaddrinfo(dns_params.hostname, servname, &hints, &res);
+		rc = getaddrinfo(hostname, servname, &hints, &res);
 		write_to_parent(child_out, &rc, sizeof(rc));
 		if (rc != 0) {
 			close(child_out);
@@ -309,9 +339,9 @@
 		}
 		freeaddrinfo(tmp);
 #else
-		if (!inet_aton(dns_params.hostname, &sin.sin_addr)) {
+		if (!inet_aton(hostname, &sin.sin_addr)) {
 			struct hostent *hp;
-			if (!(hp = gethostbyname(dns_params.hostname))) {
+			if (!(hp = gethostbyname(hostname))) {
 				write_to_parent(child_out, &h_errno, sizeof(int));
 				close(child_out);
 				if (show_debug)
@@ -332,6 +362,9 @@
 #endif
 		write_to_parent(child_out, &zero, sizeof(zero));
 		dns_params.hostname[0] = '\0';
+
+		g_free(hostname);
+		hostname = NULL;
 	}
 
 	close(child_out);
@@ -733,9 +766,27 @@
 	struct sockaddr_in sin;
 	struct hostent *hp;
 #endif
+	char *hostname;
 
 	query_data = data;
 
+#ifdef USE_IDN
+	if (!dns_str_is_ascii(dns_params.hostname)) {
+		rc = purple_network_convert_idn_to_ascii(dns_params.hostname, &hostname);
+		if (rc != 0) {
+			/* FIXME: Dirty 2.6.0 string freeze hack */
+			char tmp[8];
+			g_snprintf(tmp, sizeof(tmp), "%d", rc);
+			query_data->error_message = g_strdup_printf(_("Error resolving %s:\n%s"),
+					query_data->hostname, tmp);
+			/* back to main thread */
+			purple_timeout_add(0, dns_main_thread_cb, query_data);
+			return 0;
+		}
+	} else /* intentional fallthru */
+#endif
+	hostname = g_strdup(dns_params.hostname);
+
 #ifdef HAVE_GETADDRINFO
 	g_snprintf(servname, sizeof(servname), "%d", query_data->port);
 	memset(&hints,0,sizeof(hints));
@@ -751,7 +802,7 @@
 #ifdef AI_ADDRCONFIG
 	hints.ai_flags |= AI_ADDRCONFIG;
 #endif /* AI_ADDRCONFIG */
-	if ((rc = getaddrinfo(query_data->hostname, servname, &hints, &res)) == 0) {
+	if ((rc = getaddrinfo(hostname, servname, &hints, &res)) == 0) {
 		tmp = res;
 		while(res) {
 			query_data->hosts = g_slist_append(query_data->hosts,
@@ -765,7 +816,7 @@
 		query_data->error_message = g_strdup_printf(_("Error resolving %s:\n%s"), query_data->hostname, purple_gai_strerror(rc));
 	}
 #else
-	if ((hp = gethostbyname(query_data->hostname))) {
+	if ((hp = gethostbyname(hostname))) {
 		memset(&sin, 0, sizeof(struct sockaddr_in));
 		memcpy(&sin.sin_addr.s_addr, hp->h_addr, hp->h_length);
 		sin.sin_family = hp->h_addrtype;
@@ -779,6 +830,7 @@
 		query_data->error_message = g_strdup_printf(_("Error resolving %s: %d"), query_data->hostname, h_errno);
 	}
 #endif
+	g_free(hostname);
 
 	/* back to main thread */
 	purple_timeout_add(0, dns_main_thread_cb, query_data);
@@ -866,6 +918,7 @@
 	PurpleDnsQueryData *query_data;
 	struct sockaddr_in sin;
 	GSList *hosts = NULL;
+	char *hostname;
 
 	query_data = data;
 	query_data->timeout = 0;
@@ -878,7 +931,22 @@
 
 	if (!inet_aton(query_data->hostname, &sin.sin_addr)) {
 		struct hostent *hp;
-		if(!(hp = gethostbyname(query_data->hostname))) {
+#ifdef USE_IDN
+		if (!dns_str_is_ascii(query_data->hostname)) {
+			int ret = purple_network_convert_idn_to_ascii(query_data->hostname,
+					&hostname);
+			if (ret != 0) {
+				char message[1024];
+				g_snprintf(message, sizeof(message), _("Error resolving %s: %d"),
+						query_data->hostname, ret);
+				purple_dnsquery_failed(query_data, message);
+				return FALSE;
+			}
+		} else /* fallthrough is intentional to the g_strdup */
+#endif
+		hostname = g_strdup(query_data->hostname);
+
+		if(!(hp = gethostbyname(hostname))) {
 			char message[1024];
 			g_snprintf(message, sizeof(message), _("Error resolving %s: %d"),
 					query_data->hostname, h_errno);
@@ -892,6 +960,7 @@
 		sin.sin_family = AF_INET;
 	sin.sin_port = htons(query_data->port);
 
+	g_free(hostname);
 	hosts = g_slist_append(hosts, GINT_TO_POINTER(sizeof(sin)));
 	hosts = g_slist_append(hosts, g_memdup(&sin, sizeof(sin)));
 
--- a/libpurple/dnssrv.c	Sun Jul 19 22:40:10 2009 +0000
+++ b/libpurple/dnssrv.c	Sun Jul 19 23:45:13 2009 +0000
@@ -47,9 +47,10 @@
 #endif
 #endif
 
+#include "debug.h"
 #include "dnssrv.h"
 #include "eventloop.h"
-#include "debug.h"
+#include "network.h"
 
 #ifndef _WIN32
 typedef union {
@@ -247,6 +248,18 @@
 	return list;
 }
 
+static gboolean
+dns_str_is_ascii(const char *name)
+{
+	guchar *c;
+	for (c = (guchar *)name; c && *c; ++c) {
+		if (*c > 0x7f)
+			return FALSE;
+	}
+
+	return TRUE;
+}
+
 #ifndef _WIN32
 
 G_GNUC_NORETURN static void
@@ -588,6 +601,7 @@
 purple_srv_resolve(const char *protocol, const char *transport, const char *domain, PurpleSrvCallback cb, gpointer extradata)
 {
 	char *query;
+	char *hostname;
 	PurpleSrvQueryData *query_data;
 #ifndef _WIN32
 	PurpleSrvInternalQuery internal_query;
@@ -604,8 +618,22 @@
 		g_return_val_if_reached(NULL);
 	}
 
-	query = g_strdup_printf("_%s._%s.%s", protocol, transport, domain);
-	purple_debug_info("dnssrv","querying SRV record for %s\n", query);
+#ifdef USE_IDN
+	if (!dns_str_is_ascii(domain)) {
+		int ret = purple_network_convert_idn_to_ascii(domain, &hostname);
+		if (ret != 0) {
+			purple_debug_error("dnssrv", "IDNA ToASCII failed\n");
+			cb(NULL, 0, extradata);
+			return NULL;
+		}
+	} else /* Fallthru is intentional */
+#endif
+	hostname = g_strdup(domain);
+
+	query = g_strdup_printf("_%s._%s.%s", protocol, transport, hostname);
+	purple_debug_info("dnssrv","querying SRV record for %s: %s\n", domain,
+			query);
+	g_free(hostname);
 
 #ifndef _WIN32
 	if(pipe(in) || pipe(out)) {
@@ -692,6 +720,7 @@
 PurpleSrvQueryData *purple_txt_resolve(const char *owner, const char *domain, PurpleTxtCallback cb, gpointer extradata)
 {
 	char *query;
+	char *hostname;
 	PurpleSrvQueryData *query_data;
 #ifndef _WIN32
 	PurpleSrvInternalQuery internal_query;
@@ -702,8 +731,22 @@
 	static gboolean initialized = FALSE;
 #endif
 
-	query = g_strdup_printf("%s.%s", owner, domain);
-	purple_debug_info("dnssrv","querying TXT record for %s\n", query);
+#ifdef USE_IDN
+	if (!dns_str_is_ascii(domain)) {
+		int ret = purple_network_convert_idn_to_ascii(domain, &hostname);
+		if (ret != 0) {
+			purple_debug_error("dnssrv", "IDNA ToASCII failed\n");
+			cb(NULL, extradata);
+			return NULL;
+		}
+	} else /* fallthru is intentional */
+#endif
+	hostname = g_strdup(domain);
+
+	query = g_strdup_printf("%s.%s", owner, hostname);
+	purple_debug_info("dnssrv","querying TXT record for %s: %s\n", domain,
+			query);
+	g_free(hostname);
 
 #ifndef _WIN32
 	if(pipe(in) || pipe(out)) {
--- a/libpurple/network.c	Sun Jul 19 22:40:10 2009 +0000
+++ b/libpurple/network.c	Sun Jul 19 23:45:13 2009 +0000
@@ -50,6 +50,10 @@
 #include "upnp.h"
 #include "dnsquery.h"
 
+#ifdef USE_IDN
+#include <idna.h>
+#endif
+
 /*
  * Calling sizeof(struct ifreq) isn't always correct on
  * Mac OS X (and maybe others).
@@ -966,7 +970,33 @@
 		}
 	}
 }
-	
+
+int purple_network_convert_idn_to_ascii(const gchar *in, gchar **out)
+{
+#ifdef USE_IDN
+	char *tmp;
+	int ret;
+
+	g_return_val_if_fail(out != NULL, -1);
+
+	ret = idna_to_ascii_8z(in, &tmp, IDNA_USE_STD3_ASCII_RULES);
+	if (ret != IDNA_SUCCESS) {
+		*out = NULL;
+		return ret;
+	}
+
+	*out = g_strdup(tmp);
+	/* This *MUST* be freed with free, not g_free */
+	free(tmp);
+	return 0;
+#else
+	g_return_val_if_fail(out != NULL, -1);
+
+	*out = g_strdup(in);
+	return 0;
+#endif
+}
+
 void
 purple_network_init(void)
 {
--- a/libpurple/network.h	Sun Jul 19 22:40:10 2009 +0000
+++ b/libpurple/network.h	Sun Jul 19 23:45:13 2009 +0000
@@ -268,6 +268,25 @@
 void purple_network_remove_port_mapping(gint fd);	
 
 /**
+ * Convert a UTF-8 domain name to ASCII in accordance with the IDNA
+ * specification. If libpurple is compiled without IDN support, this function
+ * copies the input into the output buffer.
+ *
+ * Because this function is used by DNS resolver child/threads, it uses no
+ * other libpurple API and is threadsafe.
+ *
+ * In general, a buffer of about 512 bytes is the appropriate size to use.
+ *
+ * @param in      The hostname to be converted.
+ * @param out     The output buffer where an allocated string will be returned.
+ *                The caller is responsible for freeing this.
+ * @returns       0 on success, -1 if the out is NULL, or an error code
+ *                that currently corresponds to the Idna_rc enum in libidn.
+ * @since 2.6.0
+ */
+int purple_network_convert_idn_to_ascii(const gchar *in, gchar **out);
+
+/**
  * Initializes the network subsystem.
  */
 void purple_network_init(void);
--- a/libpurple/protocols/jabber/jabber.c	Sun Jul 19 22:40:10 2009 +0000
+++ b/libpurple/protocols/jabber/jabber.c	Sun Jul 19 23:45:13 2009 +0000
@@ -657,11 +657,9 @@
 			purple_debug_error("jabber", "Unable to connect to server: %s.  Trying next SRV record.\n", error);
 			try_srv_connect(js);
 		} else {
-			char *ascii_domain = jabber_try_idna_to_ascii(js->user->domain);
 			purple_debug_info("jabber","Couldn't connect directly to %s. Trying to find alternative connection methods, like BOSH.\n", js->user->domain);
 			js->srv_query_data = purple_txt_resolve("_xmppconnect",
-					ascii_domain, txt_resolved_cb, js);
-			g_free(ascii_domain);
+					js->user->domain, txt_resolved_cb, js);
 		}
 		return;
 	}
@@ -704,17 +702,14 @@
 }
 
 static gboolean jabber_login_connect(JabberStream *js, const char *domain, const char *host, int port,
-				 gboolean fatal_failure, gboolean use_domain)
+				 gboolean fatal_failure)
 {
 	/* host should be used in preference to domain to
 	 * allow SASL authentication to work with FQDN of the server,
 	 * but we use domain as fallback for when users enter IP address
-	 * in connect server.
-	 * We also want to use the domain if the hostname is the result of the
-	 * IDNA ToASCII operation.
-	 */
+	 * in connect server */
 	g_free(js->serverFQDN);
-	if (use_domain || purple_ip_address_is_valid(host))
+	if (purple_ip_address_is_valid(host))
 		js->serverFQDN = g_strdup(domain);
 	else
 		js->serverFQDN = g_strdup(host);
@@ -735,23 +730,19 @@
 
 static void try_srv_connect(JabberStream *js)
 {
-	char *ascii_domain;
-
 	while (js->srv_rec != NULL && js->srv_rec_idx < js->max_srv_rec_idx) {
 		PurpleSrvResponse *tmp_resp = js->srv_rec + (js->srv_rec_idx++);
-		if (jabber_login_connect(js, tmp_resp->hostname, tmp_resp->hostname, tmp_resp->port, FALSE, FALSE))
+		if (jabber_login_connect(js, tmp_resp->hostname, tmp_resp->hostname, tmp_resp->port, FALSE))
 			return;
 	}
 
 	g_free(js->srv_rec);
 	js->srv_rec = NULL;
 
-	ascii_domain = jabber_try_idna_to_ascii(js->user->domain);
 	/* Fall back to the defaults (I'm not sure if we should actually do this) */
-	jabber_login_connect(js, js->user->domain, ascii_domain,
+	jabber_login_connect(js, js->user->domain, js->user->domain,
 			purple_account_get_int(purple_connection_get_account(js->gc), "port", 5222),
-			TRUE, TRUE);
-	g_free(ascii_domain);
+			TRUE);
 }
 
 static void srv_resolved_cb(PurpleSrvResponse *resp, int results, gpointer data)
@@ -765,11 +756,9 @@
 		js->max_srv_rec_idx = results;
 		try_srv_connect(js);
 	} else {
-		char *ascii_domain = jabber_try_idna_to_ascii(js->user->domain);
-		jabber_login_connect(js, js->user->domain, ascii_domain,
+		jabber_login_connect(js, js->user->domain, js->user->domain,
 				purple_account_get_int(purple_connection_get_account(js->gc), "port", 5222),
-				TRUE, TRUE);
-		g_free(ascii_domain);
+				TRUE);
 	}
 }
 
@@ -860,7 +849,6 @@
 			"connect_server", "");
 	const char *bosh_url = purple_account_get_string(account,
 			"bosh_url", "");
-	char *ascii_domain;
 
 	jabber_stream_set_state(js, JABBER_STREAM_CONNECTING);
 
@@ -883,22 +871,12 @@
 
 	js->certificate_CN = g_strdup(connect_server[0] ? connect_server : js->user->domain);
 
-	ascii_domain = jabber_try_idna_to_ascii(js->certificate_CN);
-	if (ascii_domain == NULL) {
-		purple_connection_error_reason(gc,
-				PURPLE_CONNECTION_ERROR_INVALID_SETTINGS,
-				_("Invalid XMPP ID"));
-		return;
-	}
-
 	/* if they've got old-ssl mode going, we probably want to ignore SRV lookups */
 	if(purple_account_get_bool(account, "old_ssl", FALSE)) {
 		if(purple_ssl_is_supported()) {
-			js->gsc = purple_ssl_connect_with_ssl_cn(account, ascii_domain,
+			js->gsc = purple_ssl_connect(account, js->certificate_CN,
 					purple_account_get_int(account, "port", 5223),
-					jabber_login_callback_ssl, jabber_ssl_connect_failure,
-					js->certificate_CN, gc);
-			g_free(ascii_domain);
+					jabber_login_callback_ssl, jabber_ssl_connect_failure, gc);
 			if (!js->gsc) {
 				purple_connection_error_reason(gc,
 					PURPLE_CONNECTION_ERROR_NO_SSL_SUPPORT,
@@ -910,22 +888,18 @@
 				_("SSL support unavailable"));
 		}
 
-		g_free(ascii_domain);
 		return;
 	}
 
 	/* no old-ssl, so if they've specified a connect server, we'll use that, otherwise we'll
 	 * invoke the magic of SRV lookups, to figure out host and port */
 	if(connect_server[0]) {
-		jabber_login_connect(js, js->user->domain, ascii_domain,
-				purple_account_get_int(account, "port", 5222),
-				TRUE, TRUE);
+		jabber_login_connect(js, js->user->domain, connect_server,
+				purple_account_get_int(account, "port", 5222), TRUE);
 	} else {
 		js->srv_query_data = purple_srv_resolve("xmpp-client",
-				"tcp", ascii_domain, srv_resolved_cb, js);
+				"tcp", js->user->domain, srv_resolved_cb, js);
 	}
-
-	g_free(ascii_domain);
 }
 
 void
--- a/libpurple/protocols/jabber/jutil.c	Sun Jul 19 22:40:10 2009 +0000
+++ b/libpurple/protocols/jabber/jutil.c	Sun Jul 19 23:45:13 2009 +0000
@@ -37,28 +37,6 @@
 static char idn_buffer[1024];
 #endif
 
-gchar *jabber_try_idna_to_ascii(const char *input)
-{
-#ifndef USE_IDN
-	return g_strdup(input);
-#else
-	gchar *out;
-	char *tmp;
-
-	g_return_val_if_fail(input != NULL, NULL);
-	g_return_val_if_fail(*input != '\0', NULL);
-
-	if (idna_to_ascii_8z(input, &tmp, IDNA_USE_STD3_ASCII_RULES) != IDNA_SUCCESS) {
-		return NULL;
-	}
-
-	out = g_strdup(tmp);
-	/* This *MUST* be freed with free, not g_free */
-	free(tmp);
-	return out;
-#endif
-}
-
 #ifdef USE_IDN
 static gboolean jabber_nodeprep(char *str, size_t buflen)
 {
--- a/libpurple/protocols/jabber/jutil.h	Sun Jul 19 22:40:10 2009 +0000
+++ b/libpurple/protocols/jabber/jutil.h	Sun Jul 19 23:45:13 2009 +0000
@@ -44,11 +44,6 @@
 /* Returns true if JID is the bare JID of our account. */
 gboolean jabber_is_own_account(JabberStream *js, const char *jid);
 
-/* Try to convert an IDNA domain name to something we can pass to a DNS lookup.
- * If IDN support is not available, returns a copy of the input string.
- */
-gchar *jabber_try_idna_to_ascii(const gchar *input);
-
 gboolean jabber_nodeprep_validate(const char *);
 gboolean jabber_domain_validate(const char *);
 gboolean jabber_resourceprep_validate(const char *);