1 /*
2  * Hangouts Plugin for libpurple/Pidgin
3  * Copyright (c) 2015-2016 Eion Robb, Mike Ruprecht
4  *
5  * This program is free software: you can redistribute it and/or modify
6  * it under the terms of the GNU General Public License as published by
7  * the Free Software Foundation, either version 3 of the License, or
8  * (at your option) any later version.
9  *
10  * This program is distributed in the hope that it will be useful,
11  * but WITHOUT ANY WARRANTY; without even the implied warranty of
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13  * GNU General Public License for more details.
14  *
15  * You should have received a copy of the GNU General Public License
16  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
17  */
18 
19 #define PURPLE_PLUGINS
20 
21 
22 #include "hangouts_connection.h"
23 
24 
25 #include <stdlib.h>
26 #include <string.h>
27 #include <stdio.h>
28 #include <unistd.h>
29 
30 #include "debug.h"
31 #include "request.h"
32 
33 #include "hangouts_pblite.h"
34 #include "hangouts_json.h"
35 #include "hangouts.pb-c.h"
36 #include "hangouts_conversation.h"
37 
38 #include "gmail.pb-c.h"
39 
40 void
hangouts_process_data_chunks(HangoutsAccount * ha,const gchar * data,gsize len)41 hangouts_process_data_chunks(HangoutsAccount *ha, const gchar *data, gsize len)
42 {
43 	JsonArray *chunks;
44 	guint i, num_chunks;
45 
46 	chunks = json_decode_array(data, len);
47 
48 	for (i = 0, num_chunks = json_array_get_length(chunks); i < num_chunks; i++) {
49 		JsonArray *chunk;
50 		JsonArray *array;
51 		JsonNode *array0;
52 
53 		chunk = json_array_get_array_element(chunks, i);
54 
55 		array = json_array_get_array_element(chunk, 1);
56 		array0 = json_array_get_element(array, 0);
57 		if (JSON_NODE_HOLDS_VALUE(array0)) {
58 			//probably a nooooop
59 			if (g_strcmp0(json_node_get_string(array0), "noop") == 0) {
60 				//A nope ninja delivers a wicked dragon kick
61 #ifdef DEBUG
62 				printf("noop\n");
63 #endif
64 			}
65 		} else {
66 			const gchar *p = json_object_get_string_member(json_node_get_object(array0), "p");
67 			JsonObject *wrapper = json_decode_object(p, -1);
68 
69 			if (wrapper == NULL) {
70 				continue;
71 			}
72 
73 			if (json_object_has_member(wrapper, "3")) {
74 				const gchar *new_client_id = json_object_get_string_member(json_object_get_object_member(wrapper, "3"), "2");
75 				purple_debug_info("hangouts", "Received new client_id: %s\n", new_client_id);
76 
77 				g_free(ha->client_id);
78 				ha->client_id = g_strdup(new_client_id);
79 
80 				hangouts_add_channel_services(ha);
81 				hangouts_set_active_client(ha->pc);
82 				hangouts_set_status(ha->account, purple_account_get_active_status(ha->account));
83 			}
84 			if (json_object_has_member(wrapper, "2")) {
85 				const gchar *wrapper22 = json_object_get_string_member(json_object_get_object_member(wrapper, "2"), "2");
86 				JsonArray *pblite_message = json_decode_array(wrapper22, -1);
87 				const gchar *message_type;
88 
89 				if (pblite_message == NULL) {
90 #ifdef DEBUG
91 					printf("bad wrapper22 %s\n", wrapper22);
92 #endif
93 					json_object_unref(wrapper);
94 					continue;
95 				}
96 
97 				message_type = json_array_get_string_element(pblite_message, 0);
98 
99 				//cbu == ClientBatchUpdate
100 				if (purple_strequal(message_type, "cbu")) {
101 					BatchUpdate batch_update = BATCH_UPDATE__INIT;
102 					guint j;
103 
104 #ifdef DEBUG
105 					printf("----------------------\n");
106 #endif
107 					pblite_decode((ProtobufCMessage *) &batch_update, pblite_message, TRUE);
108 #ifdef DEBUG
109 					printf("======================\n");
110 					printf("Is valid? %s\n", protobuf_c_message_check((ProtobufCMessage *) &batch_update) ? "Yes" : "No");
111 					printf("======================\n");
112 					printf("CBU %s", pblite_dump_json((ProtobufCMessage *)&batch_update));
113 					JsonArray *debug = pblite_encode((ProtobufCMessage *) &batch_update);
114 					JsonNode *node = json_node_new(JSON_NODE_ARRAY);
115 					json_node_take_array(node, debug);
116 					gchar *json = json_encode(node, NULL);
117 					printf("Old: %s\nNew: %s\n", wrapper22, json);
118 
119 
120 					pblite_decode((ProtobufCMessage *) &batch_update, debug, TRUE);
121 					debug = pblite_encode((ProtobufCMessage *) &batch_update);
122 					json_node_take_array(node, debug);
123 					gchar *json2 = json_encode(node, NULL);
124 					printf("Mine1: %s\nMine2: %s\n", json, json2);
125 
126 					g_free(json);
127 					g_free(json2);
128 					printf("----------------------\n");
129 #endif
130 					for(j = 0; j < batch_update.n_state_update; j++) {
131 						purple_signal_emit(purple_connection_get_protocol(ha->pc), "hangouts-received-stateupdate", ha->pc, batch_update.state_update[j]);
132 					}
133 				} else if (purple_strequal(message_type, "n_nm")) {
134 					GmailNotification gmail_notification = GMAIL_NOTIFICATION__INIT;
135 					const gchar *username = json_object_get_string_member(json_object_get_object_member(json_object_get_object_member(wrapper, "2"), "1"), "2");
136 
137 					pblite_decode((ProtobufCMessage *) &gmail_notification, pblite_message, TRUE);
138 					purple_signal_emit(purple_connection_get_protocol(ha->pc), "hangouts-gmail-notification", ha->pc, username, &gmail_notification);
139 				}
140 
141 				json_array_unref(pblite_message);
142 			}
143 
144 			json_object_unref(wrapper);
145 		}
146 	}
147 
148 	json_array_unref(chunks);
149 }
150 
151 static int
read_all(int fd,void * buf,size_t len)152 read_all(int fd, void *buf, size_t len)
153 {
154 	unsigned int rs = 0;
155 	while(rs < len)
156 	{
157 		int rval = read(fd, buf + rs, len - rs);
158 		if (rval == 0)
159 			break;
160 		if (rval < 0)
161 			return rval;
162 
163 		rs += rval;
164 	}
165 	return rs;
166 }
167 
168 void
hangouts_process_channel(int fd)169 hangouts_process_channel(int fd)
170 {
171 	gsize len, lenpos = 0;
172 	gchar len_str[256];
173 	gchar *chunk;
174 
175 	while(read(fd, len_str + lenpos, 1) > 0) {
176 		//read up until \n
177 		if (len_str[lenpos] == '\n') {
178 			//convert to int, use as length of string to read
179 			len_str[lenpos] = '\0';
180 #ifdef DEBUG
181 			printf("len_str is %s\n", len_str);
182 #endif
183 			len = atoi(len_str);
184 
185 			chunk = g_new(gchar, len * 2);
186 			//XX - could be a utf-16 length*2 though, so read up until \n????
187 
188 			if (read_all(fd, chunk, len) > 0) {
189 				//throw chunk to hangouts_process_data_chunks
190 				hangouts_process_data_chunks(NULL, chunk, len);
191 			}
192 
193 			g_free(chunk);
194 
195 			lenpos = 0;
196 		} else {
197 			lenpos = lenpos + 1;
198 		}
199 	}
200 }
201 
202 void
hangouts_process_channel_buffer(HangoutsAccount * ha)203 hangouts_process_channel_buffer(HangoutsAccount *ha)
204 {
205 	const gchar *bufdata;
206 	gsize bufsize;
207 	gchar *len_end;
208 	gchar *len_str;
209 	guint len_len; //len len len len len len len len len
210 	gsize len;
211 
212 	g_return_if_fail(ha);
213 	g_return_if_fail(ha->channel_buffer);
214 
215 	while (ha->channel_buffer->len) {
216 		bufdata = (gchar *) ha->channel_buffer->data;
217 		bufsize = ha->channel_buffer->len;
218 
219 		len_end = g_strstr_len(bufdata, bufsize, "\n");
220 		if (len_end == NULL) {
221 			// Not enough data to read
222 			if (purple_debug_is_verbose()) {
223 				purple_debug_info("hangouts", "Couldn't find length of chunk\n");
224 			}
225 			return;
226 		}
227 		len_len = len_end - bufdata;
228 		len_str = g_strndup(bufdata, len_len);
229 		len = (gsize) atoi(len_str);
230 		g_free(len_str);
231 
232 		// Len was 0 ?  Must have been a bad read :(
233 		g_return_if_fail(len);
234 
235 		bufsize = bufsize - len_len - 1;
236 
237 		if (len > bufsize) {
238 			// Not enough data to read
239 			if (purple_debug_is_verbose()) {
240 				purple_debug_info("hangouts", "Couldn't read %" G_GSIZE_FORMAT " bytes when we only have %" G_GSIZE_FORMAT "\n", len, bufsize);
241 			}
242 			return;
243 		}
244 
245 		hangouts_process_data_chunks(ha, bufdata + len_len + 1, len);
246 
247 		g_byte_array_remove_range(ha->channel_buffer, 0, len + len_len + 1);
248 
249 	}
250 }
251 
252 static void
hangouts_set_auth_headers(HangoutsAccount * ha,PurpleHttpRequest * request)253 hangouts_set_auth_headers(HangoutsAccount *ha, PurpleHttpRequest *request)
254 {
255 	gint64 mstime;
256 	gchar *mstime_str;
257 	GTimeVal time;
258 	GChecksum *hash;
259 	const gchar *sha1;
260 	gchar *sapisid_cookie;
261 
262 	g_get_current_time(&time);
263 	mstime = (((gint64) time.tv_sec) * 1000) + (time.tv_usec / 1000);
264 	mstime_str = g_strdup_printf("%" G_GINT64_FORMAT, mstime);
265 	sapisid_cookie = purple_http_cookie_jar_get(ha->cookie_jar, "SAPISID");
266 
267 	hash = g_checksum_new(G_CHECKSUM_SHA1);
268 	g_checksum_update(hash, (guchar *) mstime_str, strlen(mstime_str));
269 	g_checksum_update(hash, (guchar *) " ", 1);
270 	if (sapisid_cookie && *sapisid_cookie) {
271 		// Should we just bail out if we dont have the cookie?
272 		g_checksum_update(hash, (guchar *) sapisid_cookie, strlen(sapisid_cookie));
273 	}
274 	g_checksum_update(hash, (guchar *) " ", 1);
275 	g_checksum_update(hash, (guchar *) HANGOUTS_PBLITE_XORIGIN_URL, strlen(HANGOUTS_PBLITE_XORIGIN_URL));
276 	sha1 = g_checksum_get_string(hash);
277 
278 	purple_http_request_header_set_printf(request, "Authorization", "SAPISIDHASH %s_%s", mstime_str, sha1);
279 	purple_http_request_header_set(request, "X-Origin", HANGOUTS_PBLITE_XORIGIN_URL);
280 	purple_http_request_header_set(request, "X-Goog-AuthUser", "0");
281 
282 	g_free(sapisid_cookie);
283 	g_free(mstime_str);
284 	g_checksum_free(hash);
285 }
286 
287 
288 static gboolean
hangouts_longpoll_request_content(PurpleHttpConnection * http_conn,PurpleHttpResponse * response,const gchar * buffer,size_t offset,size_t length,gpointer user_data)289 hangouts_longpoll_request_content(PurpleHttpConnection *http_conn, PurpleHttpResponse *response, const gchar *buffer, size_t offset, size_t length, gpointer user_data)
290 {
291 	HangoutsAccount *ha = user_data;
292 
293 	ha->last_data_received = time(NULL);
294 
295 	if (!purple_http_response_is_successful(response)) {
296 		purple_debug_error("hangouts", "longpoll_request_content had error: '%s'\n", purple_http_response_get_error(response));
297 		return FALSE;
298 	}
299 
300 	g_byte_array_append(ha->channel_buffer, (guint8 *) buffer, length);
301 
302 	hangouts_process_channel_buffer(ha);
303 
304 	return TRUE;
305 }
306 
307 static void
hangouts_longpoll_request_closed(PurpleHttpConnection * http_conn,PurpleHttpResponse * response,gpointer user_data)308 hangouts_longpoll_request_closed(PurpleHttpConnection *http_conn, PurpleHttpResponse *response, gpointer user_data)
309 {
310 	HangoutsAccount *ha = user_data;
311 
312 	if (!PURPLE_IS_CONNECTION(purple_http_conn_get_purple_connection(http_conn))) {
313 		return;
314 	}
315 
316 	if (ha->channel_watchdog) {
317 		g_source_remove(ha->channel_watchdog);
318 		ha->channel_watchdog = 0;
319 	}
320 
321 	// remaining data 'should' have been dealt with in hangouts_longpoll_request_content
322 	g_byte_array_free(ha->channel_buffer, TRUE);
323 	ha->channel_buffer = g_byte_array_sized_new(HANGOUTS_BUFFER_DEFAULT_SIZE);
324 
325 	if (purple_http_response_get_error(response) != NULL) {
326 		//TODO error checking
327 		purple_debug_error("hangouts", "longpoll_request_closed %d %s\n", purple_http_response_get_code(response), purple_http_response_get_error(response));
328 		hangouts_fetch_channel_sid(ha);
329 	} else {
330 		hangouts_longpoll_request(ha);
331 	}
332 }
333 
334 static gboolean
channel_watchdog_check(gpointer data)335 channel_watchdog_check(gpointer data)
336 {
337 	PurpleConnection *pc = data;
338 	HangoutsAccount *ha;
339 	PurpleHttpConnection *conn;
340 
341 	if (PURPLE_IS_CONNECTION(pc)) {
342 		ha = purple_connection_get_protocol_data(pc);
343 		conn = ha->channel_connection;
344 
345 		if (ha->last_data_received && ha->last_data_received < (time(NULL) - 60)) {
346 			// should have been something within the last 60 seconds
347 			purple_http_conn_cancel(conn);
348 			ha->last_data_received = 0;
349 		}
350 
351 		if (!purple_http_conn_is_running(conn)) {
352 			hangouts_longpoll_request(ha);
353 		}
354 
355 		return TRUE;
356 	}
357 
358 	return FALSE;
359 }
360 
361 void
hangouts_longpoll_request(HangoutsAccount * ha)362 hangouts_longpoll_request(HangoutsAccount *ha)
363 {
364 	PurpleHttpRequest *request;
365 	GString *url;
366 
367 
368 	url = g_string_new(HANGOUTS_CHANNEL_URL_PREFIX "channel/bind" "?");
369 	g_string_append(url, "VER=8&");           // channel protocol version
370 	g_string_append_printf(url, "gsessionid=%s&", purple_url_encode(ha->gsessionid_param));
371 	g_string_append(url, "RID=rpc&");         // request identifier
372 	g_string_append(url, "t=1&");             // trial
373 	g_string_append_printf(url, "SID=%s&", purple_url_encode(ha->sid_param));  // session ID
374 	g_string_append(url, "CI=0&");            // 0 if streaming/chunked requests should be used
375 	g_string_append(url, "ctype=hangouts&");  // client type
376 	g_string_append(url, "TYPE=xmlhttp&");    // type of request
377 
378 	request = purple_http_request_new(NULL);
379 	purple_http_request_set_cookie_jar(request, ha->cookie_jar);
380 	purple_http_request_set_url(request, url->str);
381 	purple_http_request_set_timeout(request, -1);  // to infinity and beyond!
382 	purple_http_request_set_response_writer(request, hangouts_longpoll_request_content, ha);
383 	purple_http_request_set_keepalive_pool(request, ha->channel_keepalive_pool);
384 
385 	hangouts_set_auth_headers(ha, request);
386 
387 	ha->channel_connection = purple_http_request(ha->pc, request, hangouts_longpoll_request_closed, ha);
388 
389 	g_string_free(url, TRUE);
390 
391 	if (ha->channel_watchdog) {
392 		g_source_remove(ha->channel_watchdog);
393 	}
394 	ha->channel_watchdog = g_timeout_add_seconds(1, channel_watchdog_check, ha->pc);
395 }
396 
397 
398 
399 static void
hangouts_send_maps_cb(PurpleHttpConnection * http_conn,PurpleHttpResponse * response,gpointer user_data)400 hangouts_send_maps_cb(PurpleHttpConnection *http_conn, PurpleHttpResponse *response, gpointer user_data)
401 {
402 	/*111
403 	 * [
404 	 * [0,["c","<sid>","",8]],
405 	 * [1,[{"gsid":"<gsid>"}]]
406 	 * ]
407 	 */
408 	JsonNode *node;
409 	HangoutsAccount *ha = user_data;
410 	const gchar *res_raw;
411 	gchar *json_start;
412 	size_t res_len;
413 	gchar *gsid;
414 	gchar *sid;
415 
416 	if (purple_http_response_get_error(response) != NULL) {
417 		purple_connection_error(ha->pc, PURPLE_CONNECTION_ERROR_NETWORK_ERROR, purple_http_response_get_error(response));
418 		return;
419 	}
420 
421 	res_raw = purple_http_response_get_data(response, &res_len);
422 	json_start = g_strstr_len(res_raw, res_len, "\n");
423 	if (json_start == NULL) {
424 		purple_connection_error(ha->pc, PURPLE_CONNECTION_ERROR_NETWORK_ERROR, "Blank maps response");
425 		return;
426 	}
427 	*json_start = '\0';
428 	json_start++;
429 	node = json_decode(json_start, atoi(res_raw));
430 	sid = hangouts_json_path_query_string(node, "$[0][1][1]", NULL);
431 	gsid = hangouts_json_path_query_string(node, "$[1][1][0].gsid", NULL);
432 
433 	if (sid != NULL) {
434 		g_free(ha->sid_param);
435 		ha->sid_param = sid;
436 	}
437 	if (gsid != NULL) {
438 		g_free(ha->gsessionid_param);
439 		ha->gsessionid_param = gsid;
440 	}
441 
442 	json_node_free(node);
443 
444 	hangouts_longpoll_request(ha);
445 }
446 
447 void
hangouts_fetch_channel_sid(HangoutsAccount * ha)448 hangouts_fetch_channel_sid(HangoutsAccount *ha)
449 {
450 	g_free(ha->sid_param);
451 	g_free(ha->gsessionid_param);
452 	ha->sid_param = NULL;
453 	ha->gsessionid_param = NULL;
454 
455 	hangouts_send_maps(ha, NULL, hangouts_send_maps_cb);
456 }
457 
458 void
hangouts_send_maps(HangoutsAccount * ha,JsonArray * map_list,PurpleHttpCallback send_maps_callback)459 hangouts_send_maps(HangoutsAccount *ha, JsonArray *map_list, PurpleHttpCallback send_maps_callback)
460 {
461 	PurpleHttpRequest *request;
462 	GString *url, *postdata;
463 	guint map_list_len, i;
464 
465 	url = g_string_new(HANGOUTS_CHANNEL_URL_PREFIX "channel/bind" "?");
466 	g_string_append(url, "VER=8&");           // channel protocol version
467 	g_string_append(url, "RID=81188&");       // request identifier
468 	g_string_append(url, "ctype=hangouts&");  // client type
469 	if (ha->gsessionid_param)
470 		g_string_append_printf(url, "gsessionid=%s&", purple_url_encode(ha->gsessionid_param));
471 	if (ha->sid_param)
472 		g_string_append_printf(url, "SID=%s&", purple_url_encode(ha->sid_param));  // session ID
473 
474 	request = purple_http_request_new(NULL);
475 	purple_http_request_set_cookie_jar(request, ha->cookie_jar);
476 	purple_http_request_set_url(request, url->str);
477 	purple_http_request_set_method(request, "POST");
478 	purple_http_request_header_set(request, "Content-Type", "application/x-www-form-urlencoded");
479 
480 	hangouts_set_auth_headers(ha, request);
481 
482 	postdata = g_string_new(NULL);
483 	if (map_list != NULL) {
484 		map_list_len = json_array_get_length(map_list);
485 		g_string_append_printf(postdata, "count=%u&", map_list_len);
486 		g_string_append(postdata, "ofs=0&");
487 		for(i = 0; i < map_list_len; i++) {
488 			JsonObject *obj = json_array_get_object_element(map_list, i);
489 			GList *members = json_object_get_members(obj);
490 			GList *l;
491 
492 			for (l = members; l != NULL; l = l->next) {
493 				const gchar *member_name = l->data;
494 				JsonNode *value = json_object_get_member(obj, member_name);
495 
496 				g_string_append_printf(postdata, "req%u_%s=", i, purple_url_encode(member_name));
497 				g_string_append_printf(postdata, "%s&", purple_url_encode(json_node_get_string(value)));
498 			}
499 
500 			g_list_free(members);
501 		}
502 	}
503 	purple_http_request_set_contents(request, postdata->str, postdata->len);
504 
505 	purple_http_request(ha->pc, request, send_maps_callback, ha);
506 	purple_http_request_unref(request);
507 
508 	g_string_free(postdata, TRUE);
509 	g_string_free(url, TRUE);
510 }
511 
512 void
hangouts_add_channel_services(HangoutsAccount * ha)513 hangouts_add_channel_services(HangoutsAccount *ha)
514 {
515 	JsonArray *map_list = json_array_new();
516 	JsonObject *obj;
517 
518 	// TODO Work out what this is for
519 	obj = json_object_new();
520 	json_object_set_string_member(obj, "p", "{\"3\":{\"1\":{\"1\":\"tango_service\"}}}");
521 	json_array_add_object_element(map_list, obj);
522 
523 	// This is for the chat messages
524 	obj = json_object_new();
525 	json_object_set_string_member(obj, "p", "{\"3\":{\"1\":{\"1\":\"babel\"}}}");
526 	json_array_add_object_element(map_list, obj);
527 
528 	// This is for the presence updates
529 	obj = json_object_new();
530 	json_object_set_string_member(obj, "p", "{\"3\":{\"1\":{\"1\":\"babel_presence_last_seen\"}}}");
531 	json_array_add_object_element(map_list, obj);
532 
533 	// TODO Work out what this is for
534 	obj = json_object_new();
535 	json_object_set_string_member(obj, "p", "{\"3\":{\"1\":{\"1\":\"hangout_invite\"}}}");
536 	json_array_add_object_element(map_list, obj);
537 
538 	obj = json_object_new();
539 	json_object_set_string_member(obj, "p", "{\"3\":{\"1\":{\"1\":\"gmail\"}}}");
540 	json_array_add_object_element(map_list, obj);
541 
542 	hangouts_send_maps(ha, map_list, NULL);
543 
544 	json_array_unref(map_list);
545 }
546 
547 
548 typedef struct {
549 	HangoutsAccount *ha;
550 	HangoutsPbliteResponseFunc callback;
551 	ProtobufCMessage *response_message;
552 	gpointer user_data;
553 } LazyPblistRequestStore;
554 
555 static void
hangouts_pblite_request_cb(PurpleHttpConnection * http_conn,PurpleHttpResponse * response,gpointer user_data)556 hangouts_pblite_request_cb(PurpleHttpConnection *http_conn, PurpleHttpResponse *response, gpointer user_data)
557 {
558 	LazyPblistRequestStore *request_info = user_data;
559 	HangoutsAccount *ha = request_info->ha;
560 	HangoutsPbliteResponseFunc callback = request_info->callback;
561 	gpointer real_user_data = request_info->user_data;
562 	ProtobufCMessage *response_message = request_info->response_message;
563 	ProtobufCMessage *unpacked_message;
564 	const gchar *raw_response;
565 	guchar *decoded_response;
566 	gsize response_len;
567 	const gchar *content_type;
568 
569 	if (purple_http_response_get_error(response) != NULL) {
570 		g_free(request_info);
571 		g_free(response_message);
572 		purple_debug_error("hangouts", "Error from server: (%s) %s\n", purple_http_response_get_error(response), purple_http_response_get_data(response, NULL));
573 		return; //TODO should we send NULL to the callee?
574 	}
575 
576 	if (callback != NULL) {
577 		raw_response = purple_http_response_get_data(response, NULL);
578 
579 		content_type = purple_http_response_get_header(response, "X-Goog-Safety-Content-Type");
580 		if (g_strcmp0(content_type, "application/x-protobuf") == 0) {
581 			decoded_response = g_base64_decode(raw_response, &response_len);
582 			unpacked_message = protobuf_c_message_unpack(response_message->descriptor, NULL, response_len, decoded_response);
583 
584 			if (unpacked_message != NULL) {
585 				if (purple_debug_is_verbose()) {
586 					gchar *pretty_json = pblite_dump_json(unpacked_message);
587 					purple_debug_misc("hangouts", "Response: %s", pretty_json);
588 					g_free(pretty_json);
589 				}
590 
591 				callback(ha, unpacked_message, real_user_data);
592 				protobuf_c_message_free_unpacked(unpacked_message, NULL);
593 			} else {
594 				purple_debug_error("hangouts", "Error decoding protobuf!\n");
595 			}
596 		} else {
597 			gchar *tidied_json = hangouts_json_tidy_blank_arrays(raw_response);
598 			JsonArray *response_array = json_decode_array(tidied_json, -1);
599 			const gchar *first_element = json_array_get_string_element(response_array, 0);
600 			gboolean ignore_first_element = (first_element != NULL);
601 
602 			pblite_decode(response_message, response_array, ignore_first_element);
603 			if (ignore_first_element) {
604 				purple_debug_info("hangouts", "A '%s' says '%s'\n", response_message->descriptor->name, first_element);
605 			}
606 
607 			if (purple_debug_is_verbose()) {
608 				gchar *pretty_json = pblite_dump_json(response_message);
609 				purple_debug_misc("hangouts", "Response: %s", pretty_json);
610 				g_free(pretty_json);
611 			}
612 
613 			callback(ha, response_message, real_user_data);
614 
615 			json_array_unref(response_array);
616 			g_free(tidied_json);
617 		}
618 	}
619 
620 	g_free(request_info);
621 	g_free(response_message);
622 }
623 
624 PurpleHttpConnection *
hangouts_client6_request(HangoutsAccount * ha,const gchar * path,HangoutsContentType request_type,const gchar * request_data,gssize request_len,HangoutsContentType response_type,PurpleHttpCallback callback,gpointer user_data)625 hangouts_client6_request(HangoutsAccount *ha, const gchar *path, HangoutsContentType request_type, const gchar *request_data, gssize request_len, HangoutsContentType response_type, PurpleHttpCallback callback, gpointer user_data)
626 {
627 	PurpleHttpRequest *request;
628 	PurpleHttpConnection *connection;
629 	const gchar *response_type_str;
630 
631 	switch (response_type) {
632 		default:
633 		case HANGOUTS_CONTENT_TYPE_NONE:
634 		case HANGOUTS_CONTENT_TYPE_JSON:
635 			response_type_str = "json";
636 			break;
637 		case HANGOUTS_CONTENT_TYPE_PBLITE:
638 			response_type_str = "protojson";
639 			break;
640 		case HANGOUTS_CONTENT_TYPE_PROTOBUF:
641 			response_type_str = "proto";
642 			break;
643 	}
644 
645 	request = purple_http_request_new(NULL);
646 	purple_http_request_set_url_printf(request, HANGOUTS_PBLITE_API_URL "%s%ckey=" GOOGLE_GPLUS_KEY "&alt=%s", path, (strchr(path, '?') ? '&' : '?'), response_type_str);
647 	purple_http_request_set_cookie_jar(request, ha->cookie_jar);
648 	purple_http_request_set_keepalive_pool(request, ha->client6_keepalive_pool);
649 	purple_http_request_set_max_len(request, G_MAXINT32 - 1);
650 
651 	purple_http_request_header_set(request, "X-Goog-Encode-Response-If-Executable", "base64");
652 	if (request_type != HANGOUTS_CONTENT_TYPE_NONE) {
653 		purple_http_request_set_method(request, "POST");
654 		purple_http_request_set_contents(request, request_data, request_len);
655 		if (request_type == HANGOUTS_CONTENT_TYPE_PROTOBUF) {
656 			purple_http_request_header_set(request, "Content-Type", "application/x-protobuf");
657 		} else if (request_type == HANGOUTS_CONTENT_TYPE_PBLITE) {
658 			purple_http_request_header_set(request, "Content-Type", "application/json+protobuf");
659 		} else if (request_type == HANGOUTS_CONTENT_TYPE_JSON) {
660 			purple_http_request_header_set(request, "Content-Type", "application/json");
661 		}
662 	}
663 
664 	hangouts_set_auth_headers(ha, request);
665 	connection = purple_http_request(ha->pc, request, callback, user_data);
666 	purple_http_request_unref(request);
667 
668 	return connection;
669 }
670 
671 void
hangouts_pblite_request(HangoutsAccount * ha,const gchar * endpoint,ProtobufCMessage * request_message,HangoutsPbliteResponseFunc callback,ProtobufCMessage * response_message,gpointer user_data)672 hangouts_pblite_request(HangoutsAccount *ha, const gchar *endpoint, ProtobufCMessage *request_message, HangoutsPbliteResponseFunc callback, ProtobufCMessage *response_message, gpointer user_data)
673 {
674 	gsize request_len;
675 	gchar *request_data;
676 	LazyPblistRequestStore *request_info = g_new0(LazyPblistRequestStore, 1);
677 
678 	JsonArray *request_encoded = pblite_encode(request_message);
679 	JsonNode *node = json_node_new(JSON_NODE_ARRAY);
680 	json_node_take_array(node, request_encoded);
681 	request_data = json_encode(node, &request_len);
682 	json_node_free(node);
683 
684 	request_info->ha = ha;
685 	request_info->callback = callback;
686 	request_info->response_message = response_message;
687 	request_info->user_data = user_data;
688 
689 	if (purple_debug_is_verbose()) {
690 		gchar *pretty_json = pblite_dump_json(request_message);
691 		purple_debug_misc("hangouts", "Request:  %s", pretty_json);
692 		g_free(pretty_json);
693 	}
694 
695 	hangouts_client6_request(ha, endpoint, HANGOUTS_CONTENT_TYPE_PBLITE, request_data, request_len, HANGOUTS_CONTENT_TYPE_PBLITE, hangouts_pblite_request_cb, request_info);
696 
697 	g_free(request_data);
698 }
699 
700 
701 void
hangouts_default_response_dump(HangoutsAccount * ha,ProtobufCMessage * response,gpointer user_data)702 hangouts_default_response_dump(HangoutsAccount *ha, ProtobufCMessage *response, gpointer user_data)
703 {
704 	gchar *dump = pblite_dump_json(response);
705 	purple_debug_info("hangouts", "%s\n", dump);
706 	g_free(dump);
707 }
708 
709 gboolean
hangouts_set_active_client(PurpleConnection * pc)710 hangouts_set_active_client(PurpleConnection *pc)
711 {
712 	HangoutsAccount *ha;
713 	SetActiveClientRequest request;
714 
715 	switch(purple_connection_get_state(pc)) {
716 		case PURPLE_CONNECTION_DISCONNECTED:
717 			// I couldn't eat another bite
718 			return FALSE;
719 		case PURPLE_CONNECTION_CONNECTING:
720 			// Come back for more later
721 			return TRUE;
722 		default:
723 			break;
724 	}
725 
726 	ha = purple_connection_get_protocol_data(pc);
727 	if (ha == NULL) {
728 		g_warn_if_reached();
729 		return TRUE;
730 	}
731 
732 	if (ha->active_client_state == ACTIVE_CLIENT_STATE__ACTIVE_CLIENT_STATE_IS_ACTIVE) {
733 		//We're already the active client
734 		return TRUE;
735 	}
736 	if (ha->idle_time > HANGOUTS_ACTIVE_CLIENT_TIMEOUT) {
737 		//We've gone idle
738 		return TRUE;
739 	}
740 	if (!purple_presence_is_status_primitive_active(purple_account_get_presence(ha->account), PURPLE_STATUS_AVAILABLE)) {
741 		//We're marked as not available somehow
742 		return TRUE;
743 	}
744 	ha->active_client_state = ACTIVE_CLIENT_STATE__ACTIVE_CLIENT_STATE_IS_ACTIVE;
745 
746 	set_active_client_request__init(&request);
747 
748 	request.request_header = hangouts_get_request_header(ha);
749 	request.has_is_active = TRUE;
750 	request.is_active = TRUE;
751 	request.full_jid = g_strdup_printf("%s/%s", purple_account_get_username(ha->account), ha->client_id);
752 	request.has_timeout_secs = TRUE;
753 	request.timeout_secs = HANGOUTS_ACTIVE_CLIENT_TIMEOUT;
754 
755 	hangouts_pblite_set_active_client(ha, &request, (HangoutsPbliteSetActiveClientResponseFunc)hangouts_default_response_dump, NULL);
756 
757 	hangouts_request_header_free(request.request_header);
758 	g_free(request.full_jid);
759 
760 	return TRUE;
761 }
762 
763 
764 void
hangouts_search_results_send_im(PurpleConnection * pc,GList * row,void * user_data)765 hangouts_search_results_send_im(PurpleConnection *pc, GList *row, void *user_data)
766 {
767 	PurpleAccount *account = purple_connection_get_account(pc);
768 	const gchar *who = g_list_nth_data(row, 0);
769 	PurpleIMConversation *imconv;
770 
771 	imconv = purple_conversations_find_im_with_account(who, account);
772 	if (imconv == NULL) {
773 		imconv = purple_im_conversation_new(account, who);
774 	}
775 	purple_conversation_present(PURPLE_CONVERSATION(imconv));
776 }
777 
778 void
hangouts_search_results_get_info(PurpleConnection * pc,GList * row,void * user_data)779 hangouts_search_results_get_info(PurpleConnection *pc, GList *row, void *user_data)
780 {
781 	hangouts_get_info(pc, g_list_nth_data(row, 0));
782 }
783 
784 void
hangouts_search_results_add_buddy(PurpleConnection * pc,GList * row,void * user_data)785 hangouts_search_results_add_buddy(PurpleConnection *pc, GList *row, void *user_data)
786 {
787 	PurpleAccount *account = purple_connection_get_account(pc);
788 
789 	if (!purple_blist_find_buddy(account, g_list_nth_data(row, 0)))
790 		purple_blist_request_add_buddy(account, g_list_nth_data(row, 0), "Hangouts", g_list_nth_data(row, 1));
791 }
792 
793 void
hangouts_search_users_text_cb(PurpleHttpConnection * connection,PurpleHttpResponse * response,gpointer user_data)794 hangouts_search_users_text_cb(PurpleHttpConnection *connection, PurpleHttpResponse *response, gpointer user_data)
795 {
796 	HangoutsAccount *ha = user_data;
797 	const gchar *response_data;
798 	size_t response_size;
799 	JsonArray *resultsarray;
800 	JsonObject *node;
801 	gint index, length;
802 	gchar *search_term;
803 	JsonObject *status;
804 
805 	PurpleNotifySearchResults *results;
806 	PurpleNotifySearchColumn *column;
807 
808 	if (purple_http_response_get_error(response) != NULL) {
809 		purple_notify_error(ha->pc, _("Search Error"), _("There was an error searching for the user"), purple_http_response_get_error(response), purple_request_cpar_from_connection(ha->pc));
810 		g_dataset_destroy(connection);
811 		return;
812 	}
813 
814 	response_data = purple_http_response_get_data(response, &response_size);
815 	node = json_decode_object(response_data, response_size);
816 
817 	search_term = g_dataset_get_data(connection, "search_term");
818 	resultsarray = json_object_get_array_member(node, "results");
819 	length = json_array_get_length(resultsarray);
820 
821 	if (length == 0) {
822 		status = json_object_get_object_member(node, "status");
823 
824 		if (!json_object_has_member(status, "personalResultsNotReady") || json_object_get_boolean_member(status, "personalResultsNotReady") == TRUE) {
825 			//Not ready yet, retry
826 			hangouts_search_users_text(ha, search_term);
827 
828 		} else {
829 			gchar *primary_text = g_strdup_printf(_("Your search for the user \"%s\" returned no results"), search_term);
830 			purple_notify_warning(ha->pc, _("No users found"), primary_text, "", purple_request_cpar_from_connection(ha->pc));
831 			g_free(primary_text);
832 		}
833 
834 		g_dataset_destroy(connection);
835 		json_object_unref(node);
836 		return;
837 	}
838 
839 	results = purple_notify_searchresults_new();
840 	if (results == NULL)
841 	{
842 		g_dataset_destroy(connection);
843 		json_object_unref(node);
844 		return;
845 	}
846 
847 	/* columns: Friend ID, Name, Network */
848 	column = purple_notify_searchresults_column_new(_("ID"));
849 	purple_notify_searchresults_column_add(results, column);
850 	column = purple_notify_searchresults_column_new(_("Display Name"));
851 	purple_notify_searchresults_column_add(results, column);
852 
853 	purple_notify_searchresults_button_add(results, PURPLE_NOTIFY_BUTTON_ADD, hangouts_search_results_add_buddy);
854 	purple_notify_searchresults_button_add(results, PURPLE_NOTIFY_BUTTON_INFO, hangouts_search_results_get_info);
855 	purple_notify_searchresults_button_add(results, PURPLE_NOTIFY_BUTTON_IM, hangouts_search_results_send_im);
856 
857 	for(index = 0; index < length; index++)
858 	{
859 		JsonNode *result = json_array_get_element(resultsarray, index);
860 
861 		gchar *id = hangouts_json_path_query_string(result, "$.person.personId", NULL);
862 		gchar *displayname = hangouts_json_path_query_string(result, "$.person.name[*].displayName", NULL);
863 		GList *row = NULL;
864 
865 		row = g_list_append(row, id);
866 		row = g_list_append(row, displayname);
867 
868 		purple_notify_searchresults_row_add(results, row);
869 	}
870 
871 	purple_notify_searchresults(ha->pc, NULL, search_term, NULL, results, NULL, NULL);
872 
873 	g_dataset_destroy(connection);
874 	json_object_unref(node);
875 }
876 
877 /*
878 
879 POST https://people-pa.clients6.google.com/v2/people/lookup
880 id=actual_email_address%40gmail.com&type=EMAIL&matchType=LENIENT&requestMask.includeField.paths=person.email&requestMask.includeField.paths=person.gender&requestMask.includeField.paths=person.in_app_reachability&requestMask.includeField.paths=person.metadata&requestMask.includeField.paths=person.name&requestMask.includeField.paths=person.phone&requestMask.includeField.paths=person.photo&requestMask.includeField.paths=person.read_only_profile_info&extensionSet.extensionNames=HANGOUTS_ADDITIONAL_DATA&extensionSet.extensionNames=HANGOUTS_OFF_NETWORK_GAIA_LOOKUP&extensionSet.extensionNames=HANGOUTS_PHONE_DATA&coreIdParams.useRealtimeNotificationExpandedAcls=true&key=AIzaSyAfFJCeph-euFSwtmqFZi0kaKk-cZ5wufM
881 
882 id=%2B123456789&type=PHONE&matchType=LENIENT&requestMask.includeField.paths=person.email&requestMask.includeField.paths=person.gender&requestMask.includeField.paths=person.in_app_reachability&requestMask.includeField.paths=person.metadata&requestMask.includeField.paths=person.name&requestMask.includeField.paths=person.phone&requestMask.includeField.paths=person.photo&requestMask.includeField.paths=person.read_only_profile_info&extensionSet.extensionNames=HANGOUTS_ADDITIONAL_DATA&extensionSet.extensionNames=HANGOUTS_OFF_NETWORK_GAIA_LOOKUP&extensionSet.extensionNames=HANGOUTS_PHONE_DATA&coreIdParams.useRealtimeNotificationExpandedAcls=true&quotaFilterType=PHONE&key=AIzaSyAfFJCeph-euFSwtmqFZi0kaKk-cZ5wufM
883 
884 */
885 
886 
887 void
hangouts_search_users_text(HangoutsAccount * ha,const gchar * text)888 hangouts_search_users_text(HangoutsAccount *ha, const gchar *text)
889 {
890 	PurpleHttpRequest *request;
891 	GString *url = g_string_new("https://people-pa.clients6.google.com/v2/people/autocomplete?");
892 	PurpleHttpConnection *connection;
893 
894 	g_string_append_printf(url, "query=%s&", purple_url_encode(text));
895 	g_string_append(url, "client=HANGOUTS_WITH_DATA&");
896 	g_string_append(url, "pageSize=20&");
897 	g_string_append_printf(url, "key=%s&", purple_url_encode(GOOGLE_GPLUS_KEY));
898 
899 	request = purple_http_request_new(NULL);
900 	purple_http_request_set_cookie_jar(request, ha->cookie_jar);
901 	purple_http_request_set_url(request, url->str);
902 
903 	hangouts_set_auth_headers(ha, request);
904 
905 	connection = purple_http_request(ha->pc, request, hangouts_search_users_text_cb, ha);
906 	purple_http_request_unref(request);
907 
908 	g_dataset_set_data_full(connection, "search_term", g_strdup(text), g_free);
909 
910 	g_string_free(url, TRUE);
911 }
912 
913 void
hangouts_search_users(PurpleProtocolAction * action)914 hangouts_search_users(PurpleProtocolAction *action)
915 {
916 	PurpleConnection *pc = purple_protocol_action_get_connection(action);
917 	HangoutsAccount *ha = purple_connection_get_protocol_data(pc);
918 
919 	purple_request_input(pc, _("Search for friends..."),
920 					   _("Search for friends..."),
921 					   NULL,
922 					   NULL, FALSE, FALSE, NULL,
923 					   _("_Search"), G_CALLBACK(hangouts_search_users_text),
924 					   _("_Cancel"), NULL,
925 					   purple_request_cpar_from_connection(pc),
926 					   ha);
927 
928 }
929