libpurple/protocols/zephyr/zephyr_tzc.c

Mon, 22 Aug 2022 21:40:04 -0500

author
Elliott Sales de Andrade <quantum.analyst@gmail.com>
date
Mon, 22 Aug 2022 21:40:04 -0500
branch
gtk4
changeset 41567
517ac516af27
parent 40778
507a61f2194a
permissions
-rw-r--r--

Inline pidgin_make_scrollable

We need to change it for GTK4, and there are few enough that it can be inlined. Eventually, that code might be a `.ui` anyway.

Testing Done:
Compile only.

Reviewed at https://reviews.imfreedom.org/r/1615/

/*
 * Purple - Internet Messaging Library
 * Copyright (C) Pidgin Developers <devel@pidgin.im>
 *
 * Purple is the legal property of its developers, whose names are too numerous
 * to list here.  Please refer to the COPYRIGHT file distributed with this
 * source distribution.
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, see <https://www.gnu.org/licenses/>.
 */

#include <glib/gi18n-lib.h>

#include "zephyr_tzc.h"

#define MAXCHILDREN 20

typedef gssize (*PollableInputStreamReadFunc)(GPollableInputStream *stream, void *bufcur, GError **error);

static gboolean tzc_write(zephyr_account *zephyr, const gchar *format, ...) G_GNUC_PRINTF(2, 3);

static gchar *
tzc_read(zephyr_account *zephyr, PollableInputStreamReadFunc read_func)
{
	GPollableInputStream *stream = G_POLLABLE_INPUT_STREAM(zephyr->tzc_stdout);
	gsize bufsize = 2048;
	gchar *buf = g_new(gchar, bufsize);
	gchar *bufcur = buf;
	gboolean selected = FALSE;

	while (TRUE) {
		GError *error = NULL;
		if (read_func(stream, bufcur, &error) < 0) {
			if (error->code == G_IO_ERROR_WOULD_BLOCK ||
			    error->code == G_IO_ERROR_TIMED_OUT) {
				g_error_free(error);
				break;
			}
			purple_debug_error("zephyr", "couldn't read: %s", error->message);
			purple_connection_error(purple_account_get_connection(zephyr->account), PURPLE_CONNECTION_ERROR_NETWORK_ERROR, "couldn't read");
			g_error_free(error);
			g_free(buf);
			return NULL;
		}
		selected = TRUE;
		bufcur++;
		if ((bufcur - buf) > (bufsize - 1)) {
			if ((buf = g_realloc(buf, bufsize * 2)) == NULL) {
				purple_debug_error("zephyr","Ran out of memory\n");
				exit(-1);
			} else {
				bufcur = buf + bufsize;
				bufsize *= 2;
			}
		}
	}
	*bufcur = '\0';

	if (!selected) {
		g_free(buf);
		buf = NULL;
	}
	return buf;
}

static gboolean
tzc_write(zephyr_account *zephyr, const gchar *format, ...)
{
	va_list args;
	gchar *message;
	GError *error = NULL;
	gboolean success;

	va_start(args, format);
	message = g_strdup_vprintf(format, args);
	va_end(args);

	success = g_output_stream_write_all(zephyr->tzc_stdin, message, strlen(message),
	                                    NULL, NULL, &error);
	if (!success) {
		purple_debug_error("zephyr", "Unable to write a message: %s", error->message);
		g_error_free(error);
	}
	g_free(message);
	return success;
}

/* Munge the outgoing zephyr so that any quotes or backslashes are
   escaped and do not confuse tzc: */
static char *
tzc_escape_msg(const char *message)
{
	gsize msglen;
	char *newmsg;

	if (!message || !*message) {
		return g_strdup("");
	}

	msglen = strlen(message);
	newmsg = g_new0(char, msglen*2 + 1);
	for (gsize pos = 0, pos2 = 0; pos < msglen; pos++, pos2++) {
		if (message[pos] == '\\' || message[pos] == '"') {
			newmsg[pos2] = '\\';
			pos2++;
		}
		newmsg[pos2] = message[pos];
	}

	return newmsg;
}

static char *
tzc_deescape_str(const char *message)
{
	gsize msglen;
	char *newmsg;

	if (!message || !*message) {
		return g_strdup("");
	}

	msglen = strlen(message);
	newmsg = g_new0(char, msglen + 1);
	for (gsize pos = 0, pos2 = 0; pos < msglen; pos++, pos2++) {
		if (message[pos] == '\\') {
			pos++;
		}
		newmsg[pos2] = message[pos];
	}

	return newmsg;
}

static GSubprocess *
get_tzc_process(const zephyr_account *zephyr)
{
	GSubprocess *tzc_process = NULL;
	const gchar *tzc_cmd;
	gchar **tzc_cmd_array = NULL;
	GError *error = NULL;
	gboolean found_ps = FALSE;
	gint i;

	/* tzc_command should really be of the form
	   path/to/tzc -e %s
	   or
	   ssh username@hostname pathtotzc -e %s
	   -- this should not require a password, and ideally should be
	   kerberized ssh --
	   or
	   fsh username@hostname pathtotzc -e %s
	*/
	tzc_cmd = purple_account_get_string(zephyr->account, "tzc_command", "/usr/bin/tzc -e %s");
	if (!g_shell_parse_argv(tzc_cmd, NULL, &tzc_cmd_array, &error)) {
		purple_debug_error("zephyr", "Unable to parse tzc_command: %s", error->message);
		purple_connection_error(
		        purple_account_get_connection(zephyr->account),
		        PURPLE_CONNECTION_ERROR_INVALID_SETTINGS,
		        "invalid tzc_command setting");
		g_error_free(error);
		return NULL;
	}
	for (i = 0; tzc_cmd_array[i] != NULL; i++) {
		if (purple_strequal(tzc_cmd_array[i], "%s")) {
			g_free(tzc_cmd_array[i]);
			tzc_cmd_array[i] = g_strdup(zephyr->exposure);
			found_ps = TRUE;
		}
	}

	if (!found_ps) {
		purple_debug_error("zephyr", "tzc exited early");
		purple_connection_error(
		        purple_account_get_connection(zephyr->account),
		        PURPLE_CONNECTION_ERROR_NETWORK_ERROR,
		        "invalid output by tzc (or bad parsing code)");
		g_strfreev(tzc_cmd_array);
		return NULL;
	}

	tzc_process = g_subprocess_newv(
	        (const gchar *const *)tzc_cmd_array,
	        G_SUBPROCESS_FLAGS_STDIN_PIPE | G_SUBPROCESS_FLAGS_STDOUT_PIPE,
	        &error);
	if (tzc_process == NULL) {
		purple_debug_error("zephyr", "tzc exited early: %s", error->message);
		purple_connection_error(
		        purple_account_get_connection(zephyr->account),
		        PURPLE_CONNECTION_ERROR_NETWORK_ERROR,
		        "invalid output by tzc (or bad parsing code)");
		g_error_free(error);
	}

	g_strfreev(tzc_cmd_array);
	return tzc_process;
}

static gssize
pollable_input_stream_read(GPollableInputStream *stream, void *bufcur, GError **error)
{
	return g_pollable_input_stream_read_nonblocking(stream, bufcur, 1, NULL, error);
}

static gssize
pollable_input_stream_read_with_timeout(GPollableInputStream *stream,
                                        void *bufcur, GError **error)
{
	const gint64 timeout = 10 * G_USEC_PER_SEC;
	gint64 now = g_get_monotonic_time();

	while (g_get_monotonic_time() < now + timeout) {
		GError *local_error = NULL;
		gssize ret = g_pollable_input_stream_read_nonblocking(
		        stream, bufcur, 1, NULL, &local_error);
		if (ret == 1) {
			return ret;
		}
		if (local_error->code != G_IO_ERROR_WOULD_BLOCK) {
			g_propagate_error(error, local_error);
			return ret;
		}
		/* Keep on waiting if this is a blocking error. */
		g_clear_error(&local_error);
	}

	g_set_error_literal(error, G_IO_ERROR, G_IO_ERROR_TIMED_OUT,
	                    "tzc did not respond in time");
	return -1;
}

static gint
get_paren_level(gint paren_level, gchar ch)
{
	switch (ch) {
		case '(': return paren_level + 1;
		case ')': return paren_level - 1;
		default: return paren_level;
	}
}

static void
parse_tzc_login_data(zephyr_account *zephyr, const gchar *buf, gint buflen)
{
	gchar *str = g_strndup(buf, buflen);

	purple_debug_info("zephyr", "tempstr parsed");

	/* str should now be a string containing all characters from
	 * buf after the first ( to the one before the last paren ).
	 * We should have the following possible lisp strings but we don't care
	 * (tzcspew . start) (version . "something") (pid . number)
	 * We care about 'zephyrid . "username@REALM.NAME"' and
	 * 'exposure . "SOMETHING"' */
	if (!g_ascii_strncasecmp(str, "zephyrid", 8)) {
		gchar **strv;
		gchar *username;
		const char *at;

		purple_debug_info("zephyr", "zephyrid found");

		strv = g_strsplit(str + 8, "\"", -1);
		username = strv[1] ? strv[1] : "";
		zephyr->username = g_strdup(username);

		at = strchr(username, '@');
		if (at != NULL) {
			zephyr->realm = g_strdup(at + 1);
		} else {
			zephyr->realm = get_zephyr_realm(zephyr->account, "local-realm");
		}

		g_strfreev(strv);
	} else {
		purple_debug_info("zephyr", "something that's not zephyr id found %s", str);
	}

	/* We don't care about anything else yet */
	g_free(str);
}

static gchar *
tree_child_contents(GNode *tree, int index)
{
	GNode *child = g_node_nth_child(tree, index);
	return child ? child->data : "";
}

static GNode *
find_node(GNode *ptree, gchar *key)
{
	guint num_children;
	gchar* tc;

	if (!ptree || ! key)
		return NULL;

	num_children = g_node_n_children(ptree);
	tc = tree_child_contents(ptree, 0);

	/* g_strcasecmp() is deprecated.  What is the encoding here??? */
	if (num_children > 0 && tc && !g_ascii_strcasecmp(tc, key)) {
		return ptree;
	} else {
		GNode *result = NULL;
		guint i;
		for (i = 0; i < num_children; i++) {
			result = find_node(g_node_nth_child(ptree, i), key);
			if(result != NULL) {
				break;
			}
		}
		return result;
	}
}

static GNode *
parse_buffer(const gchar *source, gboolean do_parse)
{
	GNode *ptree = g_node_new(NULL);

	if (do_parse) {
		unsigned int p = 0;
		while(p < strlen(source)) {
			unsigned int end;
			gchar *newstr;

			/* Eat white space: */
			if(g_ascii_isspace(source[p]) || source[p] == '\001') {
				p++;
				continue;
			}

			/* Skip comments */
			if(source[p] == ';') {
				while(source[p] != '\n' && p < strlen(source)) {
					p++;
				}
				continue;
			}

			if(source[p] == '(') {
				int nesting = 0;
				gboolean in_quote = FALSE;
				gboolean escape_next = FALSE;
				p++;
				end = p;
				while(!(source[end] == ')' && nesting == 0 && !in_quote) && end < strlen(source)) {
					if(!escape_next) {
						if(source[end] == '\\') {
							escape_next = TRUE;
						}
						if(!in_quote) {
							nesting = get_paren_level(nesting, source[end]);
						}
						if(source[end] == '"') {
							in_quote = !in_quote;
						}
					} else {
						escape_next = FALSE;
					}
					end++;
				}
				do_parse = TRUE;

			} else {
				gchar end_char;
				if(source[p] == '"') {
					end_char = '"';
					p++;
				} else {
					end_char = ' ';
				}
				do_parse = FALSE;

				end = p;
				while(source[end] != end_char && end < strlen(source)) {
					if(source[end] == '\\')
						end++;
					end++;
				}
			}
			newstr = g_new0(gchar, end+1-p);
			strncpy(newstr,source+p,end-p);
			if (g_node_n_children(ptree) < MAXCHILDREN) {
				/* In case we surpass maxchildren, ignore this */
				g_node_append(ptree, parse_buffer(newstr, do_parse));
			} else {
				purple_debug_error("zephyr","too many children in tzc output. skipping\n");
			}
			g_free(newstr);
			p = end + 1;
		}
	} else {
		/* XXX does this have to be strdup'd */
		ptree->data = g_strdup(source);
	}

	return ptree;
}

gboolean
tzc_login(zephyr_account *zephyr)
{
	gchar *buf = NULL;
	const gchar *bufend = NULL;
	const gchar *ptr;
	const gchar *tmp;
	gint parenlevel = 0;

	zephyr->tzc_proc = get_tzc_process(zephyr);
	if (zephyr->tzc_proc == NULL) {
		return FALSE;
	}
	zephyr->tzc_stdin = g_subprocess_get_stdin_pipe(zephyr->tzc_proc);
	zephyr->tzc_stdout = g_subprocess_get_stdout_pipe(zephyr->tzc_proc);

	purple_debug_info("zephyr", "about to read from tzc");
	buf = tzc_read(zephyr, pollable_input_stream_read_with_timeout);
	if (buf == NULL) {
		return FALSE;
	}
	bufend = buf + strlen(buf);
	ptr = buf;

	/* ignore all tzcoutput till we've received the first ( */
	while (ptr < bufend && (*ptr != '(')) {
		ptr++;
	}
	if (ptr >= bufend) {
		purple_connection_error(
		        purple_account_get_connection(zephyr->account),
		        PURPLE_CONNECTION_ERROR_NETWORK_ERROR,
		        "invalid output by tzc (or bad parsing code)");
		g_free(buf);
		return FALSE;
	}

	do {
		parenlevel = get_paren_level(parenlevel, *ptr);
		purple_debug_info("zephyr", "tzc parenlevel is %d", parenlevel);
		switch (parenlevel) {
			case 1:
				/* Search for next beginning (, or for the ending */
				do {
					ptr++;
				} while ((ptr < bufend) && (*ptr != '(') && (*ptr != ')'));
				if (ptr >= bufend) {
					purple_debug_error("zephyr", "tzc parsing error");
				}
				break;
			case 2:
				/* You are probably at
				   (foo . bar ) or (foo . "bar") or (foo . chars) or (foo . numbers) or (foo . () )
				   Parse all the data between the first and last f, and move past )
				*/
				tmp = ptr;
				do {
					ptr++;
					parenlevel = get_paren_level(parenlevel, *ptr);
				} while (parenlevel > 1);
				parse_tzc_login_data(zephyr, tmp + 1, ptr - tmp);
				ptr++;
				break;
			default:
				purple_debug_info("zephyr", "parenlevel is not 1 or 2");
				/* This shouldn't be happening */
				break;
		}
	} while (ptr < bufend && parenlevel != 0);
	purple_debug_info("zephyr", "tzc startup done");
	g_free(buf);

	return TRUE;
}

gint
tzc_check_notify(gpointer data)
{
	PurpleConnection *gc = (PurpleConnection *)data;
	zephyr_account* zephyr = purple_connection_get_protocol_data(gc);
	GNode *newparsetree = NULL;
	gchar *buf = tzc_read(zephyr, pollable_input_stream_read);

	if (buf != NULL) {
		newparsetree = parse_buffer(buf, TRUE);
		g_free(buf);
	}

	if (newparsetree != NULL) {
		gchar *spewtype;
		if ( (spewtype =  tree_child_contents(find_node(newparsetree, "tzcspew"), 2)) ) {
			if (!g_ascii_strncasecmp(spewtype,"message",7)) {
				ZNotice_t notice;
				GNode *msgnode = g_node_nth_child(find_node(newparsetree, "message"), 2);
				/*char *zsig = g_strdup(" ");*/ /* purple doesn't care about zsigs */
				char *msg  = tzc_deescape_str(tree_child_contents(msgnode, 1));
				char *buf = g_strdup_printf(" %c%s", '\0', msg);
				memset((char *)&notice, 0, sizeof(notice));
				notice.z_kind = ACKED;
				notice.z_port = 0;
				notice.z_opcode = tree_child_contents(find_node(newparsetree, "opcode"), 2);
				notice.z_class = tzc_deescape_str(tree_child_contents(find_node(newparsetree, "class"), 2));
				notice.z_class_inst = tree_child_contents(find_node(newparsetree, "instance"), 2);
				notice.z_recipient = zephyr_normalize_local_realm(zephyr, tree_child_contents(find_node(newparsetree, "recipient"), 2));
				notice.z_sender = zephyr_normalize_local_realm(zephyr, tree_child_contents(find_node(newparsetree, "sender"), 2));
				notice.z_default_format = "Class $class, Instance $instance:\n" "To: @bold($recipient) at $time $date\n" "From: @bold($1) <$sender>\n\n$2";
				notice.z_message_len = 1 + 1 + strlen(msg) + 1;
				notice.z_message = buf;
				handle_message(gc, &notice);
				g_free(msg);
				/*g_free(zsig);*/
				g_free(buf);
			}
			else if (!g_ascii_strncasecmp(spewtype,"zlocation",9)) {
				/* check_loc or zephyr_zloc respectively */
				/* XXX fix */
				GNode *locations = g_node_nth_child(g_node_nth_child(find_node(newparsetree, "locations"), 2), 0);
				ZLocations_t zloc = {
				        .host = tree_child_contents(g_node_nth_child(locations, 0), 2),
				        .time = tree_child_contents(g_node_nth_child(locations, 2), 2),
				        .tty = NULL
				};

				handle_locations(gc, tree_child_contents(find_node(newparsetree, "user"), 2),
				                 (zloc.host && *zloc.host && !purple_strequal(zloc.host, " ")) ? 1 : 0,
				                 &zloc);
			}
			else if (!g_ascii_strncasecmp(spewtype,"subscribed",10)) {
			}
			else if (!g_ascii_strncasecmp(spewtype,"start",5)) {
			}
			else if (!g_ascii_strncasecmp(spewtype,"error",5)) {
				/* XXX handle */
			}
		} else {
		}
	} else {
	}

	g_node_destroy(newparsetree);
	return TRUE;
}

gboolean
tzc_subscribe_to(zephyr_account *zephyr, ZSubscription_t *sub)
{
	/* ((tzcfodder . subscribe) ("class" "instance" "recipient")) */
	return tzc_write(zephyr, "((tzcfodder . subscribe) (\"%s\" \"%s\" \"%s\"))\n",
	                 sub->zsub_class, sub->zsub_classinst, sub->zsub_recipient);
}

gboolean
tzc_request_locations(zephyr_account *zephyr, gchar *who)
{
	return tzc_write(zephyr, "((tzcfodder . zlocate) \"%s\")\n", who);
}

gboolean
tzc_send_message(zephyr_account *zephyr, gchar *zclass, gchar *instance, gchar *recipient,
                 const gchar *html_buf, const gchar *sig, G_GNUC_UNUSED const gchar *opcode)
{
	/* CMU cclub tzc doesn't grok opcodes for now  */
	char *tzc_sig = tzc_escape_msg(sig);
	char *tzc_body = tzc_escape_msg(html_buf);
	gboolean result;

	result = tzc_write(zephyr, "((tzcfodder . send) (class . \"%s\") (auth . t) (recipients (\"%s\" . \"%s\")) (message . (\"%s\" \"%s\"))	) \n",
	                   zclass, instance, recipient, tzc_sig, tzc_body);
	g_free(tzc_sig);
	g_free(tzc_body);
	return result;
}

void
tzc_set_location(zephyr_account *zephyr, char *exposure)
{
	tzc_write(zephyr, "((tzcfodder . set-location) (hostname . \"%s\") (exposure . \"%s\"))\n",
	          zephyr->ourhost, exposure);
}

void
tzc_get_subs_from_server(G_GNUC_UNUSED zephyr_account *zephyr, PurpleConnection *gc)
{
	/* XXX fix */
	purple_notify_error(gc, "", "tzc doesn't support this action",
	        NULL, purple_request_cpar_from_connection(gc));
}

void
tzc_close(zephyr_account *zephyr)
{
#ifdef G_OS_UNIX
	GError *error = NULL;
	g_subprocess_send_signal(zephyr->tzc_proc, SIGTERM);
	if (!g_subprocess_wait(zephyr->tzc_proc, NULL, &error)) {
		purple_debug_error("zephyr",
		                   "error while attempting to close tzc: %s",
		                   error->message);
		g_error_free(error);
	}
#else
	g_subprocess_force_exit(zephyr->tzc_proc);
#endif
	zephyr->tzc_stdin = NULL;
	zephyr->tzc_stdout = NULL;
	g_clear_object(&zephyr->tzc_proc);
}

mercurial