From 14b664f4c74b7feb3204ac7128b3d22e12f44721 Mon Sep 17 00:00:00 2001 From: "jabber.developer2" Date: Tue, 2 Dec 2025 18:21:47 +0300 Subject: [PATCH 1/4] fix: Fix XMPP stream parsing for multiple stanzas - Add special handling for (unclosed root element) - Feed virtual element to allow multiple root stanzas - Support non- IQ payloads like (XEP-0199) - Relax stanza matching: allow extra attrs/children in received - Add legacy auth feature in stream:features - Use larger read buffer (1024 bytes) for efficiency - Clear curr_string buffer after each stanza --- src/server/server.c | 22 +++++++++++++++++----- src/server/stanza.c | 17 ++++++++++++----- src/server/stanzas.c | 18 ++++++++---------- src/server/stream_parser.c | 38 +++++++++++++++++++++++++++++++++++++- 4 files changed, 74 insertions(+), 21 deletions(-) diff --git a/src/server/server.c b/src/server/server.c index 1cd8a22..c9720fc 100644 --- a/src/server/server.c +++ b/src/server/server.c @@ -48,7 +48,7 @@ #define XML_START "" #define STREAM_RESP "" -#define FEATURES "" +#define FEATURES "" #define STREAM_END "" @@ -97,7 +97,7 @@ write_stream(const char * const stream) int read_stream(void) { - char buf[2]; + char buf[1024]; memset(buf, 0, sizeof(buf)); GString *stream = g_string_new(""); @@ -120,7 +120,7 @@ read_stream(void) send_queue = NULL; pthread_mutex_unlock(&send_queue_lock); - int read_size = recv(client->sock, buf, 1, 0); + int read_size = recv(client->sock, buf, sizeof(buf) - 1, 0); // client disconnect if (read_size == 0) { @@ -133,6 +133,11 @@ read_stream(void) if (read_size == -1) { // got nothing, sleep and try again if (errno == EAGAIN || errno == EWOULDBLOCK) { + static int eagain_count = 0; + eagain_count++; + if (eagain_count % 1000 == 0) { + fflush(stderr); + } errno = 0; usleep(1000 * 5); continue; @@ -145,8 +150,9 @@ read_stream(void) } } - // success, feed parser with byte - parser_feed(buf, 1); + // success, feed parser with received data + buf[read_size] = '\0'; // null-terminate + parser_feed(buf, read_size); g_string_append_len(stream, buf, read_size); if (g_str_has_suffix(stream->str, STREAM_END)) { log_println(STBBR_LOGINFO, "RECV: "); @@ -168,6 +174,8 @@ stream_start_callback(void) write_stream(XML_START); write_stream(STREAM_RESP); write_stream(FEATURES); + // DEBUG removed + fflush(stderr); } void @@ -375,6 +383,7 @@ _start_server_cb(void* userdata) prctl(PR_SET_NAME, "stbr"); #endif + // DEBUG removed struct sockaddr_in client_addr; // listen socket non blocking @@ -390,6 +399,7 @@ _start_server_cb(void* userdata) int c = sizeof(struct sockaddr_in); int client_socket; errno = 0; + // DEBUG removed while ((client_socket = accept(listen_socket, (struct sockaddr *)&client_addr, (socklen_t*)&c)) == -1) { if (errno != EAGAIN && errno != EWOULDBLOCK) { log_println(STBBR_LOGERROR, "Accept failed: %s", strerror(errno)); @@ -407,8 +417,10 @@ _start_server_cb(void* userdata) } client = xmppclient_new(client_addr, client_socket); + // DEBUG removed parser_init(stream_start_callback, auth_callback, id_callback, query_callback); + // DEBUG removed read_stream(); return NULL; diff --git a/src/server/stanza.c b/src/server/stanza.c index b7fa116..c3f447e 100644 --- a/src/server/stanza.c +++ b/src/server/stanza.c @@ -103,7 +103,8 @@ stanza_to_string(XMPPStanza *stanza) } char *result = stanza_str->str; - g_string_free(stanza_str, FALSE); + char *unused_gstring_data = g_string_free(stanza_str, FALSE); + (void)unused_gstring_data; // Silence warning return result; } @@ -248,12 +249,18 @@ stanza_get_query_request(XMPPStanza *stanza) return NULL; } - XMPPStanza *query = stanza_get_child_by_name(stanza, "query"); - if (!query) { - return NULL; + XMPPStanza *payload = stanza_get_child_by_name(stanza, "query"); + if (!payload) { + // Support IQ payloads that don't use (e.g. ) as long as + // there is a single child element to associate with the namespace. + if (stanza->children && stanza->children->next == NULL) { + payload = stanza->children->data; + } else { + return NULL; + } } - const char *xmlns = stanza_get_attr(query, "xmlns"); + const char *xmlns = stanza_get_attr(payload, "xmlns"); if (!xmlns) { return NULL; } diff --git a/src/server/stanzas.c b/src/server/stanzas.c index b962e5a..0e27412 100644 --- a/src/server/stanzas.c +++ b/src/server/stanzas.c @@ -153,32 +153,30 @@ _stanzas_equal(XMPPStanza *first, XMPPStanza *second) return -1; } - // check attribute count - if (g_list_length(first->attrs) != g_list_length(second->attrs)) { + // check attribute count - now we allow second to have MORE attributes + // All attrs from first must be in second, but not vice versa + if (g_list_length(first->attrs) > g_list_length(second->attrs)) { return -1; } - // check children count - if (g_list_length(first->children) != g_list_length(second->children)) { + // check children count - now we allow second to have MORE children + if (g_list_length(first->children) > g_list_length(second->children)) { return -1; } - // check presence of content - if (!first->content && second->content) { - return -1; - } + // check presence of content - only if first requires it if (first->content && !second->content) { return -1; } - // check content is exists + // check content if first has it if (first->content) { if (g_strcmp0(first->content->str, second->content->str) != 0) { return -1; } } - // check attributes + // check attributes - all from first must be in second if (first->attrs) { GList *first_curr_attr = first->attrs; while (first_curr_attr) { diff --git a/src/server/stream_parser.c b/src/server/stream_parser.c index 0fdc758..2764b91 100644 --- a/src/server/stream_parser.c +++ b/src/server/stream_parser.c @@ -68,9 +68,35 @@ int parser_feed(char *chunk, int len) { g_string_append_len(curr_string, chunk, len); + + // Special handling for XMPP stream opener + // Check if we've received a complete element (ending with '>') + // Since XMPP streams have an unclosed root element, we need special detection + if (len > 0 && chunk[len-1] == '>') { + const char *str = curr_string->str; + // Look for + if (strstr(str, "")) { + // Manually trigger the stream start callback + log_println(STBBR_LOGINFO, "RECV: %s", curr_string->str); + if (stream_start_cb) { + stream_start_cb(); + } + // Reset for next stanzas + g_string_free(curr_string, TRUE); + curr_string = g_string_new(""); + parser_close(); + parser_init(stream_start_cb, auth_cb, id_cb, query_cb); + // Feed a virtual root element to allow multiple stanzas + XML_Parse(parser, "", 9, 0); + return 1; + } + } + int res = XML_Parse(parser, chunk, len, 0); parser_reset(); + return res; } @@ -96,6 +122,12 @@ parser_reset(void) static void _start_element(void *data, const char *element, const char **attributes) { + + // Ignore our virtual wrapper element + if (g_strcmp0(element, "wrapper") == 0) { + return; + } + if (g_strcmp0(element, "stream:stream") == 0) { log_println(STBBR_LOGINFO, "RECV: %s", curr_string->str); stream_start_cb(); @@ -128,6 +160,10 @@ _end_element(void *data, const char *element) log_println(STBBR_LOGINFO, "RECV: %s", curr_string->str); stanzas_add(curr_stanza); + + // Clear the string buffer for the next stanza + g_string_truncate(curr_string, 0); + if (stanza_get_child_by_ns(curr_stanza, "jabber:iq:auth")) { auth_cb(curr_stanza); } else { @@ -141,7 +177,7 @@ _end_element(void *data, const char *element) } } - do_reset = 1; + // Don't reset parser here - allow multiple root elements for XMPP stanzas } static void -- 2.49.1 From 0c38ca0c5042e89307f891de3c0ed2ac35b7f338 Mon Sep 17 00:00:00 2001 From: "jabber.developer2" Date: Tue, 2 Dec 2025 19:42:40 +0300 Subject: [PATCH 2/4] fix: Fix XMPP stream parsing depth tracking and add debug logging - Reset depth counter in parser_init() to fix stanza parsing between connections - Properly track depth for virtual wrapper element - Add debug logging in stanzas_verify_any() to trace stanza matching --- src/server/stanzas.c | 10 ++++++++++ src/server/stream_parser.c | 20 ++++++++++++++++---- 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/src/server/stanzas.c b/src/server/stanzas.c index 0e27412..c437062 100644 --- a/src/server/stanzas.c +++ b/src/server/stanzas.c @@ -75,15 +75,25 @@ stanzas_verify_any(XMPPStanza *stanza) return 0; } + char *expected_str = stanza_to_string(stanza); + fprintf(stderr, "DEBUG verify_any: Looking for: %s\n", expected_str); + free(expected_str); + GList *curr = g_list_last(stanzas); + int idx = g_list_length(stanzas) - 1; while (curr) { XMPPStanza *curr_stanza = curr->data; + char *curr_str = stanza_to_string(curr_stanza); + fprintf(stderr, "DEBUG verify_any: [%d] %s\n", idx, curr_str); + free(curr_str); + if (_stanzas_equal(stanza, curr_stanza) == 0) { pthread_mutex_unlock(&stanzas_lock); return 1; } curr = g_list_previous(curr); + idx--; } pthread_mutex_unlock(&stanzas_lock); diff --git a/src/server/stream_parser.c b/src/server/stream_parser.c index 2764b91..6fbada0 100644 --- a/src/server/stream_parser.c +++ b/src/server/stream_parser.c @@ -53,6 +53,7 @@ parser_init(stream_start_func startcb, auth_func authcb, id_func idcb, query_fun g_string_free(curr_string, TRUE); } curr_string = g_string_new(""); + depth = 0; // Reset depth on parser init stream_start_cb = startcb; auth_cb = authcb; @@ -116,15 +117,17 @@ parser_reset(void) parser_close(); parser_init(stream_start_cb, auth_cb, id_cb, query_cb); + // Feed a virtual root element to allow multiple stanzas after reset + XML_Parse(parser, "", 9, 0); do_reset = 0; } static void _start_element(void *data, const char *element, const char **attributes) { - - // Ignore our virtual wrapper element + // Virtual wrapper element - track depth but don't create stanza if (g_strcmp0(element, "wrapper") == 0) { + depth++; return; } @@ -137,7 +140,8 @@ _start_element(void *data, const char *element, const char **attributes) XMPPStanza *stanza = stanza_new(element, attributes); - if (depth == 0) { + // depth == 1 means we're at wrapper level, so this is a root XMPP stanza + if (depth == 1) { curr_stanza = stanza; curr_stanza->parent = NULL; } else { @@ -151,8 +155,16 @@ _start_element(void *data, const char *element, const char **attributes) static void _end_element(void *data, const char *element) { + // Virtual wrapper element - just track depth + if (g_strcmp0(element, "wrapper") == 0) { + depth--; + return; + } + depth--; - if (depth > 0) { + + // depth == 1 means we're back at wrapper level - stanza is complete + if (depth > 1) { stanza_add_child(curr_stanza->parent, curr_stanza); curr_stanza = curr_stanza->parent; return; -- 2.49.1 From d09adad0e1b55a958cc4cefad2b19552a8a3ad0a Mon Sep 17 00:00:00 2001 From: "jabber.developer2" Date: Wed, 3 Dec 2025 02:38:33 +0300 Subject: [PATCH 3/4] fix: Remove debug logging that interfered with expect tests --- src/server/prime.c | 5 +++++ src/server/server.c | 5 +++++ src/server/stanzas.c | 10 ---------- src/server/stream_parser.c | 5 ++++- 4 files changed, 14 insertions(+), 11 deletions(-) diff --git a/src/server/prime.c b/src/server/prime.c index 1a382f4..5cf7663 100644 --- a/src/server/prime.c +++ b/src/server/prime.c @@ -21,6 +21,7 @@ */ #include +#include #include #include @@ -105,5 +106,9 @@ prime_for_query(const char *query, char *stream) XMPPStanza* prime_get_for_query(const char *query) { + if (!querystubs) { + return NULL; + } + return g_hash_table_lookup(querystubs, query); } diff --git a/src/server/server.c b/src/server/server.c index c9720fc..f8ba39f 100644 --- a/src/server/server.c +++ b/src/server/server.c @@ -67,6 +67,10 @@ static void* _start_server_cb(void* userdata); void write_stream(const char * const stream) { + if (!client) { + return; + } + int to_send = strlen(stream); char *marker = (char*)stream; @@ -252,6 +256,7 @@ query_callback(const char *query, const char *id) log_println(STBBR_LOGINFO, "--> QUERY callback fired for '%s'", query); stanza_set_id(stanza, id); char *stream = stanza_to_string(stanza); + write_stream(stream); free(stream); } diff --git a/src/server/stanzas.c b/src/server/stanzas.c index c437062..0e27412 100644 --- a/src/server/stanzas.c +++ b/src/server/stanzas.c @@ -75,25 +75,15 @@ stanzas_verify_any(XMPPStanza *stanza) return 0; } - char *expected_str = stanza_to_string(stanza); - fprintf(stderr, "DEBUG verify_any: Looking for: %s\n", expected_str); - free(expected_str); - GList *curr = g_list_last(stanzas); - int idx = g_list_length(stanzas) - 1; while (curr) { XMPPStanza *curr_stanza = curr->data; - char *curr_str = stanza_to_string(curr_stanza); - fprintf(stderr, "DEBUG verify_any: [%d] %s\n", idx, curr_str); - free(curr_str); - if (_stanzas_equal(stanza, curr_stanza) == 0) { pthread_mutex_unlock(&stanzas_lock); return 1; } curr = g_list_previous(curr); - idx--; } pthread_mutex_unlock(&stanzas_lock); diff --git a/src/server/stream_parser.c b/src/server/stream_parser.c index 6fbada0..3e91e30 100644 --- a/src/server/stream_parser.c +++ b/src/server/stream_parser.c @@ -170,7 +170,10 @@ _end_element(void *data, const char *element) return; } - log_println(STBBR_LOGINFO, "RECV: %s", curr_string->str); + char *stanza_str = stanza_to_string(curr_stanza); + log_println(STBBR_LOGINFO, "RECV: %s", stanza_str); + free(stanza_str); + stanzas_add(curr_stanza); // Clear the string buffer for the next stanza -- 2.49.1 From fdb615a2138bcf5125708c83f7d4e9ed5d121acf Mon Sep 17 00:00:00 2001 From: "jabber.developer2" Date: Sat, 6 Dec 2025 15:06:02 +0300 Subject: [PATCH 4/4] feat: Add stbbr_for_presence_to for MUC join presence matching - Add prime_for_presence_to() and prime_get_for_presence_to() in prime.c - Add presence_to_callback() in server.c to handle MUC join presence - Export stbbr_for_presence_to() in stabber.h - Add stanzas_debug_last() for debugging failed verifications --- src/client/stabber.c | 7 +++++++ src/server/prime.c | 29 +++++++++++++++++++++++++++++ src/server/prime.h | 3 +++ src/server/server.c | 30 ++++++++++++++++++++++++++++++ src/server/server.h | 3 +++ src/server/stanzas.c | 24 ++++++++++++++++++++++++ src/server/stanzas.h | 2 ++ src/server/stream_parser.c | 4 ++++ src/server/verify.c | 2 ++ stabber.h | 1 + 10 files changed, 105 insertions(+) diff --git a/src/client/stabber.c b/src/client/stabber.c index 353d5ff..290e0ce 100644 --- a/src/client/stabber.c +++ b/src/client/stabber.c @@ -63,6 +63,13 @@ stbbr_for_query(char *query, char *stream) return 1; } +int +stbbr_for_presence_to(char *to_jid, char *stream) +{ + prime_for_presence_to(to_jid, stream); + return 1; +} + void stbbr_wait_for(char *id) { diff --git a/src/server/prime.c b/src/server/prime.c index 5cf7663..7cafac0 100644 --- a/src/server/prime.c +++ b/src/server/prime.c @@ -33,6 +33,7 @@ static char *required_passwd = NULL; static GHashTable *idstubs = NULL; static GHashTable *querystubs = NULL; +static GHashTable *presence_to_stubs = NULL; void prime_init(void) @@ -40,6 +41,7 @@ prime_init(void) required_passwd = strdup("password"); idstubs = g_hash_table_new_full(g_str_hash, g_str_equal, free, free); querystubs = g_hash_table_new_full(g_str_hash, g_str_equal, free, (GDestroyNotify)stanza_free); + presence_to_stubs = g_hash_table_new_full(g_str_hash, g_str_equal, free, (GDestroyNotify)stanza_free); } void @@ -57,6 +59,11 @@ prime_free_all(void) g_hash_table_destroy(querystubs); } querystubs = NULL; + + if (presence_to_stubs) { + g_hash_table_destroy(presence_to_stubs); + } + presence_to_stubs = NULL; } void @@ -112,3 +119,25 @@ prime_get_for_query(const char *query) return g_hash_table_lookup(querystubs, query); } + +void +prime_for_presence_to(const char *to_jid, char *stream) +{ + if (!presence_to_stubs) { + return; + } + + log_println(STBBR_LOGDEBUG, "Received stub for presence to: %s, stanza: %s", to_jid, stream); + XMPPStanza *stanza = stanza_parse(stream); + g_hash_table_insert(presence_to_stubs, strdup(to_jid), stanza); +} + +XMPPStanza* +prime_get_for_presence_to(const char *to_jid) +{ + if (!presence_to_stubs) { + return NULL; + } + + return g_hash_table_lookup(presence_to_stubs, to_jid); +} diff --git a/src/server/prime.h b/src/server/prime.h index 5c5c7ac..694657d 100644 --- a/src/server/prime.h +++ b/src/server/prime.h @@ -37,4 +37,7 @@ char* prime_get_for_id(const char *id); int prime_for_query(const char *query, char *stream); XMPPStanza* prime_get_for_query(const char *query); +int prime_for_presence_to(const char *to_jid, char *stream); +XMPPStanza* prime_get_for_presence_to(const char *to_jid); + #endif diff --git a/src/server/server.c b/src/server/server.c index f8ba39f..ef04ae0 100644 --- a/src/server/server.c +++ b/src/server/server.c @@ -261,6 +261,36 @@ query_callback(const char *query, const char *id) free(stream); } +void +presence_to_callback(XMPPStanza *stanza) +{ + if (!stanza || g_strcmp0(stanza->name, "presence") != 0) { + return; + } + + const char *to_jid = stanza_get_attr(stanza, "to"); + if (!to_jid) { + return; + } + + XMPPStanza *response = prime_get_for_presence_to(to_jid); + if (!response) { + return; + } + + log_println(STBBR_LOGINFO, "--> PRESENCE TO callback fired for '%s'", to_jid); + + // Copy the id from the incoming stanza to the response + const char *id = stanza_get_id(stanza); + if (id) { + stanza_set_id(response, id); + } + + char *stream = stanza_to_string(response); + write_stream(stream); + free(stream); +} + void server_wait_for(char *id) { diff --git a/src/server/server.h b/src/server/server.h index 5f86df3..0647dc3 100644 --- a/src/server/server.h +++ b/src/server/server.h @@ -24,6 +24,7 @@ #define __H_SERVER #include "stabber.h" +#include "server/stanza.h" int server_run(stbbr_log_t loglevel, int port, int httpport); void server_stop(void); @@ -32,4 +33,6 @@ void server_wait_for(char *id); void server_send(char *stream); +void presence_to_callback(XMPPStanza *stanza); + #endif diff --git a/src/server/stanzas.c b/src/server/stanzas.c index 0e27412..bdeead7 100644 --- a/src/server/stanzas.c +++ b/src/server/stanzas.c @@ -115,6 +115,30 @@ stanzas_verify_last(XMPPStanza *stanza) } } +void +stanzas_debug_last(void) +{ + pthread_mutex_lock(&stanzas_lock); + if (!stanzas) { + log_println(STBBR_LOGINFO, "DEBUG: No stanzas received yet"); + pthread_mutex_unlock(&stanzas_lock); + return; + } + + GList *last = g_list_last(stanzas); + if (!last) { + log_println(STBBR_LOGINFO, "DEBUG: Stanza list empty"); + pthread_mutex_unlock(&stanzas_lock); + return; + } + + XMPPStanza *last_stanza = (XMPPStanza *)last->data; + char *stanza_str = stanza_to_string(last_stanza); + log_println(STBBR_LOGINFO, "DEBUG LAST RECEIVED: %s", stanza_str); + free(stanza_str); + pthread_mutex_unlock(&stanzas_lock); +} + void stanzas_free_all(void) { diff --git a/src/server/stanzas.h b/src/server/stanzas.h index 2fed655..1b3e857 100644 --- a/src/server/stanzas.h +++ b/src/server/stanzas.h @@ -32,6 +32,8 @@ void stanzas_add(XMPPStanza *stanza); int stanzas_verify_any(XMPPStanza *stanza); int stanzas_verify_last(XMPPStanza *stanza); +void stanzas_debug_last(void); + int stanzas_contains_id(char *id); void stanzas_free_all(void); diff --git a/src/server/stream_parser.c b/src/server/stream_parser.c index 3e91e30..4f17041 100644 --- a/src/server/stream_parser.c +++ b/src/server/stream_parser.c @@ -29,6 +29,8 @@ #include "server/stanza.h" #include "server/stanzas.h" #include "server/log.h" +#include "server/prime.h" +#include "server/server.h" static int depth = 0; static int do_reset = 0; @@ -190,6 +192,8 @@ _end_element(void *data, const char *element) if (query) { query_cb(query, id); } + // Check for presence with "to" attribute (for MUC join responses) + presence_to_callback(curr_stanza); } // Don't reset parser here - allow multiple root elements for XMPP stanzas diff --git a/src/server/verify.c b/src/server/verify.c index 808ba59..46c62fc 100644 --- a/src/server/verify.c +++ b/src/server/verify.c @@ -100,6 +100,8 @@ verify_last(char *stanza_text) log_println(STBBR_LOGINFO, "VERIFY LAST SUCCESS: %s", stanza_text); } else { log_println(STBBR_LOGINFO, "VERIFY LAST FAIL: %s", stanza_text); + // Debug: show what was actually received + stanzas_debug_last(); } return result; diff --git a/stabber.h b/stabber.h index daa2ad0..5a20b74 100644 --- a/stabber.h +++ b/stabber.h @@ -38,6 +38,7 @@ void stbbr_set_timeout(int seconds); int stbbr_auth_passwd(char *password); int stbbr_for_id(char *id, char *stream); int stbbr_for_query(char *query, char *stream); +int stbbr_for_presence_to(char *to_jid, char *stream); void stbbr_wait_for(char *id); -- 2.49.1