/* -- Tiny XMPP Client -- txmppc reads commands from stdin and prints to stdout. It is a KISS client for command line or automation. Out of scope: roster -> there is a presence list -> no name (alias), group or subscription state MUC management -> basic works: join (with password), write and leave -> no administration, invitation, register, disco, ... no disco, bookmark, pubsub, vcard, files, voice, ... OMEMO - encryption is good, but too complicated for a tiny client: There is one lib ( https://github.com/gkdr/libomemo ) and profanity has a good implementation, but the code of omemo is 5x larger than txmppc. -> profanity (tui - OMEMO, OTR and PGP and plugins) -> https://profanity-im.github.io/ (c uses libstrophe) -> poezio (tui - OMEMO and OTR) -> https://poez.io/ (python uses slixmpp lib) -> mcabber (tui - No OMEMO, but OTR and PGP, external scripts + fifo) -> https://mcabber.com/ (c uses lib loudmouth -> https://github.com/mcabber/loudmouth ) -> xmppc (cli, which can send messages (chat + PGP), monitor and query some data) -> https://codeberg.org/Anoxinon_e.V./xmppc (c uses libstrophe) -> smplxmpp -> https://codeberg.org/tropf/smplxmpp (c++ uses gloox lib) -> freetalk -> https://www.gnu.org/software/freetalk/ (c uses loudmouth) -> jj (creates filesystem (fifo) structure) -> https://23.fi/jj/ (c uses loudmouth) -> https://wiki.xmpp.org/web/User:MDosch/Sendxmpp_incarnations Build: Depends only on libstophe > gcc txmppc.c -lstrophe -o txmppc Static compile (musl) > gcc txmppc.c -lstrophe -lc -lssl -lcrypto -lexpat -static -o txmppc_static (depending on libstrophe, it can be xml2 instead of expat and/or gnutls instead of openssl) optional: > strip txmppc_static Use: [echo "command jid message" |] txmppc[_static] <jid> <pass> [[=]<server>[(:|=)<port>]] - jid and password are mandatory - optional is a third parameter with server (and :port or =port -> ignores if tls startup (self-signed cert) fails) - if arg "-" is provided the first line read from stdin need to be a list of NULL separated args (JID\0PASS[\0SERVER_PORT]\0\n) -> security feature password will not show up in ps / proc listing txmppc logs incomming messages to stdout and reads commands from stdin - just start as a slim client: > txmppc JID PASS - Sending single message: > echo "[mM] jid message" | txmppc -> txmppc executes the command and exits (on close of stdin) - Using fifo for commands: > tail -f fifo | txmppc -> executes the commands from fifo and prints to stdout > echo "q" >> fifo # ! tail will still wait - so it doesn't exit -> send another line -> Hint: Instead of tail, use txmppc < fifo (will block until first write) and echo "D" > fifo (will disable exit on EOF) - tmux horizontal split + rlwrap client example: (mkfifo fifo; echo "contact@example.com" >> jid_file) 90% pane> txmppc JID PASS < fifo | while read line; do printf '%s: %s\n' "$(date "+%Y-%m-%d %H:%M")" "${line}"; done 10% pane> rlwrap -s -10 -H /dev/null -b " " -f jid_file awk 'BEGIN {print("i");}; {print}; /^q$/ {exit}' >> fifo -> readline editing with 10 lines history (not saved) and jid completion from fid_file, output will have a date prepended -> BEGIN {print("i");} will activate stdin echo feedback - reading from fifo will block until first write -> Hint: In that case the fifo can't be used for other processes, because sending an EOF will end the client (or use "D" command). Commands: j[ muc [password]] -> join multi user conference (without muc "join" presence status changes - default on) J muc [password] -> join multi user conference - no history l[ muc] -> leave multi user conference (without muc "leave" presence status changes) [m ]jid[ message] -> send (chat) message to jid (without message enter multi line mode) h jid message -> send headline message to jid n jid message -> send normal message to jid M muc_jid message -> send (groupchat) message to multi user conference (if not in the muc, it will be joined and left) .[ message] -> send (chat|headline|normal|groupchat) message to last jid (without message enter multi line mode) p[ [priority] presence [status]] -> priority: -128 / 127, presence: on[line]|of[fline]|aw[ay]|ch[at]|dn[d]|xa -> without arg: show presence list; without priority: change presence only P[ jid [priority] presence [status]] -> send presence to jid, extra presence: subscribe|subscribed|unsubscribe|unsubscribed -> without arg show presence list including unavailable/offline q -> end/exit r[ stanza] -> send raw stanza (without stanza enter multi line mode) -> invalid data may result in immediate stream termination by the XMPP server R[ key] -> show raw (message, iq.result) stanza, if it contains key - disabled without key -> "<" (will match every stanza), "iq", "message", "carbons", "openpgp", "juliet@example.org", ... (to see every stanza compile with -DDEBUG, which will enable libstrophe XMPP_LEVEL_DEBUG) Extra commands: i -> stdin echo feedback I -> stdin no feedback (default) d -> exit on EOF (default) D -> ignored EOF (using multiple sender at the same time and mutliline mode might mess up commands and messages) Output: > -> stdin echo feedback i -> info I -> Info offline, lost connection p -> presence (join/leave status changes with j/l command) P -> presence list m -> message chat ("m from: message", if carbon from self "m -> to: message") n -> message normal h -> message headline M -> message muc H -> message muc history r -> raw message stanza e -> error W -> Warn - command error E -> (stderr) startup error -> will exit End-to-end encryption: - It is possible to send PGP encrypted messages (sending a raw stanza): > BASE64_OPENPGP_MESSAGE=$(echo "message" | gpg | base64)" > echo "r <message to='juliet@example.org'><openpgp xmlns='urn:xmpp:openpgp:0'>${BASE64_OPENPGP_MESSAGE}</openpgp></message>" | txmppc (might be supported by other clients: https://xmpp.org/extensions/xep-0373.html -> Be aware that most clients have implemented the obsolete: https://xmpp.org/extensions/xep-0027.html ) -> For receiving raw message stanza filtering with "R openpgp" ("R encrypted") is an option. - A non XMPP conformant way, if both clients use txmppc can be scripted with any encryption. Example with age (don't use password protected key): sender: > ENCRYPTED_MESSAGE="$(printf 'message' | age -r age1urredcd3g8dfachch0d6gzt5katx3tsfesz35zc6jn3fl23jrfmq027uu9 | base64 -w0)" > printf 'm juliet@example.org MYCRYPT:%s' "${ENCRYPTED_MESSAGE}" | txmppc receiver: > txmppc | while read line; do ENCRYPTED_MESSAGE="${line#*MYCRYPT:}" if [ "${line}" = "${ENCRYPTED_MESSAGE}" ]; then printf '%s\n' "${line}" else MESSAGE="$(printf '%s' "${ENCRYPTED_MESSAGE}" | base64 -d | age -d -i juliet.key)" printf '%s%s\n' "${line%MYCRYPT:*}" "${MESSAGE}" fi done License: https://holmeinbuch.de/repo/txmppc/ - matthias@ Copyright (c) ISC License Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. */ #define _XOPEN_SOURCE 700 #include <ctype.h> #include <errno.h> #include <fcntl.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <signal.h> #include <time.h> #include <unistd.h> // https://www.rfc-editor.org/rfc/rfc6120 - core // https://www.rfc-editor.org/rfc/rfc6121 - IM + presence #include <strophe.h> static unsigned short ignore_cert_fail = 0; // signal-safety.7 volatile sig_atomic_t reconnect = 1; // https://www.rfc-editor.org/rfc/rfc6120 // -> 4.6.4 Use of Checking Methods: RECOMMENDED not more than once every 5 minutes #define PING_INTERVAL 60 static char ping_id[32]; #define BUFIN_MAX 4096 // https://www.rfc-editor.org/rfc/rfc6121 // -> don't mess with priority, negative might get no messages static short conn_priority = 0; static unsigned short show_presence_status_change = 1; #define PRESENCE_MAX 5 #define PRESENCE_JID_MAX 9 static char presence_type[10][13] = {"offline", "online", "away", "chat", "dnd", "xa", "subscribe", "subscribed", "unsubscribe", "unsubscribed"}; #define PRESENCE_OFFLINE 0 #define PRESENCE_ONLINE 1 #define PRESENCE_ONLINE_MUC -1 #define PRESENCE_ONLINE_MUC_NO_HISTORY -101 #define PRESENCE_OFFLINE_MUC -100 // https://www.rfc-editor.org/rfc/rfc7622 - address (jid) #define JID_MAX 3071 + 1 static char last_sent_type_jid[1 + JID_MAX]; struct list_node { char jid[JID_MAX]; short presence; short priority; struct list_node *next; }; static struct list_node *jid_list = NULL; #define JID_BARE_MAX 2047 + 1 static char jid_bare[JID_BARE_MAX]; // https://xmpp.org/extensions/xep-0172.html - nick -> nickname length not defined // https://xmpp.org/extensions/xep-0045.html - muc -> nickname is resource part of jid #define NICK_MAX 1023 + 1 static char nick[NICK_MAX]; #define MULTI_LINE_OFF 0 #define MULTI_LINE_ON 1 #define MULTI_LINE_MUC 2 #define MULTI_LINE_RAW 3 static short multi_line = MULTI_LINE_OFF; #define SHOW_RAW_STANZA_MAX 32 static char show_raw_stanza[SHOW_RAW_STANZA_MAX]; static unsigned short stdin_echo_feedback = 0; static unsigned short exit_on_eof = 1; unsigned short get_start_pos(const char *buf, unsigned short start_pos) { unsigned short buf_len = (unsigned short)strlen(&buf[start_pos]) + start_pos; for (unsigned short i = start_pos; i < buf_len; i++) { if (!isspace(buf[i])) { return i; } } return buf_len; } void update_jid_list(const char *jid, short presence, short priority) { struct list_node *list_before = NULL; struct list_node *list_current = jid_list; struct list_node *list_new = NULL; size_t jid_len = strlen(jid); while (list_current) { int jid_match = strncmp(jid, list_current->jid, jid_len); if (jid_match == 0) { if (presence == PRESENCE_OFFLINE_MUC) { // remove jid, of who leaves the muc from list // if self leaves muc, called with bare jid -> remove every muc entry while (list_current && (strncmp(jid, list_current->jid, jid_len) == 0)) { if (list_before) { list_before->next = list_current->next; free(list_current); list_current = list_before->next; } else { jid_list = list_current->next; free(list_current); list_current = jid_list->next; } } return; } // update jid state for self and subscription (offline too) list_current->presence = presence; list_current->priority = priority; return; } if (jid_match < 0) { // insert new jid here break; } list_before = list_current; list_current = list_current->next; } list_new = (struct list_node*) malloc(sizeof(struct list_node)); if (list_new == NULL) { fprintf(stderr, "alloc failed\n"); fflush(stderr); exit(1); } memccpy(list_new->jid, jid, '\0', JID_MAX - 1); list_new->presence = presence; list_new->priority = priority; if (list_before) { list_before->next = list_new; } else { jid_list = list_new; } list_new->next = list_current; } void send_presence(xmpp_conn_t *conn, short presence_index, const char *status_text, const char *jid, const char *priority_text) { xmpp_ctx_t *ctx = xmpp_conn_get_context(conn); xmpp_stanza_t *presence_stanza = xmpp_presence_new(ctx); if (jid) { xmpp_stanza_set_to(presence_stanza, jid); } if (presence_index == PRESENCE_OFFLINE) { xmpp_stanza_set_type(presence_stanza, "unavailable"); } else if (PRESENCE_MAX < presence_index) { xmpp_stanza_set_type(presence_stanza, presence_type[presence_index]); // inverse presence for muc } else if (presence_index < 0) { // MUC special xmpp_stanza_t *extension_stanza; unsigned short jid_len = (unsigned short)strlen(jid); unsigned short i = 0; for (;;) { // muc_jid is only muc, no nick if (i == jid_len) { char muc_jid_nick[JID_MAX]; if (JID_MAX - 1 - NICK_MAX < strlen(jid)) { fprintf(stdout, "W invalid muc jid\n"); fflush(stdout); xmpp_stanza_release(presence_stanza); return; } sprintf(muc_jid_nick, "%s/%s", jid, nick); xmpp_stanza_set_to(presence_stanza, muc_jid_nick); break; } // muc_jid contains nick if (jid[i++] == '/') { break; } } extension_stanza = xmpp_stanza_new(ctx); xmpp_stanza_set_name(extension_stanza, "x"); xmpp_stanza_set_ns(extension_stanza, "http://jabber.org/protocol/muc"); if (presence_index == PRESENCE_ONLINE_MUC_NO_HISTORY) { xmpp_stanza_t *history_stanza; history_stanza = xmpp_stanza_new(ctx); xmpp_stanza_set_name(history_stanza, "history"); xmpp_stanza_set_attribute(history_stanza, "maxstanzas", "0"); xmpp_stanza_add_child_ex(extension_stanza, history_stanza, 0); } if (status_text) { xmpp_stanza_t *password_stanza, *value_stanza; password_stanza = xmpp_stanza_new(ctx); xmpp_stanza_set_name(password_stanza, "password"); value_stanza = xmpp_stanza_new(ctx); xmpp_stanza_set_text(value_stanza, status_text); xmpp_stanza_add_child_ex(password_stanza, value_stanza, 0); xmpp_stanza_add_child_ex(extension_stanza, password_stanza, 0); } xmpp_stanza_add_child_ex(presence_stanza, extension_stanza, 0); } else { if (1 < presence_index) { xmpp_stanza_t *show_stanza, *value_stanza; show_stanza = xmpp_stanza_new(ctx); xmpp_stanza_set_name(show_stanza, "show"); value_stanza = xmpp_stanza_new(ctx); xmpp_stanza_set_text(value_stanza, presence_type[presence_index]); xmpp_stanza_add_child_ex(show_stanza, value_stanza, 0); xmpp_stanza_add_child_ex(presence_stanza, show_stanza, 0); } if (priority_text || (conn_priority && -128 < conn_priority && conn_priority < 127)) { xmpp_stanza_t *priority_stanza, *value_stanza; priority_stanza = xmpp_stanza_new(ctx); xmpp_stanza_set_name(priority_stanza, "priority"); value_stanza = xmpp_stanza_new(ctx); if (!priority_text) { char conn_priority_text[5]; sprintf(conn_priority_text, "%d", conn_priority); xmpp_stanza_set_text(value_stanza, conn_priority_text); } else { xmpp_stanza_set_text(value_stanza, priority_text); } xmpp_stanza_add_child_ex(priority_stanza, value_stanza, 0); xmpp_stanza_add_child_ex(presence_stanza, priority_stanza, 0); } if (status_text) { xmpp_stanza_t *status_stanza, *value_stanza; status_stanza = xmpp_stanza_new(ctx); xmpp_stanza_set_name(status_stanza, "status"); value_stanza = xmpp_stanza_new(ctx); xmpp_stanza_set_text(value_stanza, status_text); xmpp_stanza_add_child_ex(status_stanza, value_stanza, 0); xmpp_stanza_add_child_ex(presence_stanza, status_stanza, 0); } } xmpp_send(conn, presence_stanza); xmpp_stanza_release(presence_stanza); } void disconnect(xmpp_conn_t *conn) { if (reconnect == 1) { reconnect = 0; } send_presence(conn, PRESENCE_OFFLINE, NULL, NULL, NULL); xmpp_disconnect(conn); } void send_message(xmpp_conn_t *conn, const char *jid, const char command, const char *text) { xmpp_ctx_t *ctx = xmpp_conn_get_context(conn); xmpp_stanza_t *message_stanza; xmpp_stanza_t *extension_stanza = NULL; const char *type = "chat"; switch (command) { case 'x': // x: m -> jid in muc extension_stanza = xmpp_stanza_new(ctx); break; case 'M': type = "groupchat"; break; case 'h': type = "headline"; break; case 'N': // N: n -> jid in muc extension_stanza = xmpp_stanza_new(ctx); // fall through case 'n': type = "normal"; break; } message_stanza = xmpp_message_new(ctx, type, jid, NULL); xmpp_message_set_body(message_stanza, text); if (extension_stanza) { xmpp_stanza_set_name(extension_stanza, "x"); xmpp_stanza_set_ns(extension_stanza, "http://jabber.org/protocol/muc#user"); xmpp_stanza_add_child_ex(message_stanza, extension_stanza, 0); } xmpp_send(conn, message_stanza); xmpp_stanza_release(message_stanza); } short muc_in_jid_list(const char *jid) { struct list_node *list_current = jid_list; unsigned short jid_len = (unsigned short)strlen(jid); while (list_current) { // inverse presence for muc jid if (list_current->presence < 0 && strncmp(jid, list_current->jid, jid_len) == 0) { return 1; } list_current = list_current->next; } return 0; } int input_handler(xmpp_conn_t *conn, void *userdata) { char bufin[BUFIN_MAX]; unsigned short index, bufin_len; // starting with a jid -> default (chat) [m]essage to jid char command = 'm'; char *jid = NULL; if (reconnect < 0) { // signal received disconnect(conn); return 1; } if (!fgets(bufin, BUFIN_MAX, stdin)) { if (exit_on_eof == 0 && feof(stdin)) { clearerr(stdin); } else if (feof(stdin) || errno != EAGAIN) { disconnect(conn); } return 1; } bufin_len = (unsigned short)strlen(bufin); if (bufin_len == BUFIN_MAX - 1 && bufin[BUFIN_MAX - 2] != '\n') { // read/remove everything from input while (fgetc(stdin) != '\n'); fprintf(stdout, "W too much input\n"); fflush(stdout); return 1; } while (0 < bufin_len && isspace(bufin[bufin_len - 1])) { // trim end of input bufin_len--; } bufin[bufin_len] = 0; if (stdin_echo_feedback && (multi_line || bufin[0] != 0)) { fprintf(stdout, "> %s\n", bufin); fflush(stdout); } if (multi_line) { if (bufin[0] == '.' && bufin[1] == 0) { if (multi_line == MULTI_LINE_MUC) { send_presence(conn, PRESENCE_OFFLINE, NULL, &last_sent_type_jid[1], NULL); } multi_line = MULTI_LINE_OFF; fprintf(stdout, "i end multi line\n"); fflush(stdout); return 1; } if (multi_line == MULTI_LINE_RAW) { xmpp_send_raw_string(conn, "%s", bufin); return 1; } send_message(conn, &last_sent_type_jid[1], last_sent_type_jid[0], bufin); return 1; } if (bufin[0] == 0) { // ignore empty lines return 1; } // trim start index = get_start_pos(bufin, 0); if (bufin[index + 1] == ' ') { // get command (if second char is empty it is no jid) command = bufin[index]; index = get_start_pos(bufin, index + 2); } else if (bufin[index + 1] == 0) { // just single char command struct list_node *list_current; char priority_text[8]; unsigned short show_offline = 0; command = bufin[index]; switch (command) { case '.': if (last_sent_type_jid[0] == 0) { fprintf(stdout, "W no last jid\n"); fflush(stdout); return 1; } multi_line = MULTI_LINE_ON; if (last_sent_type_jid[0] == 'M') { if (!muc_in_jid_list(&last_sent_type_jid[1])) { multi_line = MULTI_LINE_MUC; } } fprintf(stdout, "i multi line - end with \".\"\n"); fflush(stdout); return 1; case 'd': exit_on_eof = 1; return 1; case 'D': exit_on_eof = 0; return 1; case 'i': stdin_echo_feedback = 1; return 1; case 'I': stdin_echo_feedback = 0; return 1; case 'j': show_presence_status_change = 1; return 1; case 'l': show_presence_status_change = 0; return 1; case 'P': show_offline = 1; // fall through case 'p': list_current = jid_list; while (list_current) { if (list_current->priority) { sprintf(priority_text, " (%d)", list_current->priority); } else { priority_text[0] = 0; } if (list_current->presence < 0) { // use inverse presence for muc fprintf(stdout, "P %s (M) - %s%s\n", list_current->jid, presence_type[-list_current->presence], priority_text); } else if (show_offline || list_current->presence != PRESENCE_OFFLINE) { fprintf(stdout, "P %s - %s%s\n", list_current->jid, presence_type[list_current->presence], priority_text); } list_current = list_current->next; } fflush(stdout); return 1; case 'q': disconnect(conn); return 1; case 'r': multi_line = MULTI_LINE_RAW; fprintf(stdout, "i multi line - end with \".\"\n"); fflush(stdout); return 1; case 'R': show_raw_stanza[0] = 0; return 1; } } if (command == 'r') { xmpp_send_raw_string(conn, "%s", &bufin[index]); return 1; } if (command == 'R') { if (memccpy(show_raw_stanza, &bufin[index], '\0', SHOW_RAW_STANZA_MAX - 1) == NULL) { show_raw_stanza[SHOW_RAW_STANZA_MAX - 1] = 0; } return 1; } if (command != '.' && command != 'm' && command != 'M' && command != 'j' && command != 'J' && command != 'l' && command != 'p' && command != 'P' && command != 'n' && command != 'h') { fprintf(stdout, "W invalid command\n"); fflush(stdout); return 1; } if (command != '.' && command != 'p') { // get jid (if not message to last jid or global presence) unsigned short jid_local_and_domain = 0; for (unsigned short i = index; i < bufin_len; i++) { if (bufin[i] == '@') { // simple identification of jid (no real validation) jid_local_and_domain++; } else if (jid_local_and_domain == 1 && bufin[i] == ' ') { bufin[i] = 0; jid = &bufin[index]; index = i + 1; break; } else if (jid_local_and_domain == 1 && bufin[i + 1] == 0) { jid = &bufin[index]; index = i + 1; break; } } if (!jid || JID_MAX < strlen(jid)) { fprintf(stdout, "W invalid jid\n"); fflush(stdout); return 1; } } if (command == 'j' || command == 'J') { // join room const char *password = &bufin[get_start_pos(bufin, index)]; send_presence(conn, command == 'j' ? PRESENCE_ONLINE_MUC : PRESENCE_ONLINE_MUC_NO_HISTORY, password[0] == 0 ? NULL : password, jid, NULL); return 1; } if (command == 'l') { // leave room send_presence(conn, PRESENCE_OFFLINE, NULL, jid, NULL); return 1; } if (command == 'p' || command == 'P') { // sending presence char *presence_text = &bufin[index]; char *status_text = NULL; char *priority_text = NULL; short priority = 0; index = get_start_pos(bufin, index); while (index < bufin_len) { if (bufin[index] != ' ') { index++; continue; } bufin[index] = 0; index = get_start_pos(bufin, index + 1); priority = (short)atoi(presence_text); if (priority == 0 && (presence_text[0] != '0' || presence_text[1] != 0)) { status_text = &bufin[index]; break; } else { priority_text = presence_text; if (command == 'p') { // keep priority (if global presence) conn_priority = priority; } presence_text = &bufin[index]; } } if (presence_text[0] == 0) { fprintf(stdout, "W no presence\n"); fflush(stdout); return 1; } if (strcmp(presence_text, "unavailable") == 0) { // special check for word unavailable instead of offline send_presence(conn, PRESENCE_OFFLINE, status_text, jid, NULL); return 1; } for (short i = 0; i <= PRESENCE_JID_MAX; i++) { if (!jid && PRESENCE_MAX < i) { fprintf(stdout, "W subscription with invalid jid\n"); fflush(stdout); return 1; } if ((i <= PRESENCE_MAX && strncmp(presence_text, presence_type[i], 2) == 0) || strcmp(presence_text, presence_type[i]) == 0) { send_presence(conn, i, status_text, jid, priority_text); return 1; } } fprintf(stdout, "W invalid presence\n"); fflush(stdout); return 1; } if (command == 'm' || command == 'M' || command == 'h' || command == 'n' || command == '.') { // sending message unsigned short muc_leave = 0; if (command == '.') { // sending with last command to last jid -> restore values if (last_sent_type_jid[0] == 0) { fprintf(stdout, "W no last jid\n"); fflush(stdout); return 1; } command = last_sent_type_jid[0]; jid = &last_sent_type_jid[1]; } else { // save values for '.' command next time last_sent_type_jid[0] = command; memccpy(&last_sent_type_jid[1], jid, '\0', JID_MAX - 1); } if (command == 'm' || command == 'n') { if (muc_in_jid_list(jid)) { // marker for sending to jid in muc command = command == 'm' ? 'x' : 'N'; last_sent_type_jid[0] = command; } } else if (command == 'M') { if (!muc_in_jid_list(jid)) { send_presence(conn, PRESENCE_ONLINE_MUC_NO_HISTORY, NULL, jid, NULL); muc_leave = 1; } } if (bufin[index] == 0) { // no message text -> use multi line mode multi_line = muc_leave ? MULTI_LINE_MUC : MULTI_LINE_ON; fprintf(stdout, "i multi line - end with \".\"\n"); fflush(stdout); return 1; } send_message(conn, jid, command, &bufin[index]); if (muc_leave) { send_presence(conn, PRESENCE_OFFLINE, NULL, jid, NULL); } return 1; } // should not get here... fprintf(stdout, "W invalid input\n"); fflush(stdout); return 1; } void print_stanza_error(xmpp_ctx_t *ctx, xmpp_stanza_t *stanza, const char* error_from) { xmpp_stanza_t *error_stanza, *defined_condition_stanza, *text_stanza; const char *error_name = NULL; error_stanza = xmpp_stanza_get_child_by_name(stanza, "error"); if (!error_stanza) { return; } defined_condition_stanza = xmpp_stanza_get_child_by_ns( error_stanza, "urn:ietf:params:xml:ns:xmpp-stanzas"); while (defined_condition_stanza) { // the name is one out of 22 defined conditions, but stanzas in that namespace are only 1 or 2 const char *error_ns = xmpp_stanza_get_ns(defined_condition_stanza); if (error_ns && strcmp(error_ns, "urn:ietf:params:xml:ns:xmpp-stanzas") == 0) { error_name = xmpp_stanza_get_name(defined_condition_stanza); if (error_name && strcmp(error_name, "text") != 0) { break; } } defined_condition_stanza = xmpp_stanza_get_next(defined_condition_stanza); } if (!error_name) { return; } text_stanza = xmpp_stanza_get_child_by_name(error_stanza, "text"); if (text_stanza) { char *error_text = xmpp_stanza_get_text(text_stanza); fprintf(stdout, "e %s -> %s - %s: %s\n", error_from, xmpp_stanza_get_type(error_stanza), error_name, error_text); xmpp_free(ctx, error_text); } else { fprintf(stdout, "e %s -> %s - %s\n", error_from, xmpp_stanza_get_type(error_stanza), error_name); } fflush(stdout); } int presence_handler(xmpp_conn_t *conn, xmpp_stanza_t *presence_stanza, void *userdata) { xmpp_ctx_t *ctx = xmpp_conn_get_context(conn); const char *presence_from = xmpp_stanza_get_from(presence_stanza); xmpp_stanza_t *status_stanza = xmpp_stanza_get_child_by_name(presence_stanza, "status"); const char *type; char *status_text = NULL; if (!presence_from) { presence_from = ""; } if (status_stanza) { status_text = xmpp_stanza_get_text(status_stanza); } type = xmpp_stanza_get_type(presence_stanza); if (type) { if (strcmp(type, "error") == 0) { print_stanza_error(ctx, presence_stanza, presence_from); } else if (strcmp(type, "unavailable") == 0) { xmpp_stanza_t *extension_stanza = xmpp_stanza_get_child_by_name_and_ns( presence_stanza, "x", "http://jabber.org/protocol/muc#user"); if (extension_stanza) { // presence from a muc jid xmpp_stanza_t *x_status_stanza = xmpp_stanza_get_child_by_name(extension_stanza, "status"); unsigned short is_self_jid = 0; while (x_status_stanza) { const char *code_text = xmpp_stanza_get_attribute(x_status_stanza, "code"); if (code_text) { unsigned short code = (unsigned short)atoi(code_text); switch (code) { case 110: // self presence is_self_jid++; break; case 210: // nick assigned or modified // fall through case 303: // new nick is_self_jid--; break; } } x_status_stanza = xmpp_stanza_get_next(x_status_stanza); } if (is_self_jid == 1) { // bare muc jid will remove every room occupant char *muc_jid_bare = xmpp_jid_bare(ctx, presence_from); update_jid_list(muc_jid_bare, PRESENCE_OFFLINE_MUC, 0); xmpp_free(ctx, muc_jid_bare); } else { // other muc jid - only remove that update_jid_list(presence_from, PRESENCE_OFFLINE_MUC, 0); } } else { // no muc jid - change of presence status update_jid_list(presence_from, PRESENCE_OFFLINE, 0); } if (show_presence_status_change) { if (status_text) { fprintf(stdout, "p %s -> offline: %s\n", presence_from, status_text); } else { fprintf(stdout, "p %s -> offline\n", presence_from); } } } else { // "subscribe", "subscribed", "unsubscribe", "unsubscribed"(, "probe" - server only) fprintf(stdout, "p %s -> %s\n", presence_from, type); } } else { char *show_text = NULL; short presence = PRESENCE_ONLINE; short priority = 0; xmpp_stanza_t *priority_stanza, *show_stanza; priority_stanza = xmpp_stanza_get_child_by_name(presence_stanza, "priority"); if (priority_stanza) { char *priority_text = xmpp_stanza_get_text(priority_stanza); if (priority_text) { priority = (short)atoi(priority_text); } xmpp_free(ctx, priority_text); } show_stanza = xmpp_stanza_get_child_by_name(presence_stanza, "show"); if (show_stanza) { show_text = xmpp_stanza_get_text(show_stanza); if (show_text) { // skip 0 offline and 1 online for (short i = 2; i <= PRESENCE_MAX; i++) { if (strncmp(show_text, presence_type[i], 2) == 0) { presence = i; break; } } } } if (xmpp_stanza_get_child_by_name_and_ns(presence_stanza, "x", "http://jabber.org/protocol/muc#user")) { // use inverse presence for muc update_jid_list(presence_from, -presence, priority); } else { update_jid_list(presence_from, presence, priority); } if (show_presence_status_change) { char priority_text[8]; priority_text[0] = 0; if (priority) { sprintf(priority_text, " (%d)", priority); } fprintf(stdout, "p %s -> %s%s", presence_from, show_text ? show_text : "online", priority_text); if (status_text) { fprintf(stdout, ": %s\n", status_text); } else { fprintf(stdout, "\n"); } } if (show_text) { xmpp_free(ctx, show_text); } } if (status_text) { xmpp_free(ctx, status_text); } fflush(stdout); return 1; } void print_raw_stanza(xmpp_ctx_t *ctx, xmpp_stanza_t *stanza) { char *text_stanza; size_t buflen; if (show_raw_stanza[0] == 0) { return; } if (xmpp_stanza_to_text(stanza, &text_stanza, &buflen) == XMPP_EOK) { if (strstr(text_stanza, show_raw_stanza)) { fprintf(stdout, "r %s\n", text_stanza); fflush(stdout); } xmpp_free(ctx, text_stanza); } } int message_handler(xmpp_conn_t *conn, xmpp_stanza_t *message_stanza, void *userdata) { xmpp_ctx_t *ctx = xmpp_conn_get_context(conn); const char *type = xmpp_stanza_get_type(message_stanza); xmpp_stanza_t *carbons_stanza, *body_stanza, *subject_stanza; const char *message_from = NULL; const char *carbons_sent = ""; char indicator = 'm'; char *message_text; print_raw_stanza(ctx, message_stanza); if (!type) { return 1; } // https://xmpp.org/extensions/xep-0280.html carbons_stanza = xmpp_stanza_get_child_by_ns(message_stanza, "urn:xmpp:carbons:2"); if (carbons_stanza) { xmpp_stanza_t *forward_stanza, *forward_message_stanza; const char *carbons_name, *sender_jid; sender_jid = xmpp_stanza_get_from(message_stanza); if (!sender_jid || strcmp(jid_bare, sender_jid) != 0) { // fake sender return 1; } carbons_name = xmpp_stanza_get_name(carbons_stanza); if (carbons_name && strcmp(carbons_name, "sent") == 0) { carbons_sent = "-> "; } else if (!carbons_name || strcmp(carbons_name, "received") != 0) { // invalid carbon type return 1; } forward_stanza = xmpp_stanza_get_child_by_ns(carbons_stanza, "urn:xmpp:forward:0"); if (!forward_stanza) { return 1; } forward_message_stanza = xmpp_stanza_get_child_by_name(forward_stanza, "message"); if (!forward_message_stanza) { return 1; } if (xmpp_stanza_get_child_by_name_and_ns(forward_message_stanza, "private", "urn:xmpp:carbons:2")) { // invalid private -> no carbons return 1; } if (xmpp_stanza_get_child_by_name_and_ns(forward_message_stanza, "x", "http://jabber.org/protocol/muc#user")) { const char *message_to = xmpp_stanza_get_to(forward_message_stanza); if (message_to) { if (!muc_in_jid_list(message_to)) { // invalid message not in muc with this client return 1; } } } message_stanza = forward_message_stanza; } if (carbons_sent[0] == 0) { message_from = xmpp_stanza_get_from(message_stanza); } else { // message_from is self jid message_from = xmpp_stanza_get_to(message_stanza); } if (!message_from) { message_from = ""; } if (strcmp(type, "error") == 0) { print_stanza_error(ctx, message_stanza, message_from); return 1; } if (strcmp(type, "groupchat") == 0) { if (xmpp_stanza_get_child_by_ns(message_stanza, "urn:xmpp:delay")) { indicator = 'H'; } else { indicator = 'M'; } } else if (strcmp(type, "headline") == 0) { indicator = 'h'; } else if (strcmp(type, "normal") == 0) { indicator = 'n'; } subject_stanza = xmpp_stanza_get_child_by_name(message_stanza, "subject"); if (subject_stanza) { message_text = xmpp_stanza_get_text(subject_stanza); if (message_text) { fprintf(stdout, "%c %s%s -> subject: %s\n", indicator, carbons_sent, message_from, message_text); fflush(stdout); } xmpp_free(ctx, message_text); } body_stanza = xmpp_stanza_get_child_by_name(message_stanza, "body"); if (!body_stanza) { return 1; } message_text = xmpp_stanza_get_text(body_stanza); fprintf(stdout, "%c %s%s: %s\n", indicator, carbons_sent, message_from, message_text ? message_text : ""); fflush(stdout); xmpp_free(ctx, message_text); return 1; } // https://xmpp.org/extensions/xep-0199.html - ping int ping_send_handler(xmpp_conn_t *conn, void *userdata) { xmpp_ctx_t *ctx = xmpp_conn_get_context(conn); xmpp_stanza_t *iq_stanza, *ping_stanza; if (ping_id[0]) { // already sent -> lost xmpp_disconnect(conn); return 1; } sprintf(ping_id, "ping_server_%lu", time(NULL)); iq_stanza = xmpp_iq_new(ctx, "get", ping_id); ping_stanza = xmpp_stanza_new(ctx); xmpp_stanza_set_name(ping_stanza, "ping"); xmpp_stanza_set_ns(ping_stanza, "urn:xmpp:ping"); xmpp_stanza_add_child_ex(iq_stanza, ping_stanza, 0); xmpp_send(conn, iq_stanza); xmpp_stanza_release(iq_stanza); return 1; } int iq_handler(xmpp_conn_t *conn, xmpp_stanza_t *iq_stanza, void *userdata) { xmpp_ctx_t *ctx = xmpp_conn_get_context(conn); const char *type = xmpp_stanza_get_type(iq_stanza); const char *id = xmpp_stanza_get_id(iq_stanza); const char *iq_from = xmpp_stanza_get_from(iq_stanza); if (!type || !id) { return 1; } if (!iq_from) { iq_from = ""; } if (strcmp(type, "result") == 0) { if (strcmp(ping_id, id) == 0) { ping_id[0] = 0; return 1; } print_raw_stanza(ctx, iq_stanza); } else if (strcmp(type, "get") == 0) { xmpp_stanza_t *result_stanza; if (xmpp_stanza_get_child_by_ns(iq_stanza, "urn:xmpp:ping")) { result_stanza = xmpp_iq_new(ctx, "result", id); } else { xmpp_stanza_t *error_stanza, *defined_condition_stanza; result_stanza = xmpp_iq_new(ctx, "error", id); error_stanza = xmpp_stanza_new(ctx); xmpp_stanza_set_name(iq_stanza, "error"); xmpp_stanza_set_type(error_stanza, "cancel"); defined_condition_stanza = xmpp_stanza_new(ctx); xmpp_stanza_set_name(defined_condition_stanza, "service-unavailable"); xmpp_stanza_set_ns(defined_condition_stanza, "urn:ietf:params:xml:ns:xmpp-stanzas"); xmpp_stanza_add_child_ex(error_stanza, defined_condition_stanza, 0); xmpp_stanza_add_child_ex(result_stanza, error_stanza, 0); } xmpp_stanza_set_to(result_stanza, iq_from ? iq_from : ""); xmpp_send(conn, result_stanza); xmpp_stanza_release(result_stanza); } else if (strcmp(type, "error") == 0) { if (xmpp_stanza_get_child_by_ns(iq_stanza, "urn:xmpp:ping")) { if (strcmp(ping_id, id) == 0) { xmpp_timed_handler_delete(conn, ping_send_handler); ping_id[0] = 0; return 1; } } print_stanza_error(ctx, iq_stanza, iq_from); } return 1; } int certfail_handler(const xmpp_tlscert_t *cert, const char *const error_message) { if (ignore_cert_fail) { return 1; } fprintf(stderr, "E %s\n", error_message); fflush(stderr); reconnect = -1; return 0; } void conn_handler(xmpp_conn_t *conn, xmpp_conn_event_t status, int error, xmpp_stream_error_t *stream_error, void *userdata) { xmpp_ctx_t *ctx = xmpp_conn_get_context(conn); if (status == XMPP_CONN_CONNECT) { xmpp_stanza_t *carbons_stanza, *enable_stanza; xmpp_handler_add(conn, iq_handler, NULL, "iq", NULL, NULL); xmpp_handler_add(conn, presence_handler, NULL, "presence", NULL, NULL); xmpp_handler_add(conn, message_handler, NULL, "message", NULL, NULL); xmpp_timed_handler_add(conn, input_handler, 750, NULL); xmpp_timed_handler_add(conn, ping_send_handler, PING_INTERVAL * 1000, NULL); send_presence(conn, PRESENCE_ONLINE, NULL, NULL, NULL); carbons_stanza = xmpp_iq_new(ctx, "set", "carbons_enable1"); enable_stanza = xmpp_stanza_new(ctx); xmpp_stanza_set_name(enable_stanza, "enable"); xmpp_stanza_set_ns(enable_stanza, "urn:xmpp:carbons:2"); xmpp_stanza_add_child_ex(carbons_stanza, enable_stanza, 0); xmpp_send(conn, carbons_stanza); xmpp_stanza_release(carbons_stanza); fprintf(stdout, "i online: %s\n", xmpp_conn_get_bound_jid(conn)); fflush(stdout); } else { xmpp_timed_handler_delete(conn, ping_send_handler); xmpp_timed_handler_delete(conn, input_handler); xmpp_handler_delete(conn, message_handler); xmpp_handler_delete(conn, presence_handler); xmpp_handler_delete(conn, iq_handler); xmpp_stop(ctx); } } void signal_handler(int signum) { // inverse signum -> negative reconnect means exit reconnect = (sig_atomic_t)-(signum + 128); } int main(int argc, char **argv) { xmpp_ctx_t *ctx; xmpp_conn_t *conn; int connected; char *jid, *pass; char *arg_null = NULL; char *server = NULL; unsigned short port = 0; long flags = 0; struct sigaction st_sigaction; if (argc == 2 && argv[1][0] == '-') { arg_null = (char*) malloc(BUFIN_MAX); if (!arg_null) { return 1; } fgets(arg_null, BUFIN_MAX, stdin); argv[1] = arg_null; for (unsigned short i = 0; i < BUFIN_MAX; i++) { if (arg_null[i] == '\0') { if (arg_null[i + 1] == '\n') { break; } argv[argc++] = &arg_null[i + 1]; } } } if (argc < 3) { fprintf(stderr, "E Usage: %s <jid> <pass> [[=]<server>[(:|=)<port>]]\n", argv[0]); fflush(stderr); return 1; } jid = argv[1]; if (memccpy(nick, jid, '\0', NICK_MAX - 1) == NULL) { nick[NICK_MAX - 1] = 0; } if (memccpy(jid_bare, jid, '\0', JID_BARE_MAX - 1) == NULL) { jid_bare[JID_BARE_MAX - 1] = 0; } for (unsigned short i = 0; i < JID_BARE_MAX; i++) { if (i < NICK_MAX && nick[i] == '@') { // default nick from jid (if muc is not joined with muc_jid/nick) nick[i] = 0; } else if (jid_bare[i] == '/') { // no resource, if given jid_bare[i] = 0; break; } else if (jid_bare[i] == 0) { break; } } pass = argv[2]; if (4 == argc) { // custom server and port for (unsigned short i = 1; i < strlen(argv[3]); i++) { if (argv[3][i] == ':' || argv[3][i] == '=') { if (argv[3][i] == '=') { ignore_cert_fail = 1; } port = (unsigned short)atoi(&argv[3][i + 1]); argv[3][i] = 0; break; } } if (argv[3][0] == '=') { // TLS flags = XMPP_CONN_FLAG_LEGACY_SSL; server = &argv[3][1]; } else { server = argv[3]; } } last_sent_type_jid[0] = 0; show_raw_stanza[0] = 0; srand((unsigned int)time(NULL)); xmpp_initialize(); #ifdef DEBUG ctx = xmpp_ctx_new(NULL, xmpp_get_default_logger(XMPP_LEVEL_DEBUG)); #else ctx = xmpp_ctx_new(NULL, NULL); #endif if (!ctx) { fprintf(stderr, "E cannot initialize ctx\n"); fflush(stderr); return 1; } conn = xmpp_conn_new(ctx); if (!conn) { fprintf(stderr, "E cannot initialize conn\n"); fflush(stderr); return 1; } xmpp_conn_set_jid(conn, jid); xmpp_conn_set_pass(conn, pass); xmpp_conn_set_certfail_handler(conn, certfail_handler); xmpp_conn_set_flags(conn, flags); memset(&st_sigaction, 0, sizeof(st_sigaction)); st_sigaction.sa_handler = &signal_handler; sigaction(SIGINT, &st_sigaction, NULL); sigaction(SIGTERM, &st_sigaction, NULL); fcntl(0, F_SETFL, O_NONBLOCK); for (;;) { struct list_node *list_current; unsigned short unpredictable; ping_id[0] = 0; connected = xmpp_connect_client(conn, server, port, conn_handler, NULL); if (connected != XMPP_EOK) { fprintf(stderr, "E connection failed\n"); fflush(stderr); reconnect = -1; break; } xmpp_run(ctx); // clean jid presence list while (jid_list) { list_current = jid_list->next; free(jid_list); jid_list = list_current; } if (reconnect <= 0) { fprintf(stdout, "i offline\n"); fflush(stdout); break; } // https://www.rfc-editor.org/rfc/rfc6120 -> 3.3. Reconnection: // SHOULD be set to an unpredictable number between 0 and 60 (seconds) unpredictable = (unsigned short)rand() % 61; fprintf(stdout, "I offline - lost connection - try reconnect in %ds ...\n", unpredictable); fflush(stdout); sleep(unpredictable); } xmpp_conn_release(conn); xmpp_ctx_free(ctx); xmpp_shutdown(); if (arg_null) { free(arg_null); } // reconnect is 0, -1 on ssl fail or -signum return -reconnect; }