From ecbef993632c9b3bdf442b381e02e1ad24bc1c87 Mon Sep 17 00:00:00 2001 From: defanor Date: Fri, 17 Nov 2023 18:34:47 +0300 Subject: Implement MUC self-ping (XEP-0410) --- src/rexmpp.c | 228 +++++++++++++++++++++++++++++++++++++++++++++++---- src/rexmpp.h | 64 ++++++++++++++- src/rexmpp.rs | 41 ++++++--- src/rexmpp_console.c | 106 +++++++++++++++++++----- 4 files changed, 388 insertions(+), 51 deletions(-) (limited to 'src') diff --git a/src/rexmpp.c b/src/rexmpp.c index 5bfa375..7edf610 100644 --- a/src/rexmpp.c +++ b/src/rexmpp.c @@ -112,6 +112,142 @@ void rexmpp_log (rexmpp_t *s, int priority, const char *format, ...) } } +rexmpp_err_t rexmpp_muc_ping_set (rexmpp_t *s, + const char *occupant_jid, + const char *password, + unsigned int delay) +{ + /* At first try to edit an existing record. */ + rexmpp_muc_ping_t *mp = s->muc_ping; + while (mp != NULL) { + if (strcmp(mp->jid, occupant_jid) == 0) { + mp->delay = delay; + return REXMPP_SUCCESS; + } + mp = mp->next; + } + + /* No existing self-ping record for this occupant JID; create a new + one. */ + mp = malloc(sizeof(rexmpp_muc_ping_t)); + if (mp == NULL) { + rexmpp_log(s, LOG_ERR, + "Failed to allocate memory for a MUC self-ping record: %s", + strerror(errno)); + return REXMPP_E_MALLOC; + } + mp->jid = strdup(occupant_jid); + if (mp->jid == NULL) { + rexmpp_log(s, LOG_ERR, + "Failed to duplicate JID string for a MUC self-ping record: %s", + strerror(errno)); + free(mp); + return REXMPP_E_OTHER; + } + if (password != NULL) { + mp->password = strdup(password); + } else { + mp->password = NULL; + } + + mp->delay = delay; + mp->requested = 0; + mp->last_activity.tv_sec = 0; + mp->last_activity.tv_nsec = 0; + mp->next = s->muc_ping; + s->muc_ping = mp; + return REXMPP_SUCCESS; +} + +rexmpp_err_t rexmpp_muc_ping_remove (rexmpp_t *s, const char *occupant_jid) { + rexmpp_muc_ping_t **mp = &(s->muc_ping); + while (*mp != NULL) { + if (strcmp(occupant_jid, (*mp)->jid) == 0) { + rexmpp_muc_ping_t *found = *mp; + *mp = found->next; + free(found->jid); + if (found->password != NULL) { + free(found->password); + } + free(found); + return REXMPP_SUCCESS; + } + mp = &((*mp)->next); + } + rexmpp_log(s, LOG_WARNING, + "Removal of MUC self-ping record for JID %s is requested, " + "but no such record is found", + occupant_jid); + return REXMPP_E_OTHER; +} + +rexmpp_err_t rexmpp_muc_join (rexmpp_t *s, + const char *occupant_jid, + const char *password, + unsigned int ping_delay) +{ + rexmpp_xml_t *presence = + rexmpp_xml_new_elem("presence", "jabber:client"); + rexmpp_xml_add_id(presence); + rexmpp_xml_add_attr(presence, "from", s->assigned_jid.full); + rexmpp_xml_add_attr(presence, "to", occupant_jid); + rexmpp_xml_t *x = + rexmpp_xml_new_elem("x", "http://jabber.org/protocol/muc"); + rexmpp_xml_add_child(presence, x); + rexmpp_err_t ret = rexmpp_send(s, presence); + if (ping_delay > 0) { + rexmpp_muc_ping_set(s, occupant_jid, password, ping_delay); + } + return ret; +} + +rexmpp_err_t rexmpp_muc_leave (rexmpp_t *s, const char *occupant_jid) { + rexmpp_muc_ping_remove(s, occupant_jid); + rexmpp_xml_t *presence = + rexmpp_xml_new_elem("presence", "jabber:client"); + rexmpp_xml_add_id(presence); + rexmpp_xml_add_attr(presence, "from", s->assigned_jid.full); + rexmpp_xml_add_attr(presence, "to", occupant_jid); + rexmpp_xml_add_attr(presence, "type", "unavailable"); + return rexmpp_send(s, presence); +} + +void rexmpp_muc_pong (rexmpp_t *s, + void *ptr, + rexmpp_xml_t *req, + rexmpp_xml_t *response, + int success) +{ + rexmpp_muc_ping_t *mp = ptr; + clock_gettime(CLOCK_MONOTONIC, &(mp->last_activity)); + mp->requested = 0; + if (! success) { + const char *jid = rexmpp_xml_find_attr_val(req, "to"); + rexmpp_xml_t *error = rexmpp_xml_first_elem_child(response); + if (error == NULL) { + rexmpp_log(s, LOG_ERR, + "MUC self-ping failure for %s, and no error element received", + jid); + rexmpp_muc_ping_remove(s, jid); + } else { + rexmpp_log(s, LOG_WARNING, "MUC self-ping failure for %s: %s", + jid, error->alt.elem.qname.name); + if (rexmpp_xml_match(error, NULL, "service-unavailable") || + rexmpp_xml_match(error, NULL, "feature-not-implemented") || + rexmpp_xml_match(error, NULL, "remote-server-not-found") || + rexmpp_xml_match(error, NULL, "remote-server-timeout")) { + rexmpp_log(s, LOG_WARNING, "Giving up on pinging it"); + rexmpp_muc_ping_remove(s, jid); + } else if (rexmpp_xml_match(error, NULL, "item-not-found")) { + /* Ignore and keep pinging? */ + } else { + /* Some other error, re-join. */ + rexmpp_muc_join(s, jid, NULL, 0); + } + } + } +} + char *rexmpp_capabilities_hash (rexmpp_t *s, rexmpp_xml_t *info) { @@ -474,6 +610,7 @@ rexmpp_err_t rexmpp_init (rexmpp_t *s, s->local_address = NULL; s->jingle_prefer_rtcp_mux = 1; s->path_mtu_discovery = -1; + s->muc_ping_default_delay = 600; s->send_buffer = NULL; s->send_queue = NULL; s->server_srv = NULL; @@ -517,6 +654,7 @@ rexmpp_err_t rexmpp_init (rexmpp_t *s, s->ping_requested = 0; s->last_network_activity.tv_sec = 0; s->last_network_activity.tv_nsec = 0; + s->muc_ping = NULL; s->disco_info = NULL; s->jingle_rtp_description = @@ -747,6 +885,12 @@ void rexmpp_done (rexmpp_t *s) { free(s->roster_ver); s->roster_ver = NULL; } + while (s->muc_ping != NULL) { + rexmpp_muc_ping_t *mp_next = s->muc_ping->next; + free(s->muc_ping->jid); + free(s->muc_ping); + s->muc_ping = mp_next; + } if (s->disco_info != NULL) { rexmpp_xml_free_list(s->disco_info); s->disco_info = NULL; @@ -1983,36 +2127,32 @@ rexmpp_err_t rexmpp_process_element (rexmpp_t *s, rexmpp_xml_t *elem) { if (item_id == NULL) { continue; } - const char *autojoin = rexmpp_xml_find_attr_val(conference, "autojoin"); + const char *autojoin = + rexmpp_xml_find_attr_val(conference, "autojoin"); if (autojoin == NULL) { continue; } if (strcmp(autojoin, "true") == 0 || strcmp(autojoin, "1") == 0) { - rexmpp_xml_t *presence = - rexmpp_xml_new_elem("presence", "jabber:client"); - rexmpp_xml_add_id(presence); - rexmpp_xml_add_attr(presence, "from", - s->assigned_jid.full); rexmpp_xml_t *nick = rexmpp_xml_find_child(conference, "urn:xmpp:bookmarks:1", "nick"); - const char *nick_str; + const char *nick_str = NULL; if (nick != NULL) { nick_str = rexmpp_xml_text_child(nick); - } else { + } + if (nick_str == NULL) { nick_str = s->initial_jid.local; } - char *jid = malloc(strlen(item_id) + strlen(nick_str) + 2); - sprintf(jid, "%s/%s", item_id, nick_str); - rexmpp_xml_add_attr(presence, "to", jid); - free(jid); - rexmpp_xml_t *x = - rexmpp_xml_new_elem("x", - "http://jabber.org/protocol/muc"); - rexmpp_xml_add_child(presence, x); - rexmpp_send(s, presence); + char *occupant_jid = + malloc(strlen(item_id) + strlen(nick_str) + 2); + sprintf(occupant_jid, "%s/%s", item_id, nick_str); + const char *password = + rexmpp_xml_find_attr_val(conference, "password"); + rexmpp_muc_join(s, occupant_jid, password, + s->muc_ping_default_delay); + free(occupant_jid); } } } @@ -2446,8 +2586,41 @@ rexmpp_err_t rexmpp_run (rexmpp_t *s, fd_set *read_fds, fd_set *write_fds) { } } + /* MUC self-pinging. */ + if (s->tcp_state == REXMPP_TCP_CONNECTED && + s->stream_state == REXMPP_STREAM_READY) + { + rexmpp_muc_ping_t *mp = s->muc_ping; + while (mp != NULL) { + if (mp->last_activity.tv_sec + mp->delay <= now.tv_sec) { + if (mp->requested == 0) { + clock_gettime(CLOCK_MONOTONIC, &(mp->last_activity)); + mp->requested = 1; + rexmpp_xml_t *ping_cmd = + rexmpp_xml_new_elem("ping", "urn:xmpp:ping"); + rexmpp_iq_new(s, "get", mp->jid, + ping_cmd, rexmpp_muc_pong, mp); + } else { + /* Requested already, and delay time passed again, without + a reply (not even an error). Warn the user, remove this + MUC. */ + char *occupant_jid_to_remove = mp->jid; + rexmpp_log(s, LOG_WARNING, + "No MUC self-ping reply for %s, " + "disabling self-ping for it", + mp->jid); + mp = mp->next; + rexmpp_muc_ping_remove(s, occupant_jid_to_remove); + continue; + } + } + mp = mp->next; + } + } + /* Pinging the server. */ if (s->tcp_state == REXMPP_TCP_CONNECTED && + s->stream_state == REXMPP_STREAM_READY && s->last_network_activity.tv_sec + s->ping_delay <= now.tv_sec) { if (s->ping_requested == 0) { s->ping_requested = 1; @@ -2456,6 +2629,9 @@ rexmpp_err_t rexmpp_run (rexmpp_t *s, fd_set *read_fds, fd_set *write_fds) { rexmpp_iq_new(s, "get", s->initial_jid.domain, ping_cmd, rexmpp_pong, NULL); } else { + /* Last network activity is updated on sending as well as on + receiving, so this will not be triggered right after sending + the request. */ rexmpp_log(s, LOG_WARNING, "Ping timeout, reconnecting."); rexmpp_cleanup(s); rexmpp_schedule_reconnect(s); @@ -2609,6 +2785,24 @@ struct timespec *rexmpp_timeout (rexmpp_t *s, } if (s->tcp_state == REXMPP_TCP_CONNECTED && + s->stream_state == REXMPP_STREAM_READY) { + rexmpp_muc_ping_t *mp = s->muc_ping; + while (mp != NULL) { + if (mp->last_activity.tv_sec + mp->delay > now.tv_sec) { + time_t next_ping = + mp->last_activity.tv_sec + mp->delay - now.tv_sec; + if (ret == NULL || next_ping < ret->tv_sec) { + tv->tv_sec = next_ping; + tv->tv_nsec = 0; + ret = tv; + } + } + mp = mp->next; + } + } + + if (s->tcp_state == REXMPP_TCP_CONNECTED && + s->stream_state == REXMPP_STREAM_READY && s->last_network_activity.tv_sec + s->ping_delay > now.tv_sec) { time_t next_ping = s->last_network_activity.tv_sec + s->ping_delay - now.tv_sec; diff --git a/src/rexmpp.h b/src/rexmpp.h index e9d833f..1f7ab32 100644 --- a/src/rexmpp.h +++ b/src/rexmpp.h @@ -227,6 +227,24 @@ struct rexmpp_iq rexmpp_iq_t *next; }; +typedef struct rexmpp_muc_ping rexmpp_muc_ping_t; + +/** @brief MUC self-ping data. */ +struct rexmpp_muc_ping +{ + /** @brief Own occupant JID to ping. */ + char *jid; + /** @brief Optional password to rejoin with. */ + char *password; + /** @brief Ping delay, in seconds. */ + unsigned int delay; + /** @brief Whether a ping is requested (pending) already. */ + int requested; + /** @brief When the MUC was active. */ + struct timespec last_activity; + rexmpp_muc_ping_t *next; +}; + typedef void (*log_function_t) (rexmpp_t *s, int priority, const char *format, va_list args); typedef int (*sasl_property_cb_t) (rexmpp_t *s, rexmpp_sasl_property prop); typedef int (*xml_in_cb_t) (rexmpp_t *s, rexmpp_xml_t *node); @@ -281,6 +299,8 @@ struct rexmpp int jingle_prefer_rtcp_mux; int path_mtu_discovery; /* An IP_MTU_DISCOVER parameter for TCP sockets, or -1 to not set it */ + /* A delay in seconds, to use for MUC self-ping by default */ + unsigned int muc_ping_default_delay; /* Resource limits. */ uint32_t stanza_queue_size; @@ -337,10 +357,13 @@ struct rexmpp char *stream_id; /* Server ping configuration and state. */ - int ping_delay; + unsigned int ping_delay; int ping_requested; struct timespec last_network_activity; + /* MUC self-ping */ + rexmpp_muc_ping_t *muc_ping; + /* DNS-related structures. */ rexmpp_dns_ctx_t resolver; rexmpp_dns_result_t *server_srv; @@ -584,4 +607,43 @@ rexmpp_disco_find_feature (rexmpp_t *s, int fresh, int max_requests); +/** + @brief Add a MUC JID to self-ping + @param[in,out] s ::rexmpp + @param[in] jid Own occupant JID to ping + @param[in] password Optional password to rejoin with + @param[in] delay How often to ping, in seconds +*/ +rexmpp_err_t rexmpp_muc_ping_set (rexmpp_t *s, + const char *occupant_jid, + const char *password, + unsigned int delay); + +/** + @brief Remove a MUC JID to self-ping + @param[in,out] s ::rexmpp + @param[in] jid Own occupant JID +*/ +rexmpp_err_t rexmpp_muc_ping_remove (rexmpp_t *s, + const char *occupant_jid); + +/** + @brief Join a MUC, optionally setting self-ping + @param[in,out] s ::rexmpp + @param[in] occupant_jid Occupant JID + @param[in] password Optional password + @param[in] ping_delay MUC self-ping delay, 0 to not set it +*/ +rexmpp_err_t rexmpp_muc_join (rexmpp_t *s, + const char *occupant_jid, + const char *password, + unsigned int ping_delay); + +/** + @brief Leave a MUC, stop self-pinging it + @param[in,out] s ::rexmpp + @param[in] occupant_jid Occupant JID +*/ +rexmpp_err_t rexmpp_muc_leave (rexmpp_t *s, const char *occupant_jid); + #endif diff --git a/src/rexmpp.rs b/src/rexmpp.rs index 5531ca2..a6950a4 100644 --- a/src/rexmpp.rs +++ b/src/rexmpp.rs @@ -102,6 +102,16 @@ pub struct RexmppIQ { pub next: *mut RexmppIQ } +#[repr(C)] +pub struct RexmppMUCPing { + pub jid: *mut c_char, + pub password: *mut c_char, + pub delay: c_uint, + pub requested: bool, + pub last_activity: timespec, + pub next: *mut RexmppMUCPing +} + #[repr(C)] pub struct Rexmpp { pub resolver_state: ResolverState, @@ -118,7 +128,7 @@ pub struct Rexmpp { // Manual host/port configuration pub manual_host: *const c_char, pub manual_port: u16, - pub manual_direct_tls: c_int, + pub manual_direct_tls: bool, // Miscellaneous settings pub disco_node: *const c_char, @@ -128,24 +138,26 @@ pub struct Rexmpp { pub socks_port: u16, // Various knobs (these are used instead of loadable modules) - pub enable_carbons: c_int, // XEP-0280 - pub manage_roster: c_int, + pub enable_carbons: bool, // XEP-0280 + pub manage_roster: bool, pub roster_cache_file: *const c_char, - pub track_roster_presence: c_int, - pub track_roster_events: c_int, // XEP-0163 - pub nick_notifications: c_int, // XEP-0172 - pub retrieve_openpgp_keys: c_int, // XEP-0373 - pub autojoin_bookmarked_mucs: c_int, // XEP-0402 + pub track_roster_presence: bool, + pub track_roster_events: bool, // XEP-0163 + pub nick_notifications: bool, // XEP-0172 + pub retrieve_openpgp_keys: bool, // XEP-0373 + pub autojoin_bookmarked_mucs: bool, // XEP-0402 pub tls_policy: TLSPolicy, - pub enable_jingle: c_int, + pub enable_jingle: bool, pub client_name: *const c_char, // XEP-0030, XEP-0092 pub client_type: *const c_char, // XEP-0030 pub client_version: *const c_char, // XEP-0092 pub local_address: *const c_char, // For ICE, XEP-0176 - pub jingle_prefer_rtcp_mux: c_int, + pub jingle_prefer_rtcp_mux: bool, // An IP_MTU_DISCOVER parameter for TCP sockets, or -1 to not set // it pub path_mtu_discovery: c_int, + // A delay in seconds, to use for MUC self-ping by default + pub muc_ping_default_delay: c_uint, // Resource limits pub stanza_queue_size: u32, pub send_queue_size: u32, @@ -203,10 +215,13 @@ pub struct Rexmpp { pub stream_id: *mut c_char, // Server ping configuration and state - pub ping_delay: c_int, - pub ping_requested: c_int, + pub ping_delay: c_uint, + pub ping_requested: bool, pub last_network_activity: timespec, + // MUC self-ping + pub muc_ping: *mut RexmppMUCPing, + // DNS-related structures pub resolver: *mut c_void, pub server_srv: *mut rexmpp_dns::RexmppDNSResult, @@ -222,7 +237,7 @@ pub struct Rexmpp { // The primary socket used for communication with the server pub server_socket: c_int, // Whether the address it's connected to was verified with DNSSEC - pub server_socket_dns_secure: c_int, + pub server_socket_dns_secure: bool, // A structure used to establish a TCP connection pub server_connection: rexmpp_tcp::RexmppTCPConnection, diff --git a/src/rexmpp_console.c b/src/rexmpp_console.c index 64e5493..a6f859a 100644 --- a/src/rexmpp_console.c +++ b/src/rexmpp_console.c @@ -351,6 +351,38 @@ void rexmpp_console_pubsub_node_deleted (rexmpp_t *s, } } +void rexmpp_bookmark_added (rexmpp_t *s, + void *ptr, + rexmpp_xml_t *req, + rexmpp_xml_t *response, + int success) +{ + (void)ptr; + (void)req; + (void)response; + if (success) { + rexmpp_console_printf(s, "Added a bookmark.\n"); + } else { + rexmpp_console_printf(s, "Failed to add a bookmark.\n"); + } +} + +void rexmpp_bookmark_removed (rexmpp_t *s, + void *ptr, + rexmpp_xml_t *req, + rexmpp_xml_t *response, + int success) +{ + (void)ptr; + (void)req; + (void)response; + if (success) { + rexmpp_console_printf(s, "Removed a bookmark.\n"); + } else { + rexmpp_console_printf(s, "Failed to remove a bookmark.\n"); + } +} + void rexmpp_console_blocklist (rexmpp_t *s, void *ptr, rexmpp_xml_t *req, @@ -440,6 +472,8 @@ void rexmpp_console_feed (rexmpp_t *s, char *str, ssize_t str_len) { "muc join [as] \n" "muc leave [as] \n" "muc tell \n" + "bookmark add [autojoin] [nick] [password]\n" + "bookmark remove \n" "roster list\n" "roster add \n" "roster delete \n" @@ -564,24 +598,20 @@ void rexmpp_console_feed (rexmpp_t *s, char *str, ssize_t str_len) { return; } word = strtok_r(NULL, " ", &words_save_ptr); + if (word == NULL) { + return; + } if (! strcmp(word, "as")) { word = strtok_r(NULL, " ", &words_save_ptr); } if (word == NULL) { return; } - char *full_jid = malloc(strlen(jid.bare) + strlen(word) + 2); - snprintf(full_jid, strlen(jid_str) + strlen(word) + 2, "%s/%s", + char *occupant_jid = malloc(strlen(jid.bare) + strlen(word) + 2); + snprintf(occupant_jid, strlen(jid_str) + strlen(word) + 2, "%s/%s", jid.bare, word); - presence = rexmpp_xml_new_elem("presence", "jabber:client"); - rexmpp_xml_add_id(presence); - rexmpp_xml_add_attr(presence, "from", s->assigned_jid.full); - rexmpp_xml_add_attr(presence, "to", full_jid); - rexmpp_xml_t *x = - rexmpp_xml_new_elem("x", "http://jabber.org/protocol/muc"); - rexmpp_xml_add_child(presence, x); - rexmpp_send(s, presence); - free(full_jid); + rexmpp_muc_join(s, occupant_jid, NULL, s->muc_ping_default_delay); + free(occupant_jid); } if (! strcmp(word, "leave")) { jid_str = strtok_r(NULL, " ", &words_save_ptr); @@ -589,22 +619,58 @@ void rexmpp_console_feed (rexmpp_t *s, char *str, ssize_t str_len) { return; } word = strtok_r(NULL, " ", &words_save_ptr); + if (word == NULL) { + return; + } if (! strcmp(word, "as")) { word = strtok_r(NULL, " ", &words_save_ptr); } if (word == NULL) { return; } - char *full_jid = malloc(strlen(jid.bare) + strlen(word) + 2); - snprintf(full_jid, strlen(jid_str) + strlen(word) + 2, "%s/%s", + char *occupant_jid = malloc(strlen(jid.bare) + strlen(word) + 2); + snprintf(occupant_jid, strlen(jid_str) + strlen(word) + 2, "%s/%s", jid.bare, word); - presence = rexmpp_xml_new_elem("presence", "jabber:client"); - rexmpp_xml_add_id(presence); - rexmpp_xml_add_attr(presence, "from", s->assigned_jid.full); - rexmpp_xml_add_attr(presence, "to", full_jid); - rexmpp_xml_add_attr(presence, "type", "unavailable"); - rexmpp_send(s, presence); - free(full_jid); + rexmpp_muc_leave(s, occupant_jid); + free(occupant_jid); + } + } + + if (! strcmp(word, "bookmark")) { + word = strtok_r(NULL, " ", &words_save_ptr); + if (! strcmp(word, "add")) { + char *muc_jid = strtok_r(NULL, " ", &words_save_ptr); + if (muc_jid == NULL) { + return; + } + char *autojoin = strtok_r(NULL, " ", &words_save_ptr); + char *nick_str = strtok_r(NULL, " ", &words_save_ptr); + char *password = strtok_r(NULL, " ", &words_save_ptr); + + rexmpp_xml_t *conference = + rexmpp_xml_new_elem("conference", "urn:xmpp:bookmarks:1"); + if (autojoin != NULL) { + rexmpp_xml_add_attr(conference, "autojoin", autojoin); + } + if (password != NULL) { + rexmpp_xml_add_attr(conference, "password", password); + } + if (nick_str != NULL) { + rexmpp_xml_t *nick = + rexmpp_xml_new_elem("nick", "urn:xmpp:bookmarks:1"); + rexmpp_xml_add_text(nick, nick_str); + rexmpp_xml_add_child(conference, nick); + } + rexmpp_pubsub_item_publish(s, NULL, "urn:xmpp:bookmarks:1", muc_jid, + conference, rexmpp_bookmark_added, NULL); + } + if (! strcmp(word, "remove")) { + char *muc_jid = strtok_r(NULL, " ", &words_save_ptr); + if (muc_jid == NULL) { + return; + } + rexmpp_pubsub_item_retract(s, NULL, "urn:xmpp:bookmarks:1", muc_jid, + rexmpp_bookmark_removed, NULL); } } -- cgit v1.2.3