1 /*
2  *  This program is free software; you can redistribute it and/or modify
3  *  it under the terms of the GNU General Public License as published by
4  *  the Free Software Foundation; either version 2 of the License, or
5  *  (at your option) any later version.
6  *
7  *  This program is distributed in the hope that it will be useful,
8  *  but WITHOUT ANY WARRANTY; without even the implied warranty of
9  *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
10  *  GNU Library General Public License for more details.
11  *
12  *  You should have received a copy of the GNU General Public License
13  *  along with this program; if not, write to the Free Software
14  *  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02111-1301, USA.
15  */
16 
17 #include <string.h>
18 
19 #include "internal.h"
20 #include "debug.h"
21 
22 #include "mdns_common.h"
23 #include "mdns_interface.h"
24 #include "bonjour.h"
25 #include "buddy.h"
26 
27 
28 /**
29  * Allocate space for the dns-sd data.
30  */
bonjour_dns_sd_new()31 BonjourDnsSd * bonjour_dns_sd_new() {
32 	BonjourDnsSd *data = g_new0(BonjourDnsSd, 1);
33 	return data;
34 }
35 
36 /**
37  * Deallocate the space of the dns-sd data.
38  */
bonjour_dns_sd_free(BonjourDnsSd * data)39 void bonjour_dns_sd_free(BonjourDnsSd *data) {
40 	g_free(data->first);
41 	g_free(data->last);
42 	g_free(data->phsh);
43 	g_free(data->status);
44 	g_free(data->vc);
45 	g_free(data->msg);
46 	g_free(data);
47 }
48 
49 #define MAX_TXT_CONSTITUENT_LEN 255
50 
51 /* Make sure that the value isn't longer than it is supposed to be */
52 static const char*
get_max_txt_record_value(const char * key,const char * value)53 get_max_txt_record_value(const char *key, const char *value)
54 {
55 	/* "each constituent string of a DNS TXT record is limited to 255 bytes"
56 	 * This includes the key and the '='
57 	 */
58 	static char buffer[MAX_TXT_CONSTITUENT_LEN + 1];
59 	gchar *end_valid = NULL;
60 	int len = MIN(strlen(value), MAX_TXT_CONSTITUENT_LEN - (strlen(key) + 2));
61 
62 	strncpy(buffer, value, len);
63 
64 	buffer[len] = '\0';
65 
66 	/* If we've cut part of a utf-8 character, kill it */
67 	if (!g_utf8_validate(buffer, -1, (const gchar **)&end_valid))
68 		*end_valid = '\0';
69 
70 	return buffer;
71 }
72 
generate_presence_txt_records(BonjourDnsSd * data)73 static GSList *generate_presence_txt_records(BonjourDnsSd *data) {
74 	GSList *ret = NULL;
75 	PurpleKeyValuePair *kvp;
76 	char portstring[6];
77 	const char *jid, *aim, *email;
78 
79 	/* Convert the port to a string */
80 	snprintf(portstring, sizeof(portstring), "%d", data->port_p2pj);
81 
82 	jid = purple_account_get_string(data->account, "jid", NULL);
83 	aim = purple_account_get_string(data->account, "AIM", NULL);
84 	email = purple_account_get_string(data->account, "email", NULL);
85 
86 #define _M_ADD_R(k, v) \
87 	kvp = g_new0(PurpleKeyValuePair, 1); \
88 	kvp->key = g_strdup(k); \
89 	kvp->value = g_strdup(get_max_txt_record_value(k, v)); \
90 	ret = g_slist_prepend(ret, kvp); \
91 
92 	/* We should try to follow XEP-0174, but some clients have "issues", so we humor them.
93 	 * See http://telepathy.freedesktop.org/wiki/SalutInteroperability
94 	 */
95 
96 	/* Large TXT records are problematic.
97 	 * While it is technically possible for this to exceed a standard 512-byte
98 	 * DNS message, it shouldn't happen unless we get wacky data entered for
99 	 * some of the freeform fields.  It is even less likely to exceed the
100 	 * recommended maximum of 1300 bytes.
101 	 */
102 
103 	/* Needed by iChat */
104 	_M_ADD_R("txtvers", "1")
105 	/* Needed by Gaim/Pidgin <= 2.0.1 (remove at some point) */
106 	_M_ADD_R("1st", data->first)
107 	/* Needed by Gaim/Pidgin <= 2.0.1 (remove at some point) */
108 	_M_ADD_R("last", data->last)
109 	/* Needed by Adium */
110 	_M_ADD_R("port.p2pj", portstring)
111 	/* Needed by iChat, Gaim/Pidgin <= 2.0.1 */
112 	_M_ADD_R("status", data->status)
113 	_M_ADD_R("node", "libpurple")
114 	_M_ADD_R("ver", VERSION)
115 	/* Currently always set to "!" since we don't support AV and wont ever be in a conference */
116 	_M_ADD_R("vc", data->vc)
117 	if (email != NULL && *email != '\0') {
118 		_M_ADD_R("email", email)
119 	}
120 	if (jid != NULL && *jid != '\0') {
121 		_M_ADD_R("jid", jid)
122 	}
123 	/* Nonstandard, but used by iChat */
124 	if (aim != NULL && *aim != '\0') {
125 		_M_ADD_R("AIM", aim)
126 	}
127 	if (data->msg != NULL && *data->msg != '\0') {
128 		_M_ADD_R("msg", data->msg)
129 	}
130 	if (data->phsh != NULL && *data->phsh != '\0') {
131 		_M_ADD_R("phsh", data->phsh)
132 	}
133 
134 	/* TODO: ext, nick */
135 	return ret;
136 }
137 
free_presence_txt_records(GSList * lst)138 static void free_presence_txt_records(GSList *lst) {
139 	PurpleKeyValuePair *kvp;
140 	while(lst) {
141 		kvp = lst->data;
142 		g_free(kvp->key);
143 		g_free(kvp->value);
144 		g_free(kvp);
145 		lst = g_slist_delete_link(lst, lst);
146 	}
147 }
148 
publish_presence(BonjourDnsSd * data,PublishType type)149 static gboolean publish_presence(BonjourDnsSd *data, PublishType type) {
150 	GSList *txt_records;
151 	gboolean ret;
152 
153 	txt_records = generate_presence_txt_records(data);
154 	ret = _mdns_publish(data, type, txt_records);
155 	free_presence_txt_records(txt_records);
156 
157 	return ret;
158 }
159 
160 /**
161  * Send a new dns-sd packet updating our status.
162  */
bonjour_dns_sd_send_status(BonjourDnsSd * data,const char * status,const char * status_message)163 void bonjour_dns_sd_send_status(BonjourDnsSd *data, const char *status, const char *status_message) {
164 	g_free(data->status);
165 	g_free(data->msg);
166 
167 	data->status = g_strdup(status);
168 	data->msg = g_strdup(status_message);
169 
170 	/* Update our text record with the new status */
171 	publish_presence(data, PUBLISH_UPDATE);
172 }
173 
174 /**
175  * Retrieve the buddy icon blob
176  */
bonjour_dns_sd_retrieve_buddy_icon(BonjourBuddy * buddy)177 void bonjour_dns_sd_retrieve_buddy_icon(BonjourBuddy* buddy) {
178 	_mdns_retrieve_buddy_icon(buddy);
179 }
180 
bonjour_dns_sd_update_buddy_icon(BonjourDnsSd * data)181 void bonjour_dns_sd_update_buddy_icon(BonjourDnsSd *data) {
182 	PurpleStoredImage *img;
183 
184 	if ((img = purple_buddy_icons_find_account_icon(data->account))) {
185 		gconstpointer avatar_data;
186 		gsize avatar_len;
187 
188 		avatar_data = purple_imgstore_get_data(img);
189 		avatar_len = purple_imgstore_get_size(img);
190 
191 		if (_mdns_set_buddy_icon_data(data, avatar_data, avatar_len)) {
192 			/* The filename is a SHA-1 hash of the data (conveniently what we need) */
193 			const char *p, *filename = purple_imgstore_get_filename(img);
194 
195 			g_free(data->phsh);
196 			data->phsh = NULL;
197 
198 			/* Get rid of the extension */
199 			p = strchr(filename, '.');
200 			if (p)
201 				data->phsh = g_strndup(filename, p - filename);
202 			else
203 				purple_debug_error("bonjour", "account buddy icon returned unexpected filename (%s)"
204 								"; unable to extract hash. Clearing buddy icon\n", filename);
205 
206 			/* Update our TXT record */
207 			publish_presence(data, PUBLISH_UPDATE);
208 		}
209 
210 		purple_imgstore_unref(img);
211 	} else {
212 		/* We need to do this regardless of whether data->phsh is set so that we
213 		 * cancel any icons that are currently in the process of being set */
214 		_mdns_set_buddy_icon_data(data, NULL, 0);
215 		if (data->phsh != NULL) {
216 			/* Clear the buddy icon */
217 			g_free(data->phsh);
218 			data->phsh = NULL;
219 			/* Update our TXT record */
220 			publish_presence(data, PUBLISH_UPDATE);
221 		}
222 	}
223 }
224 
225 /**
226  * Advertise our presence within the dns-sd daemon and start browsing
227  * for other bonjour peers.
228  */
bonjour_dns_sd_start(BonjourDnsSd * data)229 gboolean bonjour_dns_sd_start(BonjourDnsSd *data) {
230 
231 	/* Initialize the dns-sd data and session */
232 	if (!_mdns_init_session(data))
233 		return FALSE;
234 
235 	/* Publish our bonjour IM client at the mDNS daemon */
236 	if (!publish_presence(data, PUBLISH_START))
237 		return FALSE;
238 
239 	/* Advise the daemon that we are waiting for connections */
240 	if (!_mdns_browse(data)) {
241 		purple_debug_error("bonjour", "Unable to get service.\n");
242 		return FALSE;
243 	}
244 
245 	return TRUE;
246 }
247 
248 /**
249  * Unregister the "_presence._tcp" service at the mDNS daemon.
250  */
251 
bonjour_dns_sd_stop(BonjourDnsSd * data)252 void bonjour_dns_sd_stop(BonjourDnsSd *data) {
253 	_mdns_stop(data);
254 }
255 
256 void
bonjour_dns_sd_set_jid(PurpleAccount * account,const char * hostname)257 bonjour_dns_sd_set_jid(PurpleAccount *account, const char *hostname)
258 {
259 	PurpleConnection *conn = purple_account_get_connection(account);
260 	BonjourData *bd = conn->proto_data;
261 	const char *tmp, *account_name = purple_account_get_username(account);
262 
263 	/* Previously we allowed the hostname part of the jid to be set
264 	 * explicitly when it should always be the current hostname.
265 	 * That is what this is intended to deal with.
266 	 */
267 	if ((tmp = strchr(account_name, '@'))
268 	    && strstr(tmp, hostname) == (tmp + 1)
269 	    && *((tmp + 1) + strlen(hostname)) == '\0')
270 		bd->jid = g_strdup(account_name);
271 	else {
272 		const char *tmp2;
273 		GString *str = g_string_new("");
274 		/* Escape an '@' in the account name */
275 		tmp = account_name;
276 		while ((tmp2 = strchr(tmp, '@')) != NULL) {
277 			g_string_append_len(str, tmp, tmp2 - tmp);
278 			g_string_append(str, "\\40");
279 			tmp = tmp2 + 1;
280 		}
281 		g_string_append(str, tmp);
282 		g_string_append_c(str, '@');
283 		g_string_append(str, hostname);
284 
285 		bd->jid = g_string_free(str, FALSE);
286 	}
287 }
288