diff -r b7ee9469eccc -r e87c321f05fa libpurple/protocols/zephyr/zephyr_tzc.c --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libpurple/protocols/zephyr/zephyr_tzc.c Wed Feb 03 18:27:42 2021 -0600 @@ -0,0 +1,657 @@ +/* + * Purple - Internet Messaging Library + * Copyright (C) Pidgin Developers + * + * 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 . + */ + +#include + +#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) { + if(source[end] == '(') { + nesting++; + } + if(source[end] == ')') { + nesting--; + } + } + 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 *)¬ice, 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, ¬ice); + 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 */ + char *user; + PurpleBuddy *b; + const char *bname; + const gchar *name; + gboolean has_locations; + GNode *locations; + gchar *locval; + + user = tree_child_contents(find_node(newparsetree, "user"), 2); + b = find_buddy(zephyr, user); + bname = b ? purple_buddy_get_name(b) : NULL; + name = b ? bname : user; + + locations = g_node_nth_child(g_node_nth_child(find_node(newparsetree, "locations"), 2), 0); + locval = tree_child_contents(g_node_nth_child(locations, 0), 2); + has_locations = (locval && *locval && !purple_strequal(locval, " ")); + if ((b && pending_zloc(zephyr, bname)) || pending_zloc(zephyr, user)) { + PurpleNotifyUserInfo *user_info = purple_notify_user_info_new(); + const char *balias; + + /* TODO: Check whether it's correct to call add_pair_html, + or if we should be using add_pair_plaintext */ + purple_notify_user_info_add_pair_html(user_info, _("User"), name); + + balias = b ? purple_buddy_get_local_alias(b) : NULL; + if (balias) + purple_notify_user_info_add_pair_plaintext(user_info, _("Alias"), balias); + + if (!has_locations) { + purple_notify_user_info_add_pair_plaintext(user_info, NULL, _("Hidden or not logged-in")); + } else { + /* TODO: Need to escape the two strings that make up tmp? */ + char *tmp = g_strdup_printf(_("
At %s since %s"), locval, + tree_child_contents(g_node_nth_child(locations, 2), 2)); + purple_notify_user_info_add_pair_html(user_info, _("Location"), tmp); + g_free(tmp); + } + + purple_notify_userinfo(gc, name, user_info, NULL, NULL); + purple_notify_user_info_destroy(user_info); + } else { + purple_protocol_got_user_status(zephyr->account, name, has_locations ? "available" : "offline", NULL); + } + } + 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); +}