From 1a2709c4f035e0b38f6d4bf3ced60e3b80f9f4a9 Mon Sep 17 00:00:00 2001 From: "Ruslan N. Marchenko" Date: Wed, 14 Oct 2020 08:10:57 +0200 Subject: [PATCH 01/14] Add draft SM implementation in porter, ack only --- wocky/wocky-c2s-porter.c | 201 +++++++++++++++++++++++++++++++++++++++ wocky/wocky-connector.c | 51 +++++++++- wocky/wocky-namespaces.h | 3 + wocky/wocky-stanza.c | 2 + wocky/wocky-stanza.h | 1 + 5 files changed, 255 insertions(+), 3 deletions(-) diff --git a/wocky/wocky-c2s-porter.c b/wocky/wocky-c2s-porter.c index 7214337c..43bedab6 100644 --- a/wocky/wocky-c2s-porter.c +++ b/wocky/wocky-c2s-porter.c @@ -37,6 +37,7 @@ #include #include #include +#include #ifdef HAVE_UNISTD_H # include @@ -76,6 +77,8 @@ struct _WockyC2SPorterPrivate /* Queue of (sending_queue_elem *) */ GQueue *sending_queue; + /* Queue of sent WockyStanza's waiting for SM Ack */ + GQueue *unacked_queue; GCancellable *receive_cancellable; gboolean sending_whitespace_ping; @@ -102,6 +105,15 @@ struct _WockyC2SPorterPrivate /* List of (owned WockyStanza *) */ GQueue queueable_stanza_patterns; + gboolean sm_enabled; /* track sent/rcvd stanzas (not nonzas!) */ + gboolean resumable; /* server confirmed it can resume the stream */ + gchar *sm_id; /* a unique stream identifier for resumption */ + gchar *sm_location; /* preferred server address for resumption */ + gsize sm_timeout; /* a time within which we can try to resume the stream */ + gsize rcv_count; /* a count of stanzas we've received and processed */ + gsize snt_count; /* a count of stanzas we've sent over the wire */ + gsize snt_acked; /* a number of last acked stanzas */ + WockyXmppConnection *connection; }; @@ -329,6 +341,9 @@ static gboolean handle_iq_reply (WockyPorter *porter, static void remote_connection_closed (WockyC2SPorter *self, const GError *error); +static void wocky_porter_sm_reset (WockyC2SPorter *self); +static gboolean wocky_porter_sm_handle (WockyC2SPorter *self, WockyNode *node); + static void wocky_c2s_porter_init (WockyC2SPorter *self) { @@ -337,6 +352,7 @@ wocky_c2s_porter_init (WockyC2SPorter *self) self->priv = priv; priv->sending_queue = g_queue_new (); + priv->unacked_queue = NULL; priv->handlers_by_id = g_hash_table_new_full (g_direct_hash, g_direct_equal, NULL, (GDestroyNotify) stanza_handler_free); @@ -456,6 +472,12 @@ wocky_c2s_porter_constructed (GObject *object) g_assert (priv->connection != NULL); + priv->sm_id = NULL; + priv->sm_location = NULL; + priv->sm_enabled = (g_object_get_data (G_OBJECT (priv->connection), + WOCKY_XMPP_NS_SM3) != NULL); + wocky_porter_sm_reset (self); + /* Register the IQ reply handler */ wocky_porter_register_handler_from_anyone (WOCKY_PORTER (self), WOCKY_STANZA_TYPE_IQ, WOCKY_STANZA_SUB_TYPE_RESULT, @@ -645,6 +667,16 @@ close_if_waiting (WockyC2SPorter *self) } } +static gboolean +request_ack_in_idle (gpointer data) +{ + WockyC2SPorter *self = WOCKY_C2S_PORTER (data); + + wocky_porter_sm_handle (self, NULL); + + return FALSE; +} + static void send_stanza_cb (GObject *source, GAsyncResult *res, @@ -674,6 +706,21 @@ send_stanza_cb (GObject *source, g_task_return_boolean (elem->task, TRUE); + if (priv->sm_enabled) + { + WockyStanzaType st; + wocky_stanza_get_type_info (elem->stanza, &st, NULL); + if (st == WOCKY_STANZA_TYPE_MESSAGE + || st == WOCKY_STANZA_TYPE_PRESENCE + || st == WOCKY_STANZA_TYPE_IQ) + { + priv->snt_count++; + g_queue_push_tail (priv->unacked_queue, + g_object_ref (elem->stanza)); + g_idle_add (request_ack_in_idle, self); + } + } + sending_queue_elem_free (elem); if (g_queue_get_length (priv->sending_queue) > 0) @@ -914,6 +961,147 @@ handle_iq_reply (WockyPorter *porter, return ret; } +static void +wocky_porter_sm_reset (WockyC2SPorter *self) +{ + WockyC2SPorterPrivate *priv = wocky_c2s_porter_get_instance_private (self); + + if (priv->sm_enabled) + { + if (G_UNLIKELY (priv->unacked_queue)) + g_queue_clear_full (priv->unacked_queue, g_object_unref); + else + priv->unacked_queue = g_queue_new (); + } + else if (priv->unacked_queue) + { + g_queue_free_full (priv->unacked_queue, g_object_unref); + priv->unacked_queue = NULL; + } + + priv->snt_count = priv->snt_acked = priv->rcv_count = 0; + g_clear_pointer (&(priv->sm_id), g_free); + g_clear_pointer (&(priv->sm_location), g_free); + priv->resumable = FALSE; + priv->sm_timeout = 600; +} + +/* We need to consider normal monotonic increase and uint32 wrap conditions */ +#define ACK_WINDOW(start, stop) ((start <= stop) \ + ? (stop - start) \ + : ((ULONG_MAX - start) + stop)) +#define ACK_WINDOW_MAX 10 + +static gboolean +wocky_porter_sm_handle (WockyC2SPorter *self, + WockyNode *node) +{ + WockyC2SPorterPrivate *priv = wocky_c2s_porter_get_instance_private (self); + + if (G_UNLIKELY(!priv->sm_enabled)) + { + g_warning ("Received SM nonza %s while SM is disabled", node->name); + return FALSE; + } + + if (node == NULL) + { + /* a probe for request - fire one if we have breached the max or fire an + * early notice when we are in the middle of it */ + if (ACK_WINDOW (priv->snt_acked, priv->snt_count) == ACK_WINDOW_MAX/2 + || ACK_WINDOW (priv->snt_acked, priv->snt_count) > ACK_WINDOW_MAX) + { + WockyStanza *r = wocky_stanza_new ("r", WOCKY_XMPP_NS_SM3); + wocky_porter_send (WOCKY_PORTER (self), r); + g_object_unref (r); + return TRUE; + } + } + else if (node->name[0] == 'r' && node->name[1] == '\0') + { + WockyStanza *a = wocky_stanza_new ("a", WOCKY_XMPP_NS_SM3); + WockyNode *an = wocky_stanza_get_top_node (a); + gchar *val = g_strdup_printf ("%lu", priv->rcv_count); + + wocky_node_set_attribute (an, "h", val); + g_free (val); + + wocky_porter_send (WOCKY_PORTER (self), a); + g_object_unref (a); + + return TRUE; + } + else if (node->name[0] == 'a' && node->name[1] == '\0') + { + const gchar *val = wocky_node_get_attribute (node, "h"); + gsize snt = 0; + + /* TODO below conditions are rather fatal, need to terminate the sream */ + if (val == NULL) + { + g_warning ("Missing 'h' attribute, we should better err this stream"); + return FALSE; + } + + snt = strtoul (val, NULL, 10); + if (snt == ULONG_MAX && errno == ERANGE) + { + g_warning ("Invalid number, cannot convert h value[%s] to gsize", val); + return FALSE; + } + + if (ACK_WINDOW (priv->snt_acked, snt) <= ACK_WINDOW (priv->snt_acked, + priv->snt_count)) + { + priv->snt_acked = snt; + } + else + { + g_warning ("Invalid acknowledgement %lu, must be between %lu and %lu", + snt, priv->snt_acked, priv->snt_count); + } + /* now we can head-drop stanzas from unacked_queue till its length is + * (snt_count - snt_acked) */ + while (g_queue_get_length (priv->unacked_queue) + > ACK_WINDOW (priv->snt_count, priv->snt_acked)) + { + WockyStanza *s = g_queue_pop_head (priv->unacked_queue); + g_assert (s); + g_object_unref (s); + } + return TRUE; + } + else if (!g_strcmp0 (node->name, "enabled")) + { + const gchar *val; + if ((val = wocky_node_get_attribute (node, "id")) != NULL) + priv->sm_id = g_strdup (val); + + if ((val = wocky_node_get_attribute (node, "max")) != NULL) + priv->sm_timeout = strtoul (val, NULL, 10); + + if ((val = wocky_node_get_attribute (node, "resume")) != NULL) + priv->resumable = (g_strcmp0 (val, "true") == 0); + + if ((val = wocky_node_get_attribute (node, "location")) != NULL) + priv->sm_location = g_strdup (val); + + g_debug ("SM on connection %p is enabled", priv->connection); + + return TRUE; + } + else if (!g_strcmp0 (node->name, "failed")) + { + g_debug ("SM on connection %p has failed", priv->connection); + priv->sm_enabled = FALSE; + wocky_porter_sm_reset (self); + } + else + g_warning ("Unknown SM nonza '%s' received", node->name); + + return FALSE; +} + static void handle_stanza (WockyC2SPorter *self, WockyStanza *stanza) @@ -929,13 +1117,26 @@ handle_stanza (WockyC2SPorter *self, wocky_stanza_get_type_info (stanza, &type, &sub_type); + if (priv->sm_enabled && (type == WOCKY_STANZA_TYPE_MESSAGE + || type == WOCKY_STANZA_TYPE_PRESENCE + || type == WOCKY_STANZA_TYPE_IQ)) + { + priv->rcv_count++; + } /* The from attribute of the stanza need not always be present, for example * when receiving roster items, so don't enforce it. */ from = wocky_stanza_get_from (stanza); if (from == NULL) { + WockyNode *top_node = wocky_stanza_get_top_node (stanza); + + g_assert (top_node != NULL); + is_from_server = TRUE; + + if (wocky_node_has_ns (top_node, WOCKY_XMPP_NS_SM3)) + handled = wocky_porter_sm_handle (self, top_node); } else if (wocky_decode_jid (from, &node, &domain, &resource)) { diff --git a/wocky/wocky-connector.c b/wocky/wocky-connector.c index e313b990..5308cbc0 100644 --- a/wocky/wocky-connector.c +++ b/wocky/wocky-connector.c @@ -73,7 +73,7 @@ * * ① * ↓ - * establish_session ─────────→ success + * establish_session ─────────→ request_sm_enable → complete_operation * ↓ ↑ * establish_session_sent_cb │ * ↓ │ @@ -2024,6 +2024,51 @@ iq_bind_resource_recv_cb (GObject *source, g_object_unref (reply); } +/* Requesting SM is an opt-in and its failure is non-fatal. + * Therefore we just request it here and handle result in + * porter as part of normal event flow. + */ +static void +request_sm_enable_cb (GObject *source, + GAsyncResult *result, + gpointer data) +{ + WockyXmppConnection *connection = WOCKY_XMPP_CONNECTION (source); + WockyConnector *self = data; + GError *error = NULL; + + if (!wocky_xmpp_connection_send_stanza_finish (connection, result, &error)) + DEBUG ("Failed to send enable nonza: %s", error->message); + else + g_object_set_data (G_OBJECT (connection), WOCKY_XMPP_NS_SM3, (void *) 1); + + complete_operation (self); +} + +static void +request_sm_enable (WockyConnector *self) +{ + WockyConnectorPrivate *priv = wocky_connector_get_instance_private (self); + WockyNode *feat = (priv->features != NULL) ? + wocky_stanza_get_top_node (priv->features) : NULL; + + if ((feat != NULL) && + wocky_node_get_child_ns (feat, "sm", WOCKY_XMPP_NS_SM3)) + { + WockyStanza *enable = wocky_stanza_new ("enable", WOCKY_XMPP_NS_SM3); + WockyNode *en = wocky_stanza_get_top_node (enable); + + wocky_node_set_attributes (en, "max", "600", "resume", "false", NULL); + + wocky_xmpp_connection_send_stanza_async (priv->conn, enable, + priv->cancellable, request_sm_enable_cb, self); + + g_object_unref (enable); + } + else + complete_operation (self); +} + /* ************************************************************************* */ /* final stage: establish a session, if so advertised: */ void @@ -2067,7 +2112,7 @@ establish_session (WockyConnector *self) priv->cancellable = NULL; } - complete_operation (self); + request_sm_enable (self); } } @@ -2166,7 +2211,7 @@ establish_session_recv_cb (GObject *source, priv->cancellable = NULL; } - complete_operation (self); + request_sm_enable (self); } break; diff --git a/wocky/wocky-namespaces.h b/wocky/wocky-namespaces.h index 758cffb7..d076e9d0 100644 --- a/wocky/wocky-namespaces.h +++ b/wocky/wocky-namespaces.h @@ -26,6 +26,9 @@ #define WOCKY_XMPP_NS_SASL_AUTH \ "urn:ietf:params:xml:ns:xmpp-sasl" +#define WOCKY_XMPP_NS_SM3 \ + "urn:xmpp:sm:3" + #define WOCKY_NS_DISCO_INFO \ "http://jabber.org/protocol/disco#info" diff --git a/wocky/wocky-stanza.c b/wocky/wocky-stanza.c index 2e5fd566..aee1214f 100644 --- a/wocky/wocky-stanza.c +++ b/wocky/wocky-stanza.c @@ -79,6 +79,8 @@ static StanzaTypeName type_names[NUM_WOCKY_STANZA_TYPE] = WOCKY_XMPP_NS_SASL_AUTH }, { WOCKY_STANZA_TYPE_STREAM_ERROR, "error", WOCKY_XMPP_NS_STREAM }, + { WOCKY_STANZA_TYPE_SM_ENABLED, "enabled", + WOCKY_XMPP_NS_SM3 }, { WOCKY_STANZA_TYPE_UNKNOWN, NULL, NULL }, }; diff --git a/wocky/wocky-stanza.h b/wocky/wocky-stanza.h index bcca6700..e8142999 100644 --- a/wocky/wocky-stanza.h +++ b/wocky/wocky-stanza.h @@ -104,6 +104,7 @@ typedef enum WOCKY_STANZA_TYPE_SUCCESS, WOCKY_STANZA_TYPE_FAILURE, WOCKY_STANZA_TYPE_STREAM_ERROR, + WOCKY_STANZA_TYPE_SM_ENABLED, WOCKY_STANZA_TYPE_UNKNOWN, /*< private >*/ NUM_WOCKY_STANZA_TYPE From 5b4a94d41d2c738e9414dc8f5f6e545997167078 Mon Sep 17 00:00:00 2001 From: "Ruslan N. Marchenko" Date: Fri, 16 Oct 2020 18:07:51 +0200 Subject: [PATCH 02/14] Replace whitespace with SM pings when SM is inabled --- wocky/wocky-c2s-porter.c | 26 ++++++++++++++++++++++---- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/wocky/wocky-c2s-porter.c b/wocky/wocky-c2s-porter.c index 43bedab6..381fe8a3 100644 --- a/wocky/wocky-c2s-porter.c +++ b/wocky/wocky-c2s-porter.c @@ -2259,11 +2259,17 @@ send_whitespace_ping_cb (GObject *source, WockyC2SPorter *self = WOCKY_C2S_PORTER (g_task_get_source_object (task)); WockyC2SPorterPrivate *priv = self->priv; GError *error = NULL; + gboolean ret; priv->sending_whitespace_ping = FALSE; - if (!wocky_xmpp_connection_send_whitespace_ping_finish ( - WOCKY_XMPP_CONNECTION (source), res, &error)) + if (priv->sm_enabled) + ret = wocky_xmpp_connection_send_stanza_finish ( + WOCKY_XMPP_CONNECTION (source), res, &error); + else + ret = wocky_xmpp_connection_send_whitespace_ping_finish ( + WOCKY_XMPP_CONNECTION (source), res, &error); + if (!ret) { g_task_return_error (task, g_error_copy (error)); @@ -2324,8 +2330,20 @@ wocky_c2s_porter_send_whitespace_ping_async (WockyC2SPorter *self, { priv->sending_whitespace_ping = TRUE; - wocky_xmpp_connection_send_whitespace_ping_async (priv->connection, - cancellable, send_whitespace_ping_cb, g_object_ref (task)); + /* when SM is enabled we need SM ping because we are doing selective + * acks hence need to catch-up the latest acks when idle. */ + if (priv->sm_enabled) + { + WockyStanza *r = wocky_stanza_new ("r", WOCKY_XMPP_NS_SM3); + wocky_xmpp_connection_send_stanza_async (priv->connection, r, + cancellable, send_whitespace_ping_cb, g_object_ref (task)); + g_object_unref (r); + } + else + { + wocky_xmpp_connection_send_whitespace_ping_async (priv->connection, + cancellable, send_whitespace_ping_cb, g_object_ref (task)); + } g_signal_emit_by_name (self, "sending", NULL); } From e4a6ea81412e204c938202f1c0ac84c54bdfc815 Mon Sep 17 00:00:00 2001 From: "Ruslan N. Marchenko" Date: Sun, 1 Nov 2020 15:48:30 +0100 Subject: [PATCH 03/14] Extend wocky API for SM resumption - Make auth_registry reusable by cleaning up the handler after successful authentication. - add _peeck_stanza_async call to wocky_xmpp_connection to be able to wait for certain stanza but do not consume it from the reader's queue. - add _resume_async call to wocky_connector which resets connector state, sets sm-resumption vector and launches connect_async - add _continue_async call to wocky_connector to be able to continue normal connection (bind/session) after sm-resume failure. --- wocky/wocky-auth-registry.c | 1 + wocky/wocky-connector.c | 251 +++++++++++++++++++++++++++++++++- wocky/wocky-connector.h | 15 ++ wocky/wocky-xmpp-connection.c | 97 ++++++++++--- wocky/wocky-xmpp-connection.h | 10 ++ 5 files changed, 350 insertions(+), 24 deletions(-) diff --git a/wocky/wocky-auth-registry.c b/wocky/wocky-auth-registry.c index 721dd64d..cce2046f 100644 --- a/wocky/wocky-auth-registry.c +++ b/wocky/wocky-auth-registry.c @@ -564,6 +564,7 @@ wocky_auth_registry_success_async_func (WockyAuthRegistry *self, g_task_return_boolean (task, TRUE); g_object_unref (task); + g_clear_object (&priv->handler); } diff --git a/wocky/wocky-connector.c b/wocky/wocky-connector.c index 5308cbc0..4c8839a9 100644 --- a/wocky/wocky-connector.c +++ b/wocky/wocky-connector.c @@ -176,6 +176,8 @@ static void xep77_signup_recv (GObject *source, GAsyncResult *result, gpointer data); +static void request_sm_resume (WockyConnector *self); + static void iq_bind_resource (WockyConnector *self); static void iq_bind_resource_sent_cb (GObject *source, GAsyncResult *result, @@ -293,6 +295,7 @@ struct _WockyConnectorPrivate GSocketConnection *sock; WockyXmppConnection *conn; WockyTLSHandler *tls_handler; + WockyStanza *resume; WockyAuthRegistry *auth_registry; @@ -1312,6 +1315,15 @@ xmpp_features_cb (GObject *source, goto out; } + /* we check for can_bind here as we're supposed to be able to proceed with + * bind should resume fail */ + if (priv->resume && can_bind && wocky_node_get_child_ns (node, "sm", + WOCKY_XMPP_NS_SM3) != NULL) + { + request_sm_resume (self); + goto out; + } + /* we MUST bind here http://www.ietf.org/rfc/rfc3920.txt */ if (can_bind) iq_bind_resource (self); @@ -2024,6 +2036,8 @@ iq_bind_resource_recv_cb (GObject *source, g_object_unref (reply); } +/* ************************************************************************* */ +/* XEP 0198 SM enable/resume calls */ /* Requesting SM is an opt-in and its failure is non-fatal. * Therefore we just request it here and handle result in * porter as part of normal event flow. @@ -2038,9 +2052,15 @@ request_sm_enable_cb (GObject *source, GError *error = NULL; if (!wocky_xmpp_connection_send_stanza_finish (connection, result, &error)) - DEBUG ("Failed to send enable nonza: %s", error->message); - else - g_object_set_data (G_OBJECT (connection), WOCKY_XMPP_NS_SM3, (void *) 1); + { + DEBUG ("Failed to send enable nonza: %s", error->message); + abort_connect_error (self, &error, "Failed to send 'enable' nonza"); + g_error_free (error); + return; + } + + g_object_set_data_full (G_OBJECT (connection), WOCKY_XMPP_NS_SM3, + g_object_ref (self), g_object_unref); complete_operation (self); } @@ -2058,7 +2078,7 @@ request_sm_enable (WockyConnector *self) WockyStanza *enable = wocky_stanza_new ("enable", WOCKY_XMPP_NS_SM3); WockyNode *en = wocky_stanza_get_top_node (enable); - wocky_node_set_attributes (en, "max", "600", "resume", "false", NULL); + wocky_node_set_attributes (en, "max", "600", "resume", "true", NULL); wocky_xmpp_connection_send_stanza_async (priv->conn, enable, priv->cancellable, request_sm_enable_cb, self); @@ -2069,6 +2089,131 @@ request_sm_enable (WockyConnector *self) complete_operation (self); } +static void +request_sm_resumed_cb (GObject *source, + GAsyncResult *result, + gpointer data) +{ + WockyXmppConnection *connection = WOCKY_XMPP_CONNECTION (source); + WockyConnector *self = data; + WockyConnectorPrivate *priv = wocky_connector_get_instance_private (self); + const WockyStanza *res; + WockyNode *rn; + GError *error = NULL; + + if ((res = wocky_xmpp_connection_peek_stanza_finish (connection, + result, &error)) == NULL) + { + DEBUG ("Failed to peek resumed nonza: %s", error->message); + abort_connect_error (self, &error, "Failed to peek 'resumed' nonza"); + g_error_free (error); + return; + } + + rn = wocky_stanza_get_top_node ((WockyStanza *) res); + if (wocky_node_has_ns (rn, WOCKY_XMPP_NS_SM3)) + { + if (g_strcmp0 (rn->name, "resumed")) + { + error = g_error_new_literal (WOCKY_XMPP_ERROR, + WOCKY_XMPP_ERROR_ITEM_NOT_FOUND, "Resumed session not found"); + g_task_return_error (priv->task, error); + } + else + { + g_object_set_data_full (G_OBJECT (connection), WOCKY_XMPP_NS_SM3, + g_object_ref (self), g_object_unref); + } + + if (priv->cancellable != NULL) + { + g_object_unref (priv->cancellable); + priv->cancellable = NULL; + } + + complete_operation (self); + return; + } + + error = g_error_new_literal (WOCKY_XMPP_ERROR, WOCKY_XMPP_ERROR_NOT_ACCEPTABLE, + "Connection reuse cannot continue without 'resumed' or 'failed' SM nonza"); + DEBUG ("Cannot continue with '%s': %s", rn->name, error->message); + + abort_connect (self, error); + g_error_free (error); +} + +static void +request_sm_resume_cb (GObject *source, + GAsyncResult *result, + gpointer data) +{ + WockyXmppConnection *connection = WOCKY_XMPP_CONNECTION (source); + WockyConnector *self = data; + WockyConnectorPrivate *priv = wocky_connector_get_instance_private (self); + GError *error = NULL; + + if (!wocky_xmpp_connection_send_stanza_finish (connection, result, &error)) + { + DEBUG ("Failed to send enable nonza: %s", error->message); + abort_connect_error (self, &error, "Failed to send 'resume' nonza"); + g_error_free (error); + return; + } + + wocky_xmpp_connection_peek_stanza_async (connection, priv->cancellable, + request_sm_resumed_cb, self); +} + +static void +request_sm_resume (WockyConnector *self) +{ + WockyConnectorPrivate *priv = wocky_connector_get_instance_private (self); + + wocky_xmpp_connection_send_stanza_async (priv->conn, priv->resume, + priv->cancellable, request_sm_resume_cb, self); + + g_clear_object (&priv->resume); +} + +static void +continue_sm_fail_cb (GObject *source, + GAsyncResult *result, + gpointer data) +{ + WockyXmppConnection *connection = WOCKY_XMPP_CONNECTION (source); + WockyConnector *self = data; + WockyStanza *res; + WockyNode *rn; + GError *error = NULL; + + if ((res = wocky_xmpp_connection_recv_stanza_finish (connection, + result, &error)) == NULL) + { + DEBUG ("Failed to receive SM nonza: %s", error->message); + abort_connect_error (self, &error, "Failed to receive 'failed' SM nonza"); + g_error_free (error); + return; + } + + rn = wocky_stanza_get_top_node ((WockyStanza *) res); + if (wocky_node_has_ns (rn, WOCKY_XMPP_NS_SM3) + && g_strcmp0 (rn->name, "failed") == 0) + { + g_object_unref (res); + /* continue normal connection process */ + iq_bind_resource (self); + return; + } + + g_error_new_literal (WOCKY_XMPP_ERROR, WOCKY_XMPP_ERROR_NOT_ACCEPTABLE, + "Connection reuse cannot continue without 'failed' SM nonza"); + DEBUG ("Cannot continue with '%s': %s", rn->name, error->message); + abort_connect (self, error); + g_error_free (error); + g_object_unref (res); +} + /* ************************************************************************* */ /* final stage: establish a session, if so advertised: */ void @@ -2277,6 +2422,30 @@ wocky_connector_connect_finish (WockyConnector *self, return self->priv->conn; } +/** + * wocky_connector_resume_finish: + * @self: a #WockyConnector instance. + * @res: a #GAsyncResult as passed to wocky_connector_resume_async() callback. + * @error: (%NULL to ignore) the #GError (if any) is stored here. + * + * Should be called by the callback passed to wocky_connector_resume_async() + * to complete async operation. + * + * Returns: a #WockyXmppConnection instance (success), or %NULL (failure). + */ +WockyXmppConnection * +wocky_connector_resume_finish (WockyConnector *self, + GAsyncResult *res, + GError **error) +{ + g_return_val_if_fail (g_task_is_valid (res, self), NULL); + + if (!g_task_propagate_boolean (G_TASK (res), error)) + return NULL; + + return self->priv->conn; +} + /** * wocky_connector_register_finish: * @self: a #WockyConnector instance. @@ -2438,6 +2607,80 @@ wocky_connector_connect_async (WockyConnector *self, cancellable, cb, user_data); } +/** + * wocky_connector_resume_async: + * @self: a #WockyConnector instance. + * @resume: (transfer full): a #WockyStanza to send for resumption + * @cancellable: (optional): a #GCancellable, or %NULL + * @cb: (optional): a #GAsyncReadyCallback to call when the operation completes. + * @user_data: (optional): a #gpointer to pass to the callback. + * + * Reconnect to the account/server specified by the @self. + * @cb should invoke wocky_connector_resume_finish(). + */ +void +wocky_connector_resume_async (WockyConnector *self, + WockyStanza *resume, + GCancellable *cancellable, + GAsyncReadyCallback cb, + gpointer user_data) +{ + WockyConnectorPrivate *priv = wocky_connector_get_instance_private (self); + + /* Reset to initial state as we are reusing the connector */ + g_clear_object (&priv->sock); + g_clear_object (&priv->conn); + g_clear_object (&priv->client); + g_clear_object (&priv->features); + g_clear_pointer (&priv->user, g_free); + g_clear_pointer (&priv->domain, g_free); + priv->encrypted = FALSE; + priv->connected = FALSE; + priv->authed = FALSE; + + priv->resume = resume; + connector_connect_async (self, wocky_connector_resume_async, + cancellable, cb, user_data); +} + +/** + * wocky_connector_continue_async: + * @self: a #WockyConnector instance. + * @cancellable: (optional): a #GCancellable, or %NULL + * @cb: (optional): a #GAsyncReadyCallback to call when the operation completes. + * @user_data: (optional): a #gpointer to pass to the callback. + * + * Continue connector operations (bind+session) on connector instance + * specified by the @self after SM resumption has failed without fatal + * error (item-not-found). + * + * @cb should invoke wocky_connector_connect_finish() - as it would for normal + * connection attempt. + */ +void +wocky_connector_continue_async (WockyConnector *self, + GCancellable *cancellable, + GAsyncReadyCallback cb, + gpointer user_data) +{ + WockyConnectorPrivate *priv = wocky_connector_get_instance_private (self); + + /* Ensure we're in a right state to continue */ + g_assert (priv->task == NULL); + g_assert (priv->cancellable == NULL); + g_assert (priv->conn != NULL); + g_assert (priv->connected); + g_assert (priv->authed); + + priv->task = g_task_new (G_OBJECT (self), cancellable, cb, user_data); + + if (cancellable != NULL) + priv->cancellable = g_object_ref (cancellable); + + /* Fetch `failed` nonza from the xmpp-connection */ + wocky_xmpp_connection_recv_stanza_async (priv->conn, cancellable, + continue_sm_fail_cb, self); +} /** * wocky_connector_unregister_async: diff --git a/wocky/wocky-connector.h b/wocky/wocky-connector.h index ef543d58..ebea20cf 100644 --- a/wocky/wocky-connector.h +++ b/wocky/wocky-connector.h @@ -198,6 +198,21 @@ gboolean wocky_connector_unregister_finish (WockyConnector *self, void wocky_connector_set_auth_registry (WockyConnector *self, WockyAuthRegistry *registry); +void wocky_connector_continue_async (WockyConnector *self, + GCancellable *cancellable, + GAsyncReadyCallback cb, + gpointer user_data); + +void wocky_connector_resume_async (WockyConnector *self, + WockyStanza *resume, + GCancellable *cancellable, + GAsyncReadyCallback cb, + gpointer user_data); + +WockyXmppConnection *wocky_connector_resume_finish (WockyConnector *self, + GAsyncResult *res, + GError **error); + G_END_DECLS #endif /* #ifndef __WOCKY_CONNECTOR_H__*/ diff --git a/wocky/wocky-xmpp-connection.c b/wocky/wocky-xmpp-connection.c index 28179359..77e27a57 100644 --- a/wocky/wocky-xmpp-connection.c +++ b/wocky/wocky-xmpp-connection.c @@ -808,30 +808,14 @@ wocky_xmpp_connection_recv_stanza_async (WockyXmppConnection *connection, return; } -/** - * wocky_xmpp_connection_recv_stanza_finish: - * @connection: a #WockyXmppConnection. - * @result: a GAsyncResult. - * @error: a GError location to store the error occuring, or NULL to ignore. - * - * Finishes receiving a stanza - * - * Returns: A #WockyStanza or NULL on error (unref after usage) - */ - -WockyStanza * -wocky_xmpp_connection_recv_stanza_finish (WockyXmppConnection *connection, - GAsyncResult *result, +static WockyStanza * +retrieve_stanza (WockyXmppConnection *connection, + WockyStanza *(*getter)(WockyXmppReader *), GError **error) { WockyXmppConnectionPrivate *priv; WockyStanza *stanza = NULL; - g_return_val_if_fail (g_task_is_valid (result, connection), NULL); - - if (!g_task_propagate_boolean (G_TASK (result), error)) - return NULL; - priv = connection->priv; switch (wocky_xmpp_reader_get_state (priv->reader)) @@ -840,7 +824,7 @@ wocky_xmpp_connection_recv_stanza_finish (WockyXmppConnection *connection, g_assert_not_reached (); break; case WOCKY_XMPP_READER_STATE_OPENED: - stanza = wocky_xmpp_reader_pop_stanza (priv->reader); + stanza = getter (priv->reader); break; case WOCKY_XMPP_READER_STATE_CLOSED: g_set_error_literal (error, WOCKY_XMPP_CONNECTION_ERROR, @@ -864,6 +848,79 @@ wocky_xmpp_connection_recv_stanza_finish (WockyXmppConnection *connection, return stanza; } +/** + * wocky_xmpp_connection_peek_stanza_async: + * @connection: a #WockyXmppConnection + * @cancellable: optional GCancellable object, NULL to ignore. + * @callback: callback to call when the request is satisfied. + * @user_data: the data to pass to callback function. + * + * Asynchronous receive a #WockyStanza. When the operation is + * finished @callback will be called. You can then call + * wocky_xmpp_connection_peek_stanza_finish() to get the result of + * the operation. + * + * Can only be called after wocky_xmpp_connection_peek_open_async has finished + * its operation. + */ +void +wocky_xmpp_connection_peek_stanza_async (WockyXmppConnection *connection, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + wocky_xmpp_connection_recv_stanza_async (connection, cancellable, callback, + user_data); +} + +/** + * wocky_xmpp_connection_peek_stanza_finish: + * @connection: a #WockyXmppConnection. + * @result: a GAsyncResult. + * @error: a GError location to store the error occuring, or NULL to ignore. + * + * Finishes receiving a stanza + * + * Returns: A #WockyStanza or NULL on error (transfer none) + */ + +const WockyStanza * +wocky_xmpp_connection_peek_stanza_finish (WockyXmppConnection *connection, + GAsyncResult *result, + GError **error) +{ + g_return_val_if_fail (g_task_is_valid (result, connection), NULL); + + if (!g_task_propagate_boolean (G_TASK (result), error)) + return NULL; + + return retrieve_stanza (connection, wocky_xmpp_reader_peek_stanza, error); +} + +/** + * wocky_xmpp_connection_recv_stanza_finish: + * @connection: a #WockyXmppConnection. + * @result: a GAsyncResult. + * @error: a GError location to store the error occuring, or NULL to ignore. + * + * Finishes receiving a stanza + * + * Returns: A #WockyStanza or NULL on error (unref after usage) + */ + +WockyStanza * +wocky_xmpp_connection_recv_stanza_finish (WockyXmppConnection *connection, + GAsyncResult *result, + GError **error) +{ + g_return_val_if_fail (g_task_is_valid (result, connection), NULL); + + if (!g_task_propagate_boolean (G_TASK (result), error)) + return NULL; + + return retrieve_stanza (connection, wocky_xmpp_reader_pop_stanza, error); +} + /** * wocky_xmpp_connection_send_close_async: * @connection: a #WockyXmppConnection. diff --git a/wocky/wocky-xmpp-connection.h b/wocky/wocky-xmpp-connection.h index edbd106d..6001c423 100644 --- a/wocky/wocky-xmpp-connection.h +++ b/wocky/wocky-xmpp-connection.h @@ -156,6 +156,16 @@ WockyStanza *wocky_xmpp_connection_recv_stanza_finish ( GAsyncResult *result, GError **error); +void wocky_xmpp_connection_peek_stanza_async (WockyXmppConnection *connection, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data); + +const WockyStanza *wocky_xmpp_connection_peek_stanza_finish ( + WockyXmppConnection *connection, + GAsyncResult *result, + GError **error); + void wocky_xmpp_connection_send_close_async (WockyXmppConnection *connection, GCancellable *cancellable, GAsyncReadyCallback callback, From 24c0a0681ffae501bccb7d0c78a97fc989a52e29 Mon Sep 17 00:00:00 2001 From: "Ruslan N. Marchenko" Date: Sun, 1 Nov 2020 16:13:14 +0100 Subject: [PATCH 04/14] Add connection resumtion to wocky_c2s_porter The resumption is triggered by non-XMPP error (eg. IO) or XMPP EOS To resume it discards current connection, raises `sending` flag (to prevent write attempts) and calls wocky_connector_resume_async on currently stashed connector. When resume_async completes with WockyXmppConnection it clears the `sending` flag and calls receive_stanza which now supposed to read `` nonza, move unacked_queue to sending_queue and flush the sending_queue (and as a side effect resume reading operations). sending_queue flush in turn resumes writing operations. Should reconnection (not resumption) fail it clears `sending` flag which together with absence of connection will trigger reconnection attempt on next heartbeat. Should resumption fail (not-found) the porter then resets porter state (discards queues) and calls _continue_async which completes normal (new) connection vector. If connector appears unset in any of the resumption steps - the step is abandoned - giving upper level control back over the resumption. --- wocky/wocky-c2s-porter.c | 295 ++++++++++++++++++++++++++++++++------- wocky/wocky-connector.c | 71 +++++----- 2 files changed, 280 insertions(+), 86 deletions(-) diff --git a/wocky/wocky-c2s-porter.c b/wocky/wocky-c2s-porter.c index 381fe8a3..7b5f9312 100644 --- a/wocky/wocky-c2s-porter.c +++ b/wocky/wocky-c2s-porter.c @@ -49,6 +49,7 @@ #include "wocky-utils.h" #include "wocky-namespaces.h" #include "wocky-contact-factory.h" +#include "wocky-connector.h" #define WOCKY_DEBUG_FLAG WOCKY_DEBUG_PORTER #include "wocky-debug-internal.h" @@ -80,7 +81,7 @@ struct _WockyC2SPorterPrivate /* Queue of sent WockyStanza's waiting for SM Ack */ GQueue *unacked_queue; GCancellable *receive_cancellable; - gboolean sending_whitespace_ping; + gboolean sending_blocked; GTask *close_task; gboolean waiting_to_close; @@ -113,6 +114,7 @@ struct _WockyC2SPorterPrivate gsize rcv_count; /* a count of stanzas we've received and processed */ gsize snt_count; /* a count of stanzas we've sent over the wire */ gsize snt_acked; /* a number of last acked stanzas */ + WockyConnector *connector; /* is set from connection when sm is discovered */ WockyXmppConnection *connection; }; @@ -474,8 +476,9 @@ wocky_c2s_porter_constructed (GObject *object) priv->sm_id = NULL; priv->sm_location = NULL; - priv->sm_enabled = (g_object_get_data (G_OBJECT (priv->connection), - WOCKY_XMPP_NS_SM3) != NULL); + priv->connector = g_object_steal_data (G_OBJECT (priv->connection), + WOCKY_XMPP_NS_SM3); + priv->sm_enabled = (priv->connector != NULL); wocky_porter_sm_reset (self); /* Register the IQ reply handler */ @@ -548,6 +551,9 @@ wocky_c2s_porter_dispose (GObject *object) g_clear_object (&(priv->force_close_task)); g_clear_object (&(priv->force_close_cancellable)); + priv->sm_enabled = FALSE; + wocky_porter_sm_reset (self); + if (G_OBJECT_CLASS (wocky_c2s_porter_parent_class)->dispose) G_OBJECT_CLASS (wocky_c2s_porter_parent_class)->dispose (object); } @@ -651,7 +657,7 @@ sending_in_progress (WockyC2SPorter *self) WockyC2SPorterPrivate *priv = self->priv; return g_queue_get_length (priv->sending_queue) > 0 || - priv->sending_whitespace_ping; + priv->sending_blocked; } static void @@ -774,7 +780,7 @@ wocky_c2s_porter_send_async (WockyPorter *porter, g_queue_push_tail (priv->sending_queue, elem); if (g_queue_get_length (priv->sending_queue) == 1 && - !priv->sending_whitespace_ping) + !priv->sending_blocked && priv->connection) { send_head_stanza (self); } @@ -973,15 +979,21 @@ wocky_porter_sm_reset (WockyC2SPorter *self) else priv->unacked_queue = g_queue_new (); } - else if (priv->unacked_queue) + else { - g_queue_free_full (priv->unacked_queue, g_object_unref); - priv->unacked_queue = NULL; + g_clear_object (&priv->connector); + + if (priv->unacked_queue) + { + g_queue_free_full (priv->unacked_queue, g_object_unref); + priv->unacked_queue = NULL; + } + priv->resumable = FALSE; } priv->snt_count = priv->snt_acked = priv->rcv_count = 0; - g_clear_pointer (&(priv->sm_id), g_free); - g_clear_pointer (&(priv->sm_location), g_free); + g_clear_pointer (&priv->sm_id, g_free); + g_clear_pointer (&priv->sm_location, g_free); priv->resumable = FALSE; priv->sm_timeout = 600; } @@ -992,6 +1004,49 @@ wocky_porter_sm_reset (WockyC2SPorter *self) : ((ULONG_MAX - start) + stop)) #define ACK_WINDOW_MAX 10 +static gboolean +wocky_porter_sm_handle_h (WockyC2SPorter *self, + WockyNode *node) +{ + WockyC2SPorterPrivate *priv = wocky_c2s_porter_get_instance_private (self); + const gchar *val = wocky_node_get_attribute (node, "h"); + gsize snt = 0; + + /* TODO below conditions are rather fatal, need to terminate the sream */ + if (val == NULL) + { + g_warning ("Missing 'h' attribute, we should better err this stream"); + return FALSE; + } + + snt = strtoul (val, NULL, 10); + if (snt == ULONG_MAX && errno == ERANGE) + { + g_warning ("Invalid number, cannot convert h value[%s] to gsize", val); + return FALSE; + } + + if (ACK_WINDOW (priv->snt_acked, snt) > ACK_WINDOW (priv->snt_acked, + priv->snt_count)) + { + g_warning ("Invalid acknowledgement %lu, must be between %lu and %lu", + snt, priv->snt_acked, priv->snt_count); + return FALSE; + } + + priv->snt_acked = snt; + /* now we can head-drop stanzas from unacked_queue till its length is + * (snt_count - snt_acked) */ + while (g_queue_get_length (priv->unacked_queue) + > ACK_WINDOW (priv->snt_count, priv->snt_acked)) + { + WockyStanza *s = g_queue_pop_head (priv->unacked_queue); + g_assert (s); + g_object_unref (s); + } + return TRUE; +} + static gboolean wocky_porter_sm_handle (WockyC2SPorter *self, WockyNode *node) @@ -1033,43 +1088,7 @@ wocky_porter_sm_handle (WockyC2SPorter *self, } else if (node->name[0] == 'a' && node->name[1] == '\0') { - const gchar *val = wocky_node_get_attribute (node, "h"); - gsize snt = 0; - - /* TODO below conditions are rather fatal, need to terminate the sream */ - if (val == NULL) - { - g_warning ("Missing 'h' attribute, we should better err this stream"); - return FALSE; - } - - snt = strtoul (val, NULL, 10); - if (snt == ULONG_MAX && errno == ERANGE) - { - g_warning ("Invalid number, cannot convert h value[%s] to gsize", val); - return FALSE; - } - - if (ACK_WINDOW (priv->snt_acked, snt) <= ACK_WINDOW (priv->snt_acked, - priv->snt_count)) - { - priv->snt_acked = snt; - } - else - { - g_warning ("Invalid acknowledgement %lu, must be between %lu and %lu", - snt, priv->snt_acked, priv->snt_count); - } - /* now we can head-drop stanzas from unacked_queue till its length is - * (snt_count - snt_acked) */ - while (g_queue_get_length (priv->unacked_queue) - > ACK_WINDOW (priv->snt_count, priv->snt_acked)) - { - WockyStanza *s = g_queue_pop_head (priv->unacked_queue); - g_assert (s); - g_object_unref (s); - } - return TRUE; + return wocky_porter_sm_handle_h (self, node); } else if (!g_strcmp0 (node->name, "enabled")) { @@ -1088,6 +1107,40 @@ wocky_porter_sm_handle (WockyC2SPorter *self, g_debug ("SM on connection %p is enabled", priv->connection); + return TRUE; + } + else if (!g_strcmp0 (node->name, "resumed")) + { + const gchar *val; + + if (G_UNLIKELY((val = wocky_node_get_attribute (node, "previd")) == NULL + || g_strcmp0 (val, priv->sm_id))) + { + /* this must not happen */ + GError err = {WOCKY_XMPP_STREAM_ERROR, + WOCKY_XMPP_STREAM_ERROR_INVALID_ID, + "Resumed SM-ID mismatch"}; + remote_connection_closed (self, &err); + return TRUE; + } + + if (G_UNLIKELY ((val = wocky_node_get_attribute (node, "previd")) == NULL + || !wocky_porter_sm_handle_h (self, node))) + { + /* this must not happen */ + GError err = {WOCKY_XMPP_STREAM_ERROR, + WOCKY_XMPP_STREAM_ERROR_BAD_FORMAT, + "Resumed nonza has wrong or missing 'h' attribute"}; + remote_connection_closed (self, &err); + return TRUE; + } + + /* passed all sanity checks and trimmed the queue, let's move its + * content to the sending one */ + while (!g_queue_is_empty (priv->unacked_queue)) + wocky_c2s_porter_send_async (WOCKY_PORTER (self), + g_queue_pop_head (priv->unacked_queue), NULL, NULL, NULL); + return TRUE; } else if (!g_strcmp0 (node->name, "failed")) @@ -1452,6 +1505,122 @@ connection_force_close_cb (GObject *source, g_object_unref (self); } +static void +connection_reconnected_cb (GObject *source, + GAsyncResult *res, + gpointer user_data) +{ + WockyC2SPorter *self = WOCKY_C2S_PORTER (user_data); + WockyC2SPorterPrivate *priv = wocky_c2s_porter_get_instance_private (self); + WockyConnector *ctr = WOCKY_CONNECTOR (source); + GError *err = NULL; + + if ((priv->connection = wocky_connector_connect_finish (ctr, + res, NULL, NULL, &err)) == NULL) + { + /* this is the last of them, no way out */ + remote_connection_closed (self, err); + g_error_free (err); + return; + } + priv->sending_blocked = FALSE; + /* We can only be here if connection supports SM and resume failed, + * hence turn SM back on and continue */ + priv->sm_enabled = TRUE; + priv->connector = g_object_ref (ctr); + wocky_porter_sm_reset (self); + /* We have reset the state already, just continue as if nothing happened */ + receive_stanza (self); +} + +static void +resume_connection_failed (WockyC2SPorter *self, + WockyConnector *ctr) +{ + WockyC2SPorterPrivate *priv = wocky_c2s_porter_get_instance_private (self); + + DEBUG ("Resuming session does not exist, proceed with %p[%p]", ctr, priv->connector); + + /* when we're managed our connector will likely be NULL */ + if (priv->connector != ctr) + return; + + /* Resume has returned failed:item-not-found, we can continue with the bind + * but first we need to reset the state */ + flush_unimportant_queue (self); + priv->sm_enabled = FALSE; + wocky_porter_sm_reset (self); + + /* by doing this we're risking to desync gabble-connection and its + * wocky-session from porter - we may have different resource after bind */ + wocky_connector_continue_async (ctr, priv->receive_cancellable, + connection_reconnected_cb, self); +} + +static void +resume_connection_cb (GObject *source, + GAsyncResult *res, + gpointer user_data) +{ + WockyC2SPorter *self = WOCKY_C2S_PORTER (user_data); + WockyC2SPorterPrivate *priv = wocky_c2s_porter_get_instance_private (self); + WockyConnector *ctr = WOCKY_CONNECTOR (source); + GError *err = NULL; + + if ((priv->connection = wocky_connector_resume_finish (ctr, res, &err)) == NULL) + { + DEBUG ("Resumption failed: %s", err->message); + terminate_sending_operations (self, err); + + /* We may have hard error (connection failed) and soft error (resumption + * failed). Former should cause retry or stop, later continue or bail + * depending on whether we're in managed or self-driven mode */ + if (g_error_matches (err, WOCKY_XMPP_ERROR, WOCKY_XMPP_ERROR_ITEM_NOT_FOUND)) + resume_connection_failed (self, ctr); + else if (priv->connector == NULL) + remote_connection_closed (self, err); + else + { + DEBUG ("Reconnecting on next heartbeat"); + priv->sending_blocked = FALSE; + } + + g_error_free (err); + } + else + { + /* We should now read `resumed` nonza and handle resumption resync. */ + DEBUG ("Connection resumed, try to resync"); + priv->sending_blocked = FALSE; + receive_stanza (self); + } +} + +static void +resume_connection (WockyC2SPorter *self) +{ + WockyC2SPorterPrivate *priv = wocky_c2s_porter_get_instance_private (self); + WockyStanza *resume = wocky_stanza_new ("resume", WOCKY_XMPP_NS_SM3); + WockyNode *rn = wocky_stanza_get_top_node (resume); + gchar *val = g_strdup_printf ("%lu", priv->rcv_count); + + g_assert (priv->connector); + g_assert (priv->sm_id); + + wocky_node_set_attribute (rn, "h", val); + wocky_node_set_attribute (rn, "previd", priv->sm_id); + + g_clear_object (&priv->connection); + priv->sending_blocked = TRUE; + DEBUG ("Attempting to resume sm %s:%s with connector %p", + priv->sm_id, val, priv->connector); + g_free (val); + + wocky_connector_resume_async (priv->connector, resume, + priv->receive_cancellable, resume_connection_cb, self); + g_object_unref (resume); +} + static void stanza_received_cb (GObject *source, GAsyncResult *res, @@ -1496,7 +1665,12 @@ stanza_received_cb (GObject *source, } else { - remote_connection_closed (self, error); + if (priv->resumable + && (error->domain != WOCKY_XMPP_CONNECTION_ERROR + || error->code == WOCKY_XMPP_CONNECTION_ERROR_EOS)) + resume_connection (self); + else + remote_connection_closed (self, error); } g_error_free (error); @@ -2222,6 +2396,20 @@ wocky_c2s_porter_force_close_async (WockyPorter *porter, priv->force_close_task = NULL; return; } + /* this may be NULL if SM resume/continue has failed */ + if (priv->connection == NULL) + { + GTask *t = priv->force_close_task; + + priv->force_close_task = NULL; + g_clear_object (&priv->force_close_cancellable); + g_clear_object (&priv->receive_cancellable); + priv->local_closed = TRUE; + + g_task_return_boolean (t, TRUE); + g_object_unref (t); + return; + } /* No need to wait, close connection right now */ DEBUG ("remote is already closed, close the XMPP connection"); g_object_ref (self); @@ -2261,7 +2449,7 @@ send_whitespace_ping_cb (GObject *source, GError *error = NULL; gboolean ret; - priv->sending_whitespace_ping = FALSE; + priv->sending_blocked = FALSE; if (priv->sm_enabled) ret = wocky_xmpp_connection_send_stanza_finish ( @@ -2322,13 +2510,18 @@ wocky_c2s_porter_send_whitespace_ping_async (WockyC2SPorter *self, g_task_return_new_error (task, WOCKY_PORTER_ERROR, WOCKY_PORTER_ERROR_CLOSING, "Porter is closing"); } - else if (sending_in_progress (self)) + else if (sending_in_progress (self) || priv->connection == NULL) { + /* With SM we may have no connection during resumption */ g_task_return_boolean (task, TRUE); + + /* But if connector is set we may need to re-try */ + if (priv->connection == NULL && !priv->sending_blocked) + resume_connection (self); } else { - priv->sending_whitespace_ping = TRUE; + priv->sending_blocked = TRUE; /* when SM is enabled we need SM ping because we are doing selective * acks hence need to catch-up the latest acks when idle. */ diff --git a/wocky/wocky-connector.c b/wocky/wocky-connector.c index 4c8839a9..e4ac9107 100644 --- a/wocky/wocky-connector.c +++ b/wocky/wocky-connector.c @@ -797,6 +797,7 @@ wocky_connector_dispose (GObject *object) g_clear_object (&priv->conn); g_clear_object (&priv->client); g_clear_object (&priv->sock); + g_clear_object (&priv->resume); g_clear_object (&priv->features); g_clear_object (&priv->auth_registry); g_clear_object (&priv->tls_handler); @@ -2100,47 +2101,42 @@ request_sm_resumed_cb (GObject *source, const WockyStanza *res; WockyNode *rn; GError *error = NULL; + GTask *t = priv->task; - if ((res = wocky_xmpp_connection_peek_stanza_finish (connection, - result, &error)) == NULL) - { - DEBUG ("Failed to peek resumed nonza: %s", error->message); - abort_connect_error (self, &error, "Failed to peek 'resumed' nonza"); - g_error_free (error); - return; - } - - rn = wocky_stanza_get_top_node ((WockyStanza *) res); - if (wocky_node_has_ns (rn, WOCKY_XMPP_NS_SM3)) + priv->task = NULL; + g_clear_object (&priv->cancellable); + res = wocky_xmpp_connection_peek_stanza_finish (connection, result, &error); + if (res != NULL) { - if (g_strcmp0 (rn->name, "resumed")) + rn = wocky_stanza_get_top_node ((WockyStanza *) res); + if (wocky_node_has_ns (rn, WOCKY_XMPP_NS_SM3)) { - error = g_error_new_literal (WOCKY_XMPP_ERROR, - WOCKY_XMPP_ERROR_ITEM_NOT_FOUND, "Resumed session not found"); - g_task_return_error (priv->task, error); + if (g_strcmp0 (rn->name, "resumed")) + { + error = g_error_new_literal (WOCKY_XMPP_ERROR, + WOCKY_XMPP_ERROR_ITEM_NOT_FOUND, "Resumed session not found"); + } + else + { + g_object_set_data_full (G_OBJECT (connection), WOCKY_XMPP_NS_SM3, + g_object_ref (self), g_object_unref); + } } else { - g_object_set_data_full (G_OBJECT (connection), WOCKY_XMPP_NS_SM3, - g_object_ref (self), g_object_unref); - } - - if (priv->cancellable != NULL) - { - g_object_unref (priv->cancellable); - priv->cancellable = NULL; + error = g_error_new_literal (WOCKY_XMPP_ERROR, WOCKY_XMPP_ERROR_NOT_ACCEPTABLE, + "Connection reuse cannot continue without 'resumed' or 'failed' SM nonza"); + DEBUG ("Cannot continue with '%s': %s", rn->name, error->message); } - - complete_operation (self); - return; } + else + DEBUG ("Failed to peek resumed nonza: %s", error->message); - error = g_error_new_literal (WOCKY_XMPP_ERROR, WOCKY_XMPP_ERROR_NOT_ACCEPTABLE, - "Connection reuse cannot continue without 'resumed' or 'failed' SM nonza"); - DEBUG ("Cannot continue with '%s': %s", rn->name, error->message); - - abort_connect (self, error); - g_error_free (error); + if (error == NULL) + g_task_return_boolean (t, TRUE); + else + g_task_return_error (t, error); + g_object_unref (t); } static void @@ -2413,13 +2409,15 @@ wocky_connector_connect_finish (WockyConnector *self, gchar **sid, GError **error) { + WockyXmppConnection *conn = self->priv->conn; g_return_val_if_fail (g_task_is_valid (res, self), NULL); if (!g_task_propagate_boolean (G_TASK (res), error)) return NULL; connector_propagate_jid_and_sid (self, jid, sid); - return self->priv->conn; + self->priv->conn = NULL; + return conn; } /** @@ -2438,12 +2436,14 @@ wocky_connector_resume_finish (WockyConnector *self, GAsyncResult *res, GError **error) { + WockyXmppConnection *conn = self->priv->conn; g_return_val_if_fail (g_task_is_valid (res, self), NULL); if (!g_task_propagate_boolean (G_TASK (res), error)) return NULL; - return self->priv->conn; + self->priv->conn = NULL; + return conn; } /** @@ -2631,6 +2631,7 @@ wocky_connector_resume_async (WockyConnector *self, g_clear_object (&priv->sock); g_clear_object (&priv->conn); g_clear_object (&priv->client); + g_clear_object (&priv->resume); g_clear_object (&priv->features); g_clear_pointer (&priv->user, g_free); g_clear_pointer (&priv->domain, g_free); @@ -2638,7 +2639,7 @@ wocky_connector_resume_async (WockyConnector *self, priv->connected = FALSE; priv->authed = FALSE; - priv->resume = resume; + priv->resume = g_object_ref (resume); connector_connect_async (self, wocky_connector_resume_async, cancellable, cb, user_data); } From c2fc4a4a720f4fb4e598368bbb8621efbef17057 Mon Sep 17 00:00:00 2001 From: "Ruslan N. Marchenko" Date: Sat, 14 Nov 2020 07:32:48 +0100 Subject: [PATCH 05/14] Add controls to manage resumption and reconnection vector Add new signals for each state of resume vector: resuming, resumed, resume-done, resume-failed, reconnected; add new api call wocky_c2s_porter_resume. Signal `resuming` allows handler to prevent automatic resumption vector via connector by returning FALSE. This signal emits resume stanza to allow handler to start resume vector over wocky-connector. Once done - should call wocky_c2s_porter_resume to capture resumed nonza, resync and continue. If left to auto-resume the next checkpoint is resume-fail signal - a recoverable soft-fail where handler may stop auto-recovery by returning FALSE. Handler then need to call wocky_connector_reconnect and create a new session/porter and discard (free) existing. If left to auto-reconnect - the emission of the `reconnected` signal will carry the new connection SID, also change of the full-jid andconnection properties will be `notify::`ed. --- wocky/wocky-c2s-porter.c | 448 ++++++++++++++++++++++----------------- wocky/wocky-c2s-porter.h | 3 + wocky/wocky-porter.c | 77 +++++++ 3 files changed, 336 insertions(+), 192 deletions(-) diff --git a/wocky/wocky-c2s-porter.c b/wocky/wocky-c2s-porter.c index 7b5f9312..73bff336 100644 --- a/wocky/wocky-c2s-porter.c +++ b/wocky/wocky-c2s-porter.c @@ -114,6 +114,7 @@ struct _WockyC2SPorterPrivate gsize rcv_count; /* a count of stanzas we've received and processed */ gsize snt_count; /* a count of stanzas we've sent over the wire */ gsize snt_acked; /* a number of last acked stanzas */ + gint sm_reqs; /* a number of unanswered sm requests */ WockyConnector *connector; /* is set from connection when sm is discovered */ WockyXmppConnection *connection; @@ -967,194 +968,6 @@ handle_iq_reply (WockyPorter *porter, return ret; } -static void -wocky_porter_sm_reset (WockyC2SPorter *self) -{ - WockyC2SPorterPrivate *priv = wocky_c2s_porter_get_instance_private (self); - - if (priv->sm_enabled) - { - if (G_UNLIKELY (priv->unacked_queue)) - g_queue_clear_full (priv->unacked_queue, g_object_unref); - else - priv->unacked_queue = g_queue_new (); - } - else - { - g_clear_object (&priv->connector); - - if (priv->unacked_queue) - { - g_queue_free_full (priv->unacked_queue, g_object_unref); - priv->unacked_queue = NULL; - } - priv->resumable = FALSE; - } - - priv->snt_count = priv->snt_acked = priv->rcv_count = 0; - g_clear_pointer (&priv->sm_id, g_free); - g_clear_pointer (&priv->sm_location, g_free); - priv->resumable = FALSE; - priv->sm_timeout = 600; -} - -/* We need to consider normal monotonic increase and uint32 wrap conditions */ -#define ACK_WINDOW(start, stop) ((start <= stop) \ - ? (stop - start) \ - : ((ULONG_MAX - start) + stop)) -#define ACK_WINDOW_MAX 10 - -static gboolean -wocky_porter_sm_handle_h (WockyC2SPorter *self, - WockyNode *node) -{ - WockyC2SPorterPrivate *priv = wocky_c2s_porter_get_instance_private (self); - const gchar *val = wocky_node_get_attribute (node, "h"); - gsize snt = 0; - - /* TODO below conditions are rather fatal, need to terminate the sream */ - if (val == NULL) - { - g_warning ("Missing 'h' attribute, we should better err this stream"); - return FALSE; - } - - snt = strtoul (val, NULL, 10); - if (snt == ULONG_MAX && errno == ERANGE) - { - g_warning ("Invalid number, cannot convert h value[%s] to gsize", val); - return FALSE; - } - - if (ACK_WINDOW (priv->snt_acked, snt) > ACK_WINDOW (priv->snt_acked, - priv->snt_count)) - { - g_warning ("Invalid acknowledgement %lu, must be between %lu and %lu", - snt, priv->snt_acked, priv->snt_count); - return FALSE; - } - - priv->snt_acked = snt; - /* now we can head-drop stanzas from unacked_queue till its length is - * (snt_count - snt_acked) */ - while (g_queue_get_length (priv->unacked_queue) - > ACK_WINDOW (priv->snt_count, priv->snt_acked)) - { - WockyStanza *s = g_queue_pop_head (priv->unacked_queue); - g_assert (s); - g_object_unref (s); - } - return TRUE; -} - -static gboolean -wocky_porter_sm_handle (WockyC2SPorter *self, - WockyNode *node) -{ - WockyC2SPorterPrivate *priv = wocky_c2s_porter_get_instance_private (self); - - if (G_UNLIKELY(!priv->sm_enabled)) - { - g_warning ("Received SM nonza %s while SM is disabled", node->name); - return FALSE; - } - - if (node == NULL) - { - /* a probe for request - fire one if we have breached the max or fire an - * early notice when we are in the middle of it */ - if (ACK_WINDOW (priv->snt_acked, priv->snt_count) == ACK_WINDOW_MAX/2 - || ACK_WINDOW (priv->snt_acked, priv->snt_count) > ACK_WINDOW_MAX) - { - WockyStanza *r = wocky_stanza_new ("r", WOCKY_XMPP_NS_SM3); - wocky_porter_send (WOCKY_PORTER (self), r); - g_object_unref (r); - return TRUE; - } - } - else if (node->name[0] == 'r' && node->name[1] == '\0') - { - WockyStanza *a = wocky_stanza_new ("a", WOCKY_XMPP_NS_SM3); - WockyNode *an = wocky_stanza_get_top_node (a); - gchar *val = g_strdup_printf ("%lu", priv->rcv_count); - - wocky_node_set_attribute (an, "h", val); - g_free (val); - - wocky_porter_send (WOCKY_PORTER (self), a); - g_object_unref (a); - - return TRUE; - } - else if (node->name[0] == 'a' && node->name[1] == '\0') - { - return wocky_porter_sm_handle_h (self, node); - } - else if (!g_strcmp0 (node->name, "enabled")) - { - const gchar *val; - if ((val = wocky_node_get_attribute (node, "id")) != NULL) - priv->sm_id = g_strdup (val); - - if ((val = wocky_node_get_attribute (node, "max")) != NULL) - priv->sm_timeout = strtoul (val, NULL, 10); - - if ((val = wocky_node_get_attribute (node, "resume")) != NULL) - priv->resumable = (g_strcmp0 (val, "true") == 0); - - if ((val = wocky_node_get_attribute (node, "location")) != NULL) - priv->sm_location = g_strdup (val); - - g_debug ("SM on connection %p is enabled", priv->connection); - - return TRUE; - } - else if (!g_strcmp0 (node->name, "resumed")) - { - const gchar *val; - - if (G_UNLIKELY((val = wocky_node_get_attribute (node, "previd")) == NULL - || g_strcmp0 (val, priv->sm_id))) - { - /* this must not happen */ - GError err = {WOCKY_XMPP_STREAM_ERROR, - WOCKY_XMPP_STREAM_ERROR_INVALID_ID, - "Resumed SM-ID mismatch"}; - remote_connection_closed (self, &err); - return TRUE; - } - - if (G_UNLIKELY ((val = wocky_node_get_attribute (node, "previd")) == NULL - || !wocky_porter_sm_handle_h (self, node))) - { - /* this must not happen */ - GError err = {WOCKY_XMPP_STREAM_ERROR, - WOCKY_XMPP_STREAM_ERROR_BAD_FORMAT, - "Resumed nonza has wrong or missing 'h' attribute"}; - remote_connection_closed (self, &err); - return TRUE; - } - - /* passed all sanity checks and trimmed the queue, let's move its - * content to the sending one */ - while (!g_queue_is_empty (priv->unacked_queue)) - wocky_c2s_porter_send_async (WOCKY_PORTER (self), - g_queue_pop_head (priv->unacked_queue), NULL, NULL, NULL); - - return TRUE; - } - else if (!g_strcmp0 (node->name, "failed")) - { - g_debug ("SM on connection %p has failed", priv->connection); - priv->sm_enabled = FALSE; - wocky_porter_sm_reset (self); - } - else - g_warning ("Unknown SM nonza '%s' received", node->name); - - return FALSE; -} - static void handle_stanza (WockyC2SPorter *self, WockyStanza *stanza) @@ -1505,6 +1318,21 @@ connection_force_close_cb (GObject *source, g_object_unref (self); } +void +wocky_c2s_porter_resume (WockyC2SPorter *self, + WockyXmppConnection *connection) +{ + WockyC2SPorterPrivate *priv = wocky_c2s_porter_get_instance_private (self); + + g_assert (priv->receive_cancellable); + g_assert (connection); + + priv->connection = connection; + priv->sending_blocked = FALSE; + receive_stanza (self); + g_signal_emit_by_name (self, "resumed"); +} + static void connection_reconnected_cb (GObject *source, GAsyncResult *res, @@ -1513,10 +1341,11 @@ connection_reconnected_cb (GObject *source, WockyC2SPorter *self = WOCKY_C2S_PORTER (user_data); WockyC2SPorterPrivate *priv = wocky_c2s_porter_get_instance_private (self); WockyConnector *ctr = WOCKY_CONNECTOR (source); + g_autofree gchar *sid = NULL; GError *err = NULL; if ((priv->connection = wocky_connector_connect_finish (ctr, - res, NULL, NULL, &err)) == NULL) + res, &priv->full_jid, &sid, &err)) == NULL) { /* this is the last of them, no way out */ remote_connection_closed (self, err); @@ -1530,6 +1359,9 @@ connection_reconnected_cb (GObject *source, priv->connector = g_object_ref (ctr); wocky_porter_sm_reset (self); /* We have reset the state already, just continue as if nothing happened */ + g_object_notify (G_OBJECT (self), "connection"); + g_object_notify (G_OBJECT (self), "full-jid"); + g_signal_emit_by_name (self, "reconnected", priv->full_jid, sid); receive_stanza (self); } @@ -1538,6 +1370,14 @@ resume_connection_failed (WockyC2SPorter *self, WockyConnector *ctr) { WockyC2SPorterPrivate *priv = wocky_c2s_porter_get_instance_private (self); + gboolean ret = FALSE; + + g_signal_emit_by_name (self, "resume-failed", &ret); + if (!ret) + { + DEBUG ("Got stop signal, handing over the reconnection vector"); + return; + } DEBUG ("Resuming session does not exist, proceed with %p[%p]", ctr, priv->connector); @@ -1591,8 +1431,7 @@ resume_connection_cb (GObject *source, { /* We should now read `resumed` nonza and handle resumption resync. */ DEBUG ("Connection resumed, try to resync"); - priv->sending_blocked = FALSE; - receive_stanza (self); + wocky_c2s_porter_resume (self, priv->connection); } } @@ -1603,6 +1442,7 @@ resume_connection (WockyC2SPorter *self) WockyStanza *resume = wocky_stanza_new ("resume", WOCKY_XMPP_NS_SM3); WockyNode *rn = wocky_stanza_get_top_node (resume); gchar *val = g_strdup_printf ("%lu", priv->rcv_count); + gboolean ret = TRUE; g_assert (priv->connector); g_assert (priv->sm_id); @@ -1611,16 +1451,240 @@ resume_connection (WockyC2SPorter *self) wocky_node_set_attribute (rn, "previd", priv->sm_id); g_clear_object (&priv->connection); + g_object_notify (G_OBJECT (self), "connection"); priv->sending_blocked = TRUE; DEBUG ("Attempting to resume sm %s:%s with connector %p", priv->sm_id, val, priv->connector); g_free (val); - wocky_connector_resume_async (priv->connector, resume, + g_signal_emit_by_name (self, "resuming", resume, &ret); + if (ret) + wocky_connector_resume_async (priv->connector, resume, priv->receive_cancellable, resume_connection_cb, self); + else + DEBUG ("Got stop signal, skipping auto-resume"); + g_object_unref (resume); } +static void +wocky_porter_resume_done (GObject *source, + GAsyncResult *res, + gpointer user_data) +{ + WockyC2SPorter *self = WOCKY_C2S_PORTER (source); + GError *error = NULL; + + if (wocky_c2s_porter_send_finish (WOCKY_PORTER (self), res, &error)) + return g_signal_emit_by_name (G_OBJECT (self), "resume-done"); + + if (error->domain != WOCKY_XMPP_CONNECTION_ERROR + || error->code == WOCKY_XMPP_CONNECTION_ERROR_EOS) + resume_connection (self); + else + remote_connection_closed (self, error); + g_error_free (error); +} + +static void +wocky_porter_sm_reset (WockyC2SPorter *self) +{ + WockyC2SPorterPrivate *priv = wocky_c2s_porter_get_instance_private (self); + + if (priv->sm_enabled) + { + if (G_UNLIKELY (priv->unacked_queue)) + g_queue_free_full (priv->unacked_queue, g_object_unref); + priv->unacked_queue = g_queue_new (); + } + else + { + g_clear_object (&priv->connector); + + if (priv->unacked_queue) + { + g_queue_free_full (priv->unacked_queue, g_object_unref); + priv->unacked_queue = NULL; + } + priv->resumable = FALSE; + } + + priv->snt_count = priv->snt_acked = priv->rcv_count = 0; + g_clear_pointer (&priv->sm_id, g_free); + g_clear_pointer (&priv->sm_location, g_free); + priv->resumable = FALSE; + priv->sm_timeout = 600; +} + +/* We need to consider normal monotonic increase and uint32 wrap conditions */ +#define ACK_WINDOW(start, stop) ((start <= stop) \ + ? (stop - start) \ + : ((ULONG_MAX - start) + stop)) +#define ACK_WINDOW_MAX 10 + +static gboolean +wocky_porter_sm_handle_h (WockyC2SPorter *self, + WockyNode *node) +{ + WockyC2SPorterPrivate *priv = wocky_c2s_porter_get_instance_private (self); + const gchar *val = wocky_node_get_attribute (node, "h"); + gsize snt = 0; + + /* TODO below conditions are rather fatal, need to terminate the sream */ + if (val == NULL) + { + g_warning ("Missing 'h' attribute, we should better err this stream"); + return FALSE; + } + + snt = strtoul (val, NULL, 10); + if (snt == ULONG_MAX && errno == ERANGE) + { + g_warning ("Invalid number, cannot convert h value[%s] to gsize", val); + return FALSE; + } + + if (ACK_WINDOW (priv->snt_acked, snt) > ACK_WINDOW (priv->snt_acked, + priv->snt_count)) + { + g_warning ("Invalid acknowledgement %lu, must be between %lu and %lu", + snt, priv->snt_acked, priv->snt_count); + return FALSE; + } + + priv->snt_acked = snt; + /* now we can head-drop stanzas from unacked_queue till its length is + * (snt_count - snt_acked) */ + while (g_queue_get_length (priv->unacked_queue) + > ACK_WINDOW (priv->snt_count, priv->snt_acked)) + { + WockyStanza *s = g_queue_pop_head (priv->unacked_queue); + g_assert (s); + g_object_unref (s); + } + return TRUE; +} + +static gboolean +wocky_porter_sm_handle (WockyC2SPorter *self, + WockyNode *node) +{ + WockyC2SPorterPrivate *priv = wocky_c2s_porter_get_instance_private (self); + + if (G_UNLIKELY(!priv->sm_enabled)) + { + g_warning ("Received SM nonza %s while SM is disabled", node->name); + return FALSE; + } + + if (node == NULL) + { + /* a probe for request - fire one if we have breached the max or fire an + * early notice when we are in the middle of it */ + if (ACK_WINDOW (priv->snt_acked, priv->snt_count) == ACK_WINDOW_MAX/2 + || ACK_WINDOW (priv->snt_acked, priv->snt_count) > ACK_WINDOW_MAX) + { + WockyStanza *r = wocky_stanza_new ("r", WOCKY_XMPP_NS_SM3); + if (priv->sm_reqs == 0) + { + wocky_porter_send (WOCKY_PORTER (self), r); + priv->sm_reqs++; + } + g_object_unref (r); + return TRUE; + } + } + else if (node->name[0] == 'r' && node->name[1] == '\0') + { + WockyStanza *a = wocky_stanza_new ("a", WOCKY_XMPP_NS_SM3); + WockyNode *an = wocky_stanza_get_top_node (a); + gchar *val = g_strdup_printf ("%lu", priv->rcv_count); + + wocky_node_set_attribute (an, "h", val); + g_free (val); + + wocky_porter_send (WOCKY_PORTER (self), a); + g_object_unref (a); + + return TRUE; + } + else if (node->name[0] == 'a' && node->name[1] == '\0') + { + if (priv->sm_reqs > 0) + priv->sm_reqs--; + return wocky_porter_sm_handle_h (self, node); + } + else if (!g_strcmp0 (node->name, "enabled")) + { + const gchar *val; + if ((val = wocky_node_get_attribute (node, "id")) != NULL) + priv->sm_id = g_strdup (val); + + if ((val = wocky_node_get_attribute (node, "max")) != NULL) + priv->sm_timeout = strtoul (val, NULL, 10); + + if ((val = wocky_node_get_attribute (node, "resume")) != NULL) + priv->resumable = (g_strcmp0 (val, "true") == 0); + + if ((val = wocky_node_get_attribute (node, "location")) != NULL) + priv->sm_location = g_strdup (val); + + g_debug ("SM on connection %p is enabled", priv->connection); + + return TRUE; + } + else if (!g_strcmp0 (node->name, "resumed")) + { + const gchar *val; + WockyStanza *smr; + + if (G_UNLIKELY((val = wocky_node_get_attribute (node, "previd")) == NULL + || g_strcmp0 (val, priv->sm_id))) + { + /* this must not happen */ + GError err = {WOCKY_XMPP_STREAM_ERROR, + WOCKY_XMPP_STREAM_ERROR_INVALID_ID, + "Resumed SM-ID mismatch"}; + remote_connection_closed (self, &err); + return TRUE; + } + + if (G_UNLIKELY ((val = wocky_node_get_attribute (node, "previd")) == NULL + || !wocky_porter_sm_handle_h (self, node))) + { + /* this must not happen */ + GError err = {WOCKY_XMPP_STREAM_ERROR, + WOCKY_XMPP_STREAM_ERROR_BAD_FORMAT, + "Resumed nonza has wrong or missing 'h' attribute"}; + remote_connection_closed (self, &err); + return TRUE; + } + + /* passed all sanity checks and trimmed the queue, let's move its + * content to the sending one */ + while (!g_queue_is_empty (priv->unacked_queue)) + wocky_c2s_porter_send_async (WOCKY_PORTER (self), + g_queue_pop_head (priv->unacked_queue), NULL, NULL, NULL); + + /* Add `r` to the end of the queue and track its progress */ + smr = wocky_stanza_new ("r", WOCKY_XMPP_NS_SM3); + wocky_c2s_porter_send_async (WOCKY_PORTER (self), smr, + priv->receive_cancellable, wocky_porter_resume_done, NULL); + g_object_unref (smr); + return TRUE; + } + else if (!g_strcmp0 (node->name, "failed")) + { + g_debug ("SM on connection %p has failed", priv->connection); + priv->sm_enabled = FALSE; + wocky_porter_sm_reset (self); + } + else + g_warning ("Unknown SM nonza '%s' received", node->name); + + return FALSE; +} + static void stanza_received_cb (GObject *source, GAsyncResult *res, diff --git a/wocky/wocky-c2s-porter.h b/wocky/wocky-c2s-porter.h index 83f7638f..c661d29b 100644 --- a/wocky/wocky-c2s-porter.h +++ b/wocky/wocky-c2s-porter.h @@ -122,6 +122,9 @@ guint wocky_c2s_porter_register_handler_from_server ( void wocky_c2s_porter_enable_power_saving_mode (WockyC2SPorter *porter, gboolean enable); +void wocky_c2s_porter_resume (WockyC2SPorter *self, + WockyXmppConnection *connection); + G_END_DECLS #endif /* #ifndef __WOCKY_C2S_PORTER_H__*/ diff --git a/wocky/wocky-porter.c b/wocky/wocky-porter.c index 4161fe25..6b4bf476 100644 --- a/wocky/wocky-porter.c +++ b/wocky/wocky-porter.c @@ -133,6 +133,83 @@ wocky_porter_default_init (WockyPorterInterface *iface) g_cclosure_marshal_VOID__OBJECT, G_TYPE_NONE, 1, WOCKY_TYPE_STANZA); + /** + * WockyPorter::resuming: + * @porter: the object on which the signal is emitted + * + * The ::resuming signal is emitted when the #WockyPorter detects broken + * XMPP connection and start resumption vector of the attached connector. + * After this signal all outgoing stanzas will be queued and might be + * discarded if XEP-0198 resumption fails. The signal is emitted after + * XMPP connection is discarded from the porter (which also sends + * notify::connection for changed property) but before connector_resume. + * The signal passes resume stanza from the porter which needs to be + * passed to the wocky_connector_resume_async call should signal handler + * decide to take over the resumption control flow by returning FALSE. + */ + g_signal_new ("resuming", iface_type, + G_SIGNAL_RUN_LAST, 0, NULL, NULL, + NULL, G_TYPE_BOOLEAN, 1, WOCKY_TYPE_STANZA); + + /** + * WockyPorter::resumed: + * @porter: the object on which the signal is emitted + * + * The ::resumed signal is emitted when the #WockyPorter resumed the + * XMPP connection, returned from connector and processed XEP-0198 + * `resumed` nonza. This signal may be used to update UI that connection + * is fully available for send/receive now. + */ + g_signal_new ("resumed", iface_type, + G_SIGNAL_RUN_LAST, 0, NULL, NULL, + g_cclosure_marshal_VOID__VOID, + G_TYPE_NONE, 0); + + /** + * WockyPorter::resume-done: + * @porter: the object on which the signal is emitted + * + * The ::resume-done signal is emitted when the #WockyPorter finished + * flushing sending queues after XMPP connection is resumed. + * This signal may be used to reset sending timeouts. + */ + g_signal_new ("resume-done", iface_type, + G_SIGNAL_RUN_LAST, 0, NULL, NULL, + g_cclosure_marshal_VOID__VOID, + G_TYPE_NONE, 0); + + /** + * WockyPorter::resume-failed: + * @porter: the object on which the signal is emitted + * + * The ::resume-failed signal is emitted when the #WockyPorter returns + * from connector with soft-error - the connection is established but + * the session is not found on the server. As per XEP-0198 the client + * may proceed with bind at this stage. If signal returns with %FALSE + * the porter simply returns from this error without action. You would + * need to call wocky_connector_reconnect_async manually to proceed with + * the bind (and obtain XMPP connection). If signal returns with %TRUE + * then #WockyPorter calls the _reconnect call and proceeds with bind + * automatically. + * Note: In case of hard-fail #WockyPorter will continue trying to + * reconnect with each heartbeat. + */ + g_signal_new ("resume-failed", iface_type, + G_SIGNAL_RUN_LAST, 0, NULL, NULL, + NULL, G_TYPE_BOOLEAN, 0); + + /** + * WockyPorter::reconnected: + * @porter: the object on which the signal is emitted + * + * The ::reconnected signal is emitted when the #WockyPorter completes + * automatic reconnection after resumption soft-fail. The signal carries + * new SID and JID of the re-bound session. + */ + g_signal_new ("reconnected", iface_type, + G_SIGNAL_RUN_LAST, 0, NULL, NULL, + NULL, G_TYPE_NONE, 2, G_TYPE_STRING, G_TYPE_STRING); + g_once_init_leave (&initialization_value, 1); } } From b1aa1f9b849740aa4beb86f85832892099c078c6 Mon Sep 17 00:00:00 2001 From: "Ruslan N. Marchenko" Date: Sat, 14 Nov 2020 07:48:41 +0100 Subject: [PATCH 06/14] Track the change of porter properties from session --- wocky/wocky-session.c | 39 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) diff --git a/wocky/wocky-session.c b/wocky/wocky-session.c index d3dc08bf..72594d25 100644 --- a/wocky/wocky-session.c +++ b/wocky/wocky-session.c @@ -144,6 +144,36 @@ wocky_session_get_property (GObject *object, } } +static void +wocky_session_full_jid_notify (GObject *source, + GParamSpec *pspec, + gpointer user_data) +{ + WockySession *self = WOCKY_SESSION (user_data); + WockySessionPrivate *priv = wocky_session_get_instance_private (self); + + g_clear_pointer (&priv->full_jid, g_free); + g_object_get (source, + "full-jid", &priv->full_jid, + NULL); + g_object_notify (G_OBJECT (self), "full-jid"); +} + +static void +wocky_session_connection_notify (GObject *source, + GParamSpec *pspec, + gpointer user_data) +{ + WockySession *self = WOCKY_SESSION (user_data); + WockySessionPrivate *priv = wocky_session_get_instance_private (self); + + g_clear_object (&priv->connection); + g_object_get (source, + "connection", &priv->connection, + NULL); + g_object_notify (G_OBJECT (self), "connection"); +} + static void wocky_session_constructed (GObject *object) { @@ -151,7 +181,13 @@ wocky_session_constructed (GObject *object) WockySessionPrivate *priv = self->priv; if (priv->connection != NULL) - priv->porter = wocky_c2s_porter_new (priv->connection, priv->full_jid); + { + priv->porter = wocky_c2s_porter_new (priv->connection, priv->full_jid); + g_signal_connect (priv->porter, "notify::full-jid", + G_CALLBACK (wocky_session_full_jid_notify), self); + g_signal_connect (priv->porter, "notify::connection", + G_CALLBACK (wocky_session_connection_notify), self); + } else priv->porter = wocky_meta_porter_new (priv->full_jid, priv->contact_factory); } @@ -173,6 +209,7 @@ wocky_session_dispose (GObject *object) priv->connection = NULL; } + g_signal_handlers_disconnect_by_data (priv->porter, self); g_object_unref (priv->porter); g_object_unref (priv->contact_factory); From 5eab976f6886776baf0e7f0a507a6cfbc95967cf Mon Sep 17 00:00:00 2001 From: "Ruslan N. Marchenko" Date: Tue, 17 Nov 2020 23:16:57 +0100 Subject: [PATCH 07/14] Extend wocky_c2s_porter api to expose SM internals --- wocky/wocky-c2s-porter.c | 169 +++++++++++++++++++++++++-------------- wocky/wocky-c2s-porter.h | 14 ++++ 2 files changed, 124 insertions(+), 59 deletions(-) diff --git a/wocky/wocky-c2s-porter.c b/wocky/wocky-c2s-porter.c index 73bff336..95f41c81 100644 --- a/wocky/wocky-c2s-porter.c +++ b/wocky/wocky-c2s-porter.c @@ -63,6 +63,7 @@ enum PROP_FULL_JID, PROP_BARE_JID, PROP_RESOURCE, + PROP_CONNECTOR }; /* private structure */ @@ -106,15 +107,7 @@ struct _WockyC2SPorterPrivate /* List of (owned WockyStanza *) */ GQueue queueable_stanza_patterns; - gboolean sm_enabled; /* track sent/rcvd stanzas (not nonzas!) */ - gboolean resumable; /* server confirmed it can resume the stream */ - gchar *sm_id; /* a unique stream identifier for resumption */ - gchar *sm_location; /* preferred server address for resumption */ - gsize sm_timeout; /* a time within which we can try to resume the stream */ - gsize rcv_count; /* a count of stanzas we've received and processed */ - gsize snt_count; /* a count of stanzas we've sent over the wire */ - gsize snt_acked; /* a number of last acked stanzas */ - gint sm_reqs; /* a number of unanswered sm requests */ + WockyPorterSmCtx *sm; WockyConnector *connector; /* is set from connection when sm is discovered */ WockyXmppConnection *connection; @@ -404,6 +397,11 @@ wocky_c2s_porter_set_property (GObject *object, g_free (node); break; + case PROP_CONNECTOR: + g_clear_object (&priv->connector); + priv->connector = g_value_dup_object (value); + break; + default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); break; @@ -438,6 +436,10 @@ wocky_c2s_porter_get_property (GObject *object, g_value_set_string (value, priv->resource); break; + case PROP_CONNECTOR: + g_value_set_object (value, priv->connector); + break; + default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); break; @@ -475,12 +477,14 @@ wocky_c2s_porter_constructed (GObject *object) g_assert (priv->connection != NULL); - priv->sm_id = NULL; - priv->sm_location = NULL; priv->connector = g_object_steal_data (G_OBJECT (priv->connection), WOCKY_XMPP_NS_SM3); - priv->sm_enabled = (priv->connector != NULL); - wocky_porter_sm_reset (self); + if (priv->connector != NULL) + { + priv->sm = g_new0 (WockyPorterSmCtx, 1); + priv->sm->enabled = TRUE; + wocky_porter_sm_reset (self); + } /* Register the IQ reply handler */ wocky_porter_register_handler_from_anyone (WOCKY_PORTER (self), @@ -508,6 +512,7 @@ wocky_c2s_porter_class_init ( WockyC2SPorterClass *wocky_c2s_porter_class) { GObjectClass *object_class = G_OBJECT_CLASS (wocky_c2s_porter_class); + GParamSpec *spec; object_class->constructed = wocky_c2s_porter_constructed; object_class->set_property = wocky_c2s_porter_set_property; @@ -523,6 +528,19 @@ wocky_c2s_porter_class_init ( PROP_BARE_JID, "bare-jid"); g_object_class_override_property (object_class, PROP_RESOURCE, "resource"); + /** + * WockyC2SPorter:connector: + * + * A #WockyConnector instance, the instance which was used to establish + * current connection and which is supposed to be passed by the connector + * itself as XmppConnection data when connector discovers SM resumable + * connection. The property could be reset by the porter owner if automatic + * connection resumption is not desirable. + */ + spec = g_param_spec_object ("connector", "WockyConnector", + "The connector which created this connection", WOCKY_TYPE_CONNECTOR, + (G_PARAM_READABLE | G_PARAM_STATIC_STRINGS)); + g_object_class_install_property (object_class, PROP_CONNECTOR, spec); } void @@ -552,8 +570,12 @@ wocky_c2s_porter_dispose (GObject *object) g_clear_object (&(priv->force_close_task)); g_clear_object (&(priv->force_close_cancellable)); - priv->sm_enabled = FALSE; - wocky_porter_sm_reset (self); + if (priv->sm) + { + priv->sm->enabled = FALSE; + wocky_porter_sm_reset (self); + g_clear_pointer (&priv->sm, g_free); + } if (G_OBJECT_CLASS (wocky_c2s_porter_parent_class)->dispose) G_OBJECT_CLASS (wocky_c2s_porter_parent_class)->dispose (object); @@ -713,7 +735,7 @@ send_stanza_cb (GObject *source, g_task_return_boolean (elem->task, TRUE); - if (priv->sm_enabled) + if (priv->sm && priv->sm->enabled) { WockyStanzaType st; wocky_stanza_get_type_info (elem->stanza, &st, NULL); @@ -721,7 +743,7 @@ send_stanza_cb (GObject *source, || st == WOCKY_STANZA_TYPE_PRESENCE || st == WOCKY_STANZA_TYPE_IQ) { - priv->snt_count++; + priv->sm->snt_count++; g_queue_push_tail (priv->unacked_queue, g_object_ref (elem->stanza)); g_idle_add (request_ack_in_idle, self); @@ -983,11 +1005,12 @@ handle_stanza (WockyC2SPorter *self, wocky_stanza_get_type_info (stanza, &type, &sub_type); - if (priv->sm_enabled && (type == WOCKY_STANZA_TYPE_MESSAGE - || type == WOCKY_STANZA_TYPE_PRESENCE - || type == WOCKY_STANZA_TYPE_IQ)) + if (priv->sm && priv->sm->enabled + && (type == WOCKY_STANZA_TYPE_MESSAGE + || type == WOCKY_STANZA_TYPE_PRESENCE + || type == WOCKY_STANZA_TYPE_IQ)) { - priv->rcv_count++; + priv->sm->rcv_count++; } /* The from attribute of the stanza need not always be present, for example * when receiving roster items, so don't enforce it. */ @@ -1318,6 +1341,27 @@ connection_force_close_cb (GObject *source, g_object_unref (self); } +/** + * wocky_c2s_porter_get_sm_ctx: + * @self: a #WockyC2SPorter + * + * This call is rather for the test units - to enable visibility of the + * StreamManagement internals in WockyC2SPorter. However it may be used + * for other purposes as well. It returns the SM context structure which + * holds SM internal state. Do not modify the SM counters otherwise the + * connection will most likely be terminated. + * + * Returns: (nullable): #WockyPorterSmCtx of the given porter or %NULL + * if SM was not discovered on the connection. + */ +const WockyPorterSmCtx * +wocky_c2s_porter_get_sm_ctx (WockyC2SPorter *self) +{ + WockyC2SPorterPrivate *priv = wocky_c2s_porter_get_instance_private (self); + + return priv->sm; +} + void wocky_c2s_porter_resume (WockyC2SPorter *self, WockyXmppConnection *connection) @@ -1355,7 +1399,7 @@ connection_reconnected_cb (GObject *source, priv->sending_blocked = FALSE; /* We can only be here if connection supports SM and resume failed, * hence turn SM back on and continue */ - priv->sm_enabled = TRUE; + priv->sm->enabled = TRUE; priv->connector = g_object_ref (ctr); wocky_porter_sm_reset (self); /* We have reset the state already, just continue as if nothing happened */ @@ -1388,7 +1432,7 @@ resume_connection_failed (WockyC2SPorter *self, /* Resume has returned failed:item-not-found, we can continue with the bind * but first we need to reset the state */ flush_unimportant_queue (self); - priv->sm_enabled = FALSE; + priv->sm->enabled = FALSE; wocky_porter_sm_reset (self); /* by doing this we're risking to desync gabble-connection and its @@ -1441,26 +1485,29 @@ resume_connection (WockyC2SPorter *self) WockyC2SPorterPrivate *priv = wocky_c2s_porter_get_instance_private (self); WockyStanza *resume = wocky_stanza_new ("resume", WOCKY_XMPP_NS_SM3); WockyNode *rn = wocky_stanza_get_top_node (resume); - gchar *val = g_strdup_printf ("%lu", priv->rcv_count); + gchar *val = g_strdup_printf ("%lu", priv->sm->rcv_count); gboolean ret = TRUE; - g_assert (priv->connector); - g_assert (priv->sm_id); + g_assert (priv->sm); + g_assert (priv->sm->id); wocky_node_set_attribute (rn, "h", val); - wocky_node_set_attribute (rn, "previd", priv->sm_id); + wocky_node_set_attribute (rn, "previd", priv->sm->id); g_clear_object (&priv->connection); g_object_notify (G_OBJECT (self), "connection"); priv->sending_blocked = TRUE; DEBUG ("Attempting to resume sm %s:%s with connector %p", - priv->sm_id, val, priv->connector); + priv->sm->id, val, priv->connector); g_free (val); g_signal_emit_by_name (self, "resuming", resume, &ret); if (ret) - wocky_connector_resume_async (priv->connector, resume, - priv->receive_cancellable, resume_connection_cb, self); + { + g_assert (priv->connector); + wocky_connector_resume_async (priv->connector, resume, + priv->receive_cancellable, resume_connection_cb, self); + } else DEBUG ("Got stop signal, skipping auto-resume"); @@ -1491,7 +1538,10 @@ wocky_porter_sm_reset (WockyC2SPorter *self) { WockyC2SPorterPrivate *priv = wocky_c2s_porter_get_instance_private (self); - if (priv->sm_enabled) + if (priv->sm == NULL) + return; + + if (priv->sm->enabled) { if (G_UNLIKELY (priv->unacked_queue)) g_queue_free_full (priv->unacked_queue, g_object_unref); @@ -1506,14 +1556,14 @@ wocky_porter_sm_reset (WockyC2SPorter *self) g_queue_free_full (priv->unacked_queue, g_object_unref); priv->unacked_queue = NULL; } - priv->resumable = FALSE; + priv->sm->resumable = FALSE; } - priv->snt_count = priv->snt_acked = priv->rcv_count = 0; - g_clear_pointer (&priv->sm_id, g_free); - g_clear_pointer (&priv->sm_location, g_free); - priv->resumable = FALSE; - priv->sm_timeout = 600; + priv->sm->snt_count = priv->sm->snt_acked = priv->sm->rcv_count = 0; + g_clear_pointer (&priv->sm->id, g_free); + g_clear_pointer (&priv->sm->location, g_free); + priv->sm->resumable = FALSE; + priv->sm->timeout = 600; } /* We need to consider normal monotonic increase and uint32 wrap conditions */ @@ -1544,19 +1594,19 @@ wocky_porter_sm_handle_h (WockyC2SPorter *self, return FALSE; } - if (ACK_WINDOW (priv->snt_acked, snt) > ACK_WINDOW (priv->snt_acked, - priv->snt_count)) + if (ACK_WINDOW (priv->sm->snt_acked, snt) > ACK_WINDOW (priv->sm->snt_acked, + priv->sm->snt_count)) { g_warning ("Invalid acknowledgement %lu, must be between %lu and %lu", - snt, priv->snt_acked, priv->snt_count); + snt, priv->sm->snt_acked, priv->sm->snt_count); return FALSE; } - priv->snt_acked = snt; + priv->sm->snt_acked = snt; /* now we can head-drop stanzas from unacked_queue till its length is * (snt_count - snt_acked) */ while (g_queue_get_length (priv->unacked_queue) - > ACK_WINDOW (priv->snt_count, priv->snt_acked)) + > ACK_WINDOW (priv->sm->snt_count, priv->sm->snt_acked)) { WockyStanza *s = g_queue_pop_head (priv->unacked_queue); g_assert (s); @@ -1571,7 +1621,7 @@ wocky_porter_sm_handle (WockyC2SPorter *self, { WockyC2SPorterPrivate *priv = wocky_c2s_porter_get_instance_private (self); - if (G_UNLIKELY(!priv->sm_enabled)) + if (G_UNLIKELY(!priv->sm->enabled)) { g_warning ("Received SM nonza %s while SM is disabled", node->name); return FALSE; @@ -1581,14 +1631,14 @@ wocky_porter_sm_handle (WockyC2SPorter *self, { /* a probe for request - fire one if we have breached the max or fire an * early notice when we are in the middle of it */ - if (ACK_WINDOW (priv->snt_acked, priv->snt_count) == ACK_WINDOW_MAX/2 - || ACK_WINDOW (priv->snt_acked, priv->snt_count) > ACK_WINDOW_MAX) + if (ACK_WINDOW (priv->sm->snt_acked, priv->sm->snt_count) == ACK_WINDOW_MAX/2 + || ACK_WINDOW (priv->sm->snt_acked, priv->sm->snt_count) > ACK_WINDOW_MAX) { WockyStanza *r = wocky_stanza_new ("r", WOCKY_XMPP_NS_SM3); - if (priv->sm_reqs == 0) + if (priv->sm->reqs == 0) { wocky_porter_send (WOCKY_PORTER (self), r); - priv->sm_reqs++; + priv->sm->reqs++; } g_object_unref (r); return TRUE; @@ -1598,7 +1648,7 @@ wocky_porter_sm_handle (WockyC2SPorter *self, { WockyStanza *a = wocky_stanza_new ("a", WOCKY_XMPP_NS_SM3); WockyNode *an = wocky_stanza_get_top_node (a); - gchar *val = g_strdup_printf ("%lu", priv->rcv_count); + gchar *val = g_strdup_printf ("%lu", priv->sm->rcv_count); wocky_node_set_attribute (an, "h", val); g_free (val); @@ -1610,24 +1660,24 @@ wocky_porter_sm_handle (WockyC2SPorter *self, } else if (node->name[0] == 'a' && node->name[1] == '\0') { - if (priv->sm_reqs > 0) - priv->sm_reqs--; + if (priv->sm->reqs > 0) + priv->sm->reqs--; return wocky_porter_sm_handle_h (self, node); } else if (!g_strcmp0 (node->name, "enabled")) { const gchar *val; if ((val = wocky_node_get_attribute (node, "id")) != NULL) - priv->sm_id = g_strdup (val); + priv->sm->id = g_strdup (val); if ((val = wocky_node_get_attribute (node, "max")) != NULL) - priv->sm_timeout = strtoul (val, NULL, 10); + priv->sm->timeout = strtoul (val, NULL, 10); if ((val = wocky_node_get_attribute (node, "resume")) != NULL) - priv->resumable = (g_strcmp0 (val, "true") == 0); + priv->sm->resumable = (g_strcmp0 (val, "true") == 0); if ((val = wocky_node_get_attribute (node, "location")) != NULL) - priv->sm_location = g_strdup (val); + priv->sm->location = g_strdup (val); g_debug ("SM on connection %p is enabled", priv->connection); @@ -1639,7 +1689,7 @@ wocky_porter_sm_handle (WockyC2SPorter *self, WockyStanza *smr; if (G_UNLIKELY((val = wocky_node_get_attribute (node, "previd")) == NULL - || g_strcmp0 (val, priv->sm_id))) + || g_strcmp0 (val, priv->sm->id))) { /* this must not happen */ GError err = {WOCKY_XMPP_STREAM_ERROR, @@ -1676,7 +1726,7 @@ wocky_porter_sm_handle (WockyC2SPorter *self, else if (!g_strcmp0 (node->name, "failed")) { g_debug ("SM on connection %p has failed", priv->connection); - priv->sm_enabled = FALSE; + priv->sm->enabled = FALSE; wocky_porter_sm_reset (self); } else @@ -1729,7 +1779,7 @@ stanza_received_cb (GObject *source, } else { - if (priv->resumable + if (priv->sm && priv->sm->resumable && (error->domain != WOCKY_XMPP_CONNECTION_ERROR || error->code == WOCKY_XMPP_CONNECTION_ERROR_EOS)) resume_connection (self); @@ -2515,7 +2565,7 @@ send_whitespace_ping_cb (GObject *source, priv->sending_blocked = FALSE; - if (priv->sm_enabled) + if (priv->sm && priv->sm->enabled) ret = wocky_xmpp_connection_send_stanza_finish ( WOCKY_XMPP_CONNECTION (source), res, &error); else @@ -2589,12 +2639,13 @@ wocky_c2s_porter_send_whitespace_ping_async (WockyC2SPorter *self, /* when SM is enabled we need SM ping because we are doing selective * acks hence need to catch-up the latest acks when idle. */ - if (priv->sm_enabled) + if (priv->sm && priv->sm->enabled) { WockyStanza *r = wocky_stanza_new ("r", WOCKY_XMPP_NS_SM3); wocky_xmpp_connection_send_stanza_async (priv->connection, r, cancellable, send_whitespace_ping_cb, g_object_ref (task)); g_object_unref (r); + priv->sm->reqs++; } else { diff --git a/wocky/wocky-c2s-porter.h b/wocky/wocky-c2s-porter.h index c661d29b..88eb1fd3 100644 --- a/wocky/wocky-c2s-porter.h +++ b/wocky/wocky-c2s-porter.h @@ -122,6 +122,20 @@ guint wocky_c2s_porter_register_handler_from_server ( void wocky_c2s_porter_enable_power_saving_mode (WockyC2SPorter *porter, gboolean enable); +typedef struct { + gboolean enabled; /* track sent/rcvd stanzas (not nonzas!) */ + gboolean resumable; /* server confirmed it can resume the stream */ + gchar *id; /* a unique stream identifier for resumption */ + gchar *location; /* preferred server address for resumption */ + gsize timeout; /* a time within which we can try to resume the stream */ + gsize rcv_count; /* a count of stanzas we've received and processed */ + gsize snt_count; /* a count of stanzas we've sent over the wire */ + gsize snt_acked; /* a number of last acked stanzas */ + gint reqs; /* a number of unanswered sm requests */ +} WockyPorterSmCtx; + +const WockyPorterSmCtx * wocky_c2s_porter_get_sm_ctx (WockyC2SPorter *self); + void wocky_c2s_porter_resume (WockyC2SPorter *self, WockyXmppConnection *connection); From 15dbf2a1fe59f1bf17b70ee16a0c148518afe996 Mon Sep 17 00:00:00 2001 From: "Ruslan N. Marchenko" Date: Sun, 22 Nov 2020 19:16:17 +0100 Subject: [PATCH 08/14] Fix handled attribute processing h is specified as 32bit unsigned integer while gsize is long thus might be 64bit. Also reset snt_count on resumption and add some debugging to track unacked queue. --- wocky/wocky-c2s-porter.c | 69 ++++++++++++++++++++++++++++++++++++---- 1 file changed, 63 insertions(+), 6 deletions(-) diff --git a/wocky/wocky-c2s-porter.c b/wocky/wocky-c2s-porter.c index 95f41c81..2ecdfd36 100644 --- a/wocky/wocky-c2s-porter.c +++ b/wocky/wocky-c2s-porter.c @@ -744,6 +744,8 @@ send_stanza_cb (GObject *source, || st == WOCKY_STANZA_TYPE_IQ) { priv->sm->snt_count++; + DEBUG ("Queuing stanza %s", + wocky_stanza_get_top_node (elem->stanza)->name); g_queue_push_tail (priv->unacked_queue, g_object_ref (elem->stanza)); g_idle_add (request_ack_in_idle, self); @@ -1374,7 +1376,6 @@ wocky_c2s_porter_resume (WockyC2SPorter *self, priv->connection = connection; priv->sending_blocked = FALSE; receive_stanza (self); - g_signal_emit_by_name (self, "resumed"); } static void @@ -1566,10 +1567,36 @@ wocky_porter_sm_reset (WockyC2SPorter *self) priv->sm->timeout = 600; } +static void +wocky_porter_sm_error_cb (GObject *source, + GAsyncResult *res, + gpointer user_data) +{ + WockyC2SPorter *self = WOCKY_C2S_PORTER (source); + WockyC2SPorterPrivate *priv = wocky_c2s_porter_get_instance_private (self); + GError *error = NULL; + gboolean ret; + + if (user_data) + { + ret = wocky_porter_send_finish (WOCKY_PORTER (self), res, &error); + if (ret) + wocky_porter_close_async (WOCKY_PORTER (self), + priv->receive_cancellable, wocky_porter_sm_error_cb, NULL); + } + else + { + ret = wocky_porter_close_finish (WOCKY_PORTER (self), res, &error); + g_signal_emit_by_name (self, "remote-error", WOCKY_XMPP_STREAM_ERROR, + WOCKY_XMPP_STREAM_ERROR_UNDEFINED_CONDITION, + "Server acknowledged more stanzas than we have sent"); + } +} + /* We need to consider normal monotonic increase and uint32 wrap conditions */ #define ACK_WINDOW(start, stop) ((start <= stop) \ ? (stop - start) \ - : ((ULONG_MAX - start) + stop)) + : ((G_MAXUINT32 - start + 1) + stop)) #define ACK_WINDOW_MAX 10 static gboolean @@ -1597,21 +1624,39 @@ wocky_porter_sm_handle_h (WockyC2SPorter *self, if (ACK_WINDOW (priv->sm->snt_acked, snt) > ACK_WINDOW (priv->sm->snt_acked, priv->sm->snt_count)) { - g_warning ("Invalid acknowledgement %lu, must be between %lu and %lu", + /* Try to send a stanza using the closing porter */ + WockyStanza *error = wocky_stanza_build (WOCKY_STANZA_TYPE_STREAM_ERROR, + WOCKY_STANZA_SUB_TYPE_NONE, NULL, NULL, + ':', WOCKY_XMPP_NS_STREAM, + '(', "undefined-condition", + ':', WOCKY_XMPP_NS_STREAMS, ')', + '(', "handled-count-too-high", + ':', WOCKY_XMPP_NS_SM3, ')', + NULL); + + DEBUG ("Invalid acknowledgement %lu, must be between %lu and %lu", snt, priv->sm->snt_acked, priv->sm->snt_count); + wocky_porter_send_async (WOCKY_PORTER (self), error, + priv->receive_cancellable, wocky_porter_sm_error_cb, self); + + g_object_unref (error); return FALSE; } + DEBUG ("Acking %lu stanzas handled by the server", snt); priv->sm->snt_acked = snt; /* now we can head-drop stanzas from unacked_queue till its length is * (snt_count - snt_acked) */ while (g_queue_get_length (priv->unacked_queue) - > ACK_WINDOW (priv->sm->snt_count, priv->sm->snt_acked)) + > ACK_WINDOW (priv->sm->snt_acked, priv->sm->snt_count)) { WockyStanza *s = g_queue_pop_head (priv->unacked_queue); g_assert (s); g_object_unref (s); } + DEBUG ("After ack %u(%lu) stanzas left in the queue", + g_queue_get_length (priv->unacked_queue), + ACK_WINDOW (priv->sm->snt_acked, priv->sm->snt_count)); return TRUE; } @@ -1679,7 +1724,7 @@ wocky_porter_sm_handle (WockyC2SPorter *self, if ((val = wocky_node_get_attribute (node, "location")) != NULL) priv->sm->location = g_strdup (val); - g_debug ("SM on connection %p is enabled", priv->connection); + DEBUG ("SM on connection %p is enabled", priv->connection); return TRUE; } @@ -1712,10 +1757,22 @@ wocky_porter_sm_handle (WockyC2SPorter *self, /* passed all sanity checks and trimmed the queue, let's move its * content to the sending one */ + DEBUG ("Moving %u stanzas from SM into sending queue", + g_queue_get_length (priv->unacked_queue)); + /* Note - they will be pushed back to the unacked_queue once sent over + * the connection. Thus we need to reset snt_count to prevent its + * creeping */ + priv->sm->snt_count = priv->sm->snt_acked; while (!g_queue_is_empty (priv->unacked_queue)) wocky_c2s_porter_send_async (WOCKY_PORTER (self), g_queue_pop_head (priv->unacked_queue), NULL, NULL, NULL); + /* The signal processign may inject some stanzas breaking the order + * of stanzas and thus violating RFC6120. Hence signaling only after + * the SM queue is flushed into sending queue */ + DEBUG ("Emitting resumed signal after SM queue is flushed"); + g_signal_emit_by_name (self, "resumed"); + /* Add `r` to the end of the queue and track its progress */ smr = wocky_stanza_new ("r", WOCKY_XMPP_NS_SM3); wocky_c2s_porter_send_async (WOCKY_PORTER (self), smr, @@ -1725,7 +1782,7 @@ wocky_porter_sm_handle (WockyC2SPorter *self, } else if (!g_strcmp0 (node->name, "failed")) { - g_debug ("SM on connection %p has failed", priv->connection); + DEBUG ("SM on connection %p has failed", priv->connection); priv->sm->enabled = FALSE; wocky_porter_sm_reset (self); } From 1c6a7319094547275c1058e13b47012cc6c2c955 Mon Sep 17 00:00:00 2001 From: "Ruslan N. Marchenko" Date: Sat, 14 Nov 2020 23:10:13 +0100 Subject: [PATCH 09/14] Add SM enable, ack and wrap unit tests --- tests/Makefile.am | 16 +- tests/meson.build | 10 + tests/wocky-sm-test.c | 926 ++++++++++++++++++++++++++++ tests/wocky-test-connector-server.c | 208 +++++++ tests/wocky-test-connector-server.h | 15 + 5 files changed, 1174 insertions(+), 1 deletion(-) create mode 100644 tests/wocky-sm-test.c diff --git a/tests/Makefile.am b/tests/Makefile.am index a3cc7177..2967acb4 100644 --- a/tests/Makefile.am +++ b/tests/Makefile.am @@ -88,6 +88,7 @@ TEST_PROGS = \ wocky-session-test \ wocky-stanza-test \ wocky-tls-test \ + wocky-sm-test \ wocky-utils-test \ wocky-xmpp-connection-test \ wocky-xmpp-node-test \ @@ -229,6 +230,19 @@ wocky_tls_test_SOURCES = \ wocky-test-stream.c wocky-test-stream.h wocky_tls_test_CFLAGS = $(AM_CFLAGS) $(TLSDEFS) +EXTRA_wocky_sm_test_DEPENDENCIES = $(CA_DIR) certs +wocky_sm_test_SOURCES = \ + wocky-sm-test.c \ + wocky-test-sasl-auth-server.c \ + wocky-test-sasl-auth-server.h \ + wocky-test-connector-server.c \ + wocky-test-connector-server.h \ + wocky-test-helper.c wocky-test-helper.h \ + wocky-test-stream.c wocky-test-stream.h \ + test-resolver.c test-resolver.h +wocky_sm_test_LDADD = $(LDADD) @LIBSASL2_LIBS@ +wocky_sm_test_CFLAGS = $(AM_CFLAGS) @LIBSASL2_CFLAGS@ $(TLSDEFS) + wocky_utils_test_SOURCES = wocky-utils-test.c wocky_xmpp_connection_test_SOURCES = \ @@ -297,7 +311,7 @@ test-%: wocky-%-test .PHONY: test test-report include $(top_srcdir)/tools/check-coding-style.mk -check-local: test check-coding-style +check-local: check-coding-style test ############################################################################ diff --git a/tests/meson.build b/tests/meson.build index 602f29d4..725dfad1 100644 --- a/tests/meson.build +++ b/tests/meson.build @@ -144,6 +144,16 @@ tests = { 'wocky-test-stream.c', 'wocky-test-stream.h', 'wocky-tls-test.c', ], + 'wocky-sm-test': [ + 'wocky-test-sasl-auth-server.c', + 'wocky-test-sasl-auth-server.h', + 'wocky-test-connector-server.c', + 'wocky-test-connector-server.h', + 'wocky-test-helper.c', 'wocky-test-helper.h', + 'wocky-test-stream.c', 'wocky-test-stream.h', + 'test-resolver.c', 'test-resolver.h', + 'wocky-sm-test.c', + ], 'wocky-utils-test': [ 'wocky-utils-test.c', ], diff --git a/tests/wocky-sm-test.c b/tests/wocky-sm-test.c new file mode 100644 index 00000000..15affc2b --- /dev/null +++ b/tests/wocky-sm-test.c @@ -0,0 +1,926 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef G_OS_WIN32 +#include +#include +#else +#include +#include +#include +#include +#include +#endif + +#ifdef G_OS_UNIX +#include +#endif + +#include + +#include "wocky-test-connector-server.h" +#include "test-resolver.h" +#include "wocky-test-helper.h" +#include "config.h" + +#ifdef G_LOG_DOMAIN +#undef G_LOG_DOMAIN +#endif +#define G_LOG_DOMAIN "wocky-sm-test" + +#define SASL_DB_NAME "sasl-test.db" + +#define INVISIBLE_HOST "unreachable.host" +#define VISIBLE_HOST "reachable.host" +#define REACHABLE "127.0.0.1" +#define UNREACHABLE "127.255.255.255" +#define DUFF_H0ST "no_such_host.at.all" + +#define OLD_SSL TRUE +#define OLD_JABBER TRUE +#define XMPP_V1 FALSE +#define STARTTLS FALSE + +#define CERT_CHECK_STRICT FALSE +#define CERT_CHECK_LENIENT TRUE + +#define TLS_REQUIRED TRUE +#define PLAINTEXT_OK FALSE + +#define QUIET TRUE +#define NOISY FALSE + +#define TLS TRUE +#define NOTLS FALSE + +#define PLAIN FALSE +#define DIGEST TRUE + +#define DEFAULT_SASL_MECH "SCRAM-SHA-256" + +#define PORT_XMPP 5222 +#define PORT_NONE 0 + +#define OK 0 +#define CONNECTOR_OK { OK, OK, OK, OK, OK, OK, OK } +#define SM_PROBLEM(x) { OK, OK, OK, OK, OK, OK, SM_PROBLEM_##x } + + +static GError *error = NULL; +static GResolver *original; +static GResolver *kludged; +static GMainLoop *mainloop; + +enum { + S_NO_ERROR = 0, + S_WOCKY_AUTH_ERROR, + S_WOCKY_CONNECTOR_ERROR, + S_WOCKY_XMPP_CONNECTION_ERROR, + S_WOCKY_TLS_CERT_ERROR, + S_WOCKY_XMPP_STREAM_ERROR, + S_G_IO_ERROR, + S_G_RESOLVER_ERROR, + S_ANY_ERROR = 0xff +}; + +#define MAP(x) case S_##x: return x +static GQuark +map_static_domain (gint domain) +{ + switch (domain) + { + MAP (WOCKY_AUTH_ERROR); + MAP (WOCKY_CONNECTOR_ERROR); + MAP (WOCKY_XMPP_CONNECTION_ERROR); + MAP (WOCKY_TLS_CERT_ERROR); + MAP (WOCKY_XMPP_STREAM_ERROR); + MAP (G_IO_ERROR); + MAP (G_RESOLVER_ERROR); + default: + g_assert_not_reached (); + } +} +#undef MAP + + +typedef void (*test_setup) (gpointer); + +typedef struct _ServerParameters ServerParameters; +struct _ServerParameters { + struct { gboolean tls; gchar *auth_mech; gchar *version; } features; + struct { ServerProblem sasl; ConnectorProblem conn; } problem; + struct { gchar *user; gchar *pass; } auth; + guint port; + CertSet cert; + + /* Extra server for see-other-host problem */ + ServerParameters *extra_server; + + /* Runtime */ + TestConnectorServer *server; + GIOChannel *channel; + guint watch; +}; + +typedef struct { + gchar *desc; + gboolean quiet; + struct { int domain; + int code; + int fallback_code; + gchar *mech; + gchar *used_mech; + gpointer xmpp; + gchar *jid; + gchar *sid; + } result; + ServerParameters server_parameters; + struct { char *srv; guint port; char *host; char *addr; char *srvhost; } dns; + struct { + gboolean require_tls; + struct { gchar *jid; gchar *pass; gboolean secure; gboolean tls; } auth; + struct { gchar *host; guint port; gboolean jabber; gboolean ssl; gboolean lax_ssl; const gchar *ca; } options; + int op; + test_setup setup; + } client; + + /* Runtime */ + WockyConnector *connector; + WockySession *session; + WockyPorter *porter; + gboolean ok; +} test_t; + +test_t tests[] = + { /* basic connection test, no SRV record, no host or port supplied: */ + /* + { "/name/of/test", + SUPPRESS_STDERR, + // result of test: + { DOMAIN, CODE, FALLBACK_CODE, + AUTH_MECH_USED, XMPP_CONNECTION_PLACEHOLDER }, + // When an error is expected it should match the domain and either + // the given CODE or the FALLBACK_CODE (GIO over time became more + // specific about the error codes it gave in certain conditions) + + // Server Details: + { { TLS_SUPPORT, AUTH_MECH_OR_NULL_FOR_ALL }, + { SERVER_PROBLEM..., CONNECTOR_PROBLEM... }, + { USERNAME, PASSWORD }, + SERVER_LISTEN_PORT, SERVER_CERT }, + + // Fake DNS Record: + // SRV_HOSTs SRV record → { HOSTNAME, PORT } + // HOSTs A record → IP_ADDRESS + // SRV_HOSTs A record → IP_ADDR_OF_SRV_HOST + { SRV_HOST, PORT, HOSTNAME, IP_ADDRESS, IP_ADDR_OF_SRV_HOST }, + + // Client Details + { TLS_REQUIRED, + { BARE_JID, PASSWORD, MUST_BE_DIGEST_AUTH, MUST_BE_SECURE }, + { XMPP_HOSTNAME_OR_NULL, XMPP_PORT_OR_ZERO, OLD_JABBER, OLD_SSL } } + SERVER_PROCESS_ID }, */ + + /* simple connection, followed by checks on all the internal state * + * and get/set property methods to make sure they work */ + + /* ********************************************************************* */ + /* SM test conditions */ + { "/tls/sasl/sm/enable", + NOISY, + { S_NO_ERROR }, + { { TLS, DEFAULT_SASL_MECH }, + { SERVER_PROBLEM_NO_PROBLEM, SM_PROBLEM (ENABLED) }, + { "moose", "something" }, + PORT_XMPP }, + { "weasel-juice.org", PORT_XMPP, "thud.org", REACHABLE, UNREACHABLE }, + { TLS_REQUIRED, + { "moose@weasel-juice.org", "something", DIGEST, TLS }, + { NULL, 0 } } }, + + { "/tls/sasl/sm/ack0", + NOISY, + { S_NO_ERROR }, + { { TLS, DEFAULT_SASL_MECH }, + { SERVER_PROBLEM_NO_PROBLEM, SM_PROBLEM (ACK0) }, + { "moose", "something" }, + PORT_XMPP }, + { "weasel-juice.org", PORT_XMPP, "thud.org", REACHABLE, UNREACHABLE }, + { TLS_REQUIRED, + { "moose@weasel-juice.org", "something", DIGEST, TLS }, + { NULL, 0 } } }, + + { "/tls/sasl/sm/ack1", + NOISY, + { S_NO_ERROR }, + { { TLS, DEFAULT_SASL_MECH }, + { SERVER_PROBLEM_NO_PROBLEM, SM_PROBLEM (ACK1) }, + { "moose", "something" }, + PORT_XMPP }, + { "weasel-juice.org", PORT_XMPP, "thud.org", REACHABLE, UNREACHABLE }, + { TLS_REQUIRED, + { "moose@weasel-juice.org", "something", DIGEST, TLS }, + { NULL, 0 } } }, + + { "/tls/sasl/sm/ack-overrun", + NOISY, + { S_WOCKY_XMPP_STREAM_ERROR, WOCKY_XMPP_STREAM_ERROR_UNDEFINED_CONDITION, }, + { { TLS, DEFAULT_SASL_MECH }, + { SERVER_PROBLEM_NO_PROBLEM, SM_PROBLEM (ACK0_OVER) }, + { "moose", "something" }, + PORT_XMPP }, + { "weasel-juice.org", PORT_XMPP, "thud.org", REACHABLE, UNREACHABLE }, + { TLS_REQUIRED, + { "moose@weasel-juice.org", "something", DIGEST, TLS }, + { NULL, 0 } } }, + + { "/tls/sasl/sm/ack-wrap0", + NOISY, + { S_NO_ERROR }, + { { TLS, DEFAULT_SASL_MECH }, + { SERVER_PROBLEM_NO_PROBLEM, SM_PROBLEM (WRAP0) }, + { "moose", "something" }, + PORT_XMPP }, + { "weasel-juice.org", PORT_XMPP, "thud.org", REACHABLE, UNREACHABLE }, + { TLS_REQUIRED, + { "moose@weasel-juice.org", "something", DIGEST, TLS }, + { NULL, 0 } } }, + + { "/tls/sasl/sm/ack-wrap1", + NOISY, + { S_NO_ERROR }, + { { TLS, DEFAULT_SASL_MECH }, + { SERVER_PROBLEM_NO_PROBLEM, SM_PROBLEM (WRAP1) }, + { "moose", "something" }, + PORT_XMPP }, + { "weasel-juice.org", PORT_XMPP, "thud.org", REACHABLE, UNREACHABLE }, + { TLS_REQUIRED, + { "moose@weasel-juice.org", "something", DIGEST, TLS }, + { NULL, 0 } } }, + + { "/tls/sasl/sm/wrap-overrun", + NOISY, + { S_WOCKY_XMPP_STREAM_ERROR, WOCKY_XMPP_STREAM_ERROR_UNDEFINED_CONDITION, }, + { { TLS, DEFAULT_SASL_MECH }, + { SERVER_PROBLEM_NO_PROBLEM, SM_PROBLEM (WRAP0_OVER) }, + { "moose", "something" }, + PORT_XMPP }, + { "weasel-juice.org", PORT_XMPP, "thud.org", REACHABLE, UNREACHABLE }, + { TLS_REQUIRED, + { "moose@weasel-juice.org", "something", DIGEST, TLS }, + { NULL, 0 } } }, + + /* we are done, cap the list: */ + { NULL } + }; + +/* ************************************************************************* */ +#define STRING_OK(x) (((x) != NULL) && (*x != '\0')) + +static void +setup_dummy_dns_entries (const test_t *test) +{ + TestResolver *tr = TEST_RESOLVER (kludged); + guint port = test->dns.port ? test->dns.port : test->server_parameters.port; + const char *domain = test->dns.srv; + const char *host = test->dns.host; + const char *addr = test->dns.addr; + const char *s_ip = test->dns.srvhost; + + test_resolver_reset (tr); + + if (STRING_OK (domain) && STRING_OK (host)) + test_resolver_add_SRV (tr, "xmpp-client", "tcp", domain, host, port); + + if (STRING_OK (domain) && STRING_OK (s_ip)) + test_resolver_add_A (tr, domain, s_ip); + + if (STRING_OK (host) && STRING_OK (addr)) + test_resolver_add_A (tr, host, addr); + + test_resolver_add_A (tr, INVISIBLE_HOST, UNREACHABLE); + test_resolver_add_A (tr, VISIBLE_HOST, REACHABLE); +} + +/* ************************************************************************* */ +/* Dummy XMPP server */ +static void start_dummy_xmpp_server (ServerParameters *srv); + +static gboolean +client_connected (GIOChannel *channel, + GIOCondition cond, + gpointer data) +{ + ServerParameters *srv = data; + struct sockaddr_in client; + socklen_t clen = sizeof (client); + int ssock = g_io_channel_unix_get_fd (channel); + int csock = accept (ssock, (struct sockaddr *) &client, &clen); + GSocket *gsock = g_socket_new_from_fd (csock, NULL); + ConnectorProblem *cproblem = &srv->problem.conn; + + GSocketConnection *gconn; + + if (csock < 0) + { + perror ("accept() failed"); + g_warning ("accept() failed on socket that should have been ready."); + return TRUE; + } + + if (!srv->features.tls) + cproblem->xmpp |= XMPP_PROBLEM_NO_TLS; + + gconn = g_object_new (G_TYPE_SOCKET_CONNECTION, "socket", gsock, NULL); + g_object_unref (gsock); + + srv->server = test_connector_server_new (G_IO_STREAM (gconn), + srv->features.auth_mech, + srv->auth.user, + srv->auth.pass, + srv->features.version, + NULL, + cproblem, + srv->problem.sasl, + srv->cert); + g_object_unref (gconn); + + /* Recursively start extra servers */ + if (srv->extra_server != NULL) + { + test_connector_server_set_other_host (srv->server, REACHABLE, + srv->extra_server->port); + start_dummy_xmpp_server (srv->extra_server); + } + + test_connector_server_start (srv->server); + + srv->watch = 0; + return FALSE; +} + +static void +start_dummy_xmpp_server (ServerParameters *srv) +{ + int ssock; + int reuse = 1; + struct sockaddr_in server; + int res = -1; + guint port = srv->port; + + if (port == 0) + return; + + memset (&server, 0, sizeof (server)); + + server.sin_family = AF_INET; + + /* mingw doesn't support aton or pton so using more portable inet_addr */ + server.sin_addr.s_addr = inet_addr ((const char * ) REACHABLE); + server.sin_port = htons (port); + ssock = socket (AF_INET, SOCK_STREAM, IPPROTO_TCP); + setsockopt (ssock, SOL_SOCKET, SO_REUSEADDR, (const char *) &reuse, sizeof (reuse)); +#ifdef G_OS_UNIX + setsockopt (ssock, IPPROTO_TCP, TCP_NODELAY, (const char *) &reuse, sizeof (reuse)); +#endif + + res = bind (ssock, (struct sockaddr *) &server, sizeof (server)); + + if (res != 0) + { + int code = errno; + char *err = g_strdup_printf ("bind to " REACHABLE ":%d failed", port); + perror (err); + g_free (err); + exit (code); + } + + res = listen (ssock, 1024); + if (res != 0) + { + int code = errno; + char *err = g_strdup_printf ("listen on " REACHABLE ":%d failed", port); + perror (err); + g_free (err); + exit (code); + } + + srv->channel = g_io_channel_unix_new (ssock); + g_io_channel_set_flags (srv->channel, G_IO_FLAG_NONBLOCK, NULL); + srv->watch = g_io_add_watch (srv->channel, G_IO_IN|G_IO_PRI, + client_connected, srv); + g_io_channel_set_close_on_unref (srv->channel, TRUE); + return; +} +/* ************************************************************************* */ +static void +test_server_teardown_cb (GObject *source, + GAsyncResult *result, + gpointer user_data) +{ + GMainLoop *loop = user_data; + + g_assert (test_connector_server_teardown_finish ( + TEST_CONNECTOR_SERVER (source), result, NULL)); + + g_main_loop_quit (loop); +} + +static gboolean +test_server_idle_quit_loop_cb (GMainLoop *loop) +{ + static int retries = 0; + + if (retries == 5) + { + g_main_loop_quit (loop); + retries = 0; + return G_SOURCE_REMOVE; + } + else + { + retries ++; + return G_SOURCE_CONTINUE; + } +} + +static void +test_server_teardown (test_t *test, + ServerParameters *srv) +{ + /* Recursively teardown extra servers */ + if (srv->extra_server != NULL) + test_server_teardown (test, srv->extra_server); + srv->extra_server = NULL; + + if (srv->server != NULL) + { + GMainLoop *loop = g_main_loop_new (NULL, FALSE); + + if (test->result.used_mech == NULL) + { + test->result.used_mech = g_strdup ( + test_connector_server_get_used_mech (srv->server)); + } + + /* let the server dispatch any pending events before + * forcing it to tear down */ + g_idle_add ((GSourceFunc) test_server_idle_quit_loop_cb, loop); + g_main_loop_run (loop); + + /* Run until server is down */ + test_connector_server_teardown (srv->server, + test_server_teardown_cb, loop); + g_main_loop_run (loop); + + g_clear_object (&srv->server); + g_main_loop_unref (loop); + } + + if (srv->watch != 0) + g_source_remove (srv->watch); + srv->watch = 0; + + if (srv->channel != NULL) + g_io_channel_unref (srv->channel); + srv->channel = NULL; +} + +static void +test_done (GObject *source, + GAsyncResult *res, + gpointer data) +{ + test_t *test = data; + + if (WOCKY_IS_XMPP_CONNECTION (source)) + test->result.xmpp = g_object_ref (source); + else if (WOCKY_IS_C2S_PORTER (source)) + { + WockyXmppConnection *conn = NULL; + g_object_get (source, + "connection", &conn, + NULL); + wocky_porter_close_finish (WOCKY_PORTER (source), res, &error); + g_object_unref (source); + test->result.xmpp = g_object_ref (conn); + } + + test_server_teardown (test, &test->server_parameters); + + g_main_loop_quit (mainloop); +} + +typedef void (*test_func) (gconstpointer); + +#ifdef G_OS_UNIX +static void +connection_established_cb (WockyConnector *connector, + GSocketConnection *conn, + gpointer user_data) +{ + GSocket *sock = g_socket_connection_get_socket (conn); + gint fd, flag = 1; + + fd = g_socket_get_fd (sock); + setsockopt (fd, IPPROTO_TCP, TCP_NODELAY, + (const char *) &flag, sizeof (flag)); +} +#endif + +static void +iq_result (GObject *source, + GAsyncResult *res, + gpointer data); + +static gboolean +test_phase (gpointer data) +{ + test_t *test = data; + SMProblem sm = test->server_parameters.problem.conn.sm; + const WockyPorterSmCtx *smc; + + if (test->porter == NULL) + { + test_done (NULL, NULL, test); + return FALSE; + } + + smc = wocky_c2s_porter_get_sm_ctx (WOCKY_C2S_PORTER (test->porter)); + + g_debug ("Reqs: %d", smc->reqs); + g_assert_cmpint (smc->reqs, ==, 0); + if (sm & SM_PROBLEM_WRAP) + { + if (test->client.op == 0) + g_assert_cmpint (smc->snt_acked, ==, G_MAXUINT32); + else + g_assert_cmpint (smc->snt_acked, ==, 0); + } + else + g_assert_cmpint (smc->snt_acked, ==, test->client.op); + + if (sm & SM_PROBLEM_ACK1 && test->client.op == 0) + { + WockyStanza *iq = wocky_stanza_build (WOCKY_STANZA_TYPE_IQ, + WOCKY_STANZA_SUB_TYPE_SET, + NULL, NULL, + '(', "session", ':', WOCKY_XMPP_NS_SESSION, + ')', + NULL); + wocky_porter_send_iq_async (WOCKY_PORTER (test->porter), iq, NULL, + iq_result, test); + test->client.op++; + g_object_unref (iq); + return FALSE; + } + + wocky_porter_close_async (WOCKY_PORTER (test->porter), NULL, test_done, + test); + return FALSE; +} + +static void +smreqd (GObject *source, + GAsyncResult *res, + gpointer data) +{ + test_t *test = data; + WockyC2SPorter *p = WOCKY_C2S_PORTER (source); + const WockyPorterSmCtx *smc = wocky_c2s_porter_get_sm_ctx (p); + + g_assert_true ( + wocky_c2s_porter_send_whitespace_ping_finish (p, res, &error)); + g_assert_cmpint (smc->reqs, ==, 1); + + /* and wait for `a` in response */ + + g_idle_add (test_phase, test); +} + +static void +iq_result (GObject *source, + GAsyncResult *res, + gpointer data) +{ + test_t *test = data; + WockyPorter *p = WOCKY_PORTER (source); + WockyC2SPorter *cp = WOCKY_C2S_PORTER (source); + const WockyPorterSmCtx *smc = wocky_c2s_porter_get_sm_ctx (cp); + WockyStanza *iq = wocky_porter_send_iq_finish (p, res, &error); + + g_assert_nonnull (iq); + g_object_unref (iq); + + g_assert_cmpint (smc->snt_count, ==, 1); + g_assert_cmpint (smc->rcv_count, ==, 1); + + g_debug ("Acked: %lu", smc->snt_acked); + wocky_c2s_porter_send_whitespace_ping_async (WOCKY_C2S_PORTER (test->porter), + NULL, smreqd, test); +} + +static void +remote_error_cb (WockyPorter *porter, + GQuark domain, + gint code, + gchar *msg, + gpointer data) +{ + test_t *test = data; + + g_debug ("Error: %s", msg); + g_set_error_literal (&error, domain, code, msg); + + if (test->server_parameters.problem.conn.sm & SM_PROBLEM_OVER) + { + test->porter = NULL; + g_object_unref (porter); + } +} + +static void +enabled (GObject *source, + GAsyncResult *result, + gpointer data) +{ + test_t *test = data; + WockyXmppConnection *conn = WOCKY_XMPP_CONNECTION (source); + SMProblem sm = test->server_parameters.problem.conn.sm; + const WockyStanza *enabled; + WockyNode *enn; + WockyPorterSmCtx *smc; + + enabled = wocky_xmpp_connection_peek_stanza_finish (conn, result, &error); + g_assert_nonnull (enabled); + g_assert_null (error); + + enn = wocky_stanza_get_top_node ((WockyStanza *) enabled); + g_assert_cmpstr (enn->name, ==, "enabled"); + + test->session = wocky_session_new_with_connection (conn, test->result.jid); + test->porter = wocky_session_get_porter (test->session); + if (sm & SM_PROBLEM_OVER) + g_signal_connect (test->porter, "remote-error", + G_CALLBACK (remote_error_cb), test); + wocky_porter_start (test->porter); + + /* Do NOT EVER do that in real life - context must not me modified */ + smc = (WockyPorterSmCtx *) wocky_c2s_porter_get_sm_ctx ( + WOCKY_C2S_PORTER (test->porter)); + g_assert_nonnull (smc); + g_assert_true (smc->enabled); + + /* let the world spin - consume the enabled */ + g_main_context_iteration (NULL, FALSE); + + g_assert_true (smc->resumable); + g_assert_cmpstr (smc->id, ==, "deadbeef"); + + if (sm < SM_PROBLEM_ACK0) + { + test_done (G_OBJECT (conn), NULL, test); + return; + } + + /* let the world spin again - there will be `r` from ... */ + g_main_context_iteration (NULL, FALSE); + /* ... and `a` to the server */ + g_main_context_iteration (NULL, FALSE); + + /* before we do get the chance to process the `a`nswer let set some tests */ + if (sm & SM_PROBLEM_WRAP) + { + g_debug ("unwrapping snt_acked"); + smc->snt_acked = G_MAXUINT32-1; + } + + /* now send our r there */ + wocky_c2s_porter_send_whitespace_ping_async (WOCKY_C2S_PORTER (test->porter), + NULL, smreqd, test); +} + +static void +connected (GObject *source, + GAsyncResult *res, + gpointer data) +{ + test_t *test = data; + WockyConnector *wcon = WOCKY_CONNECTOR (source); + WockyXmppConnection *conn = NULL; + + error = NULL; + conn = wocky_connector_connect_finish (wcon, res, + &test->result.jid, &test->result.sid, &error); + g_assert_nonnull (conn); + g_assert_null (error); + + g_debug ("Connected %d", test->server_parameters.problem.conn.sm); + wocky_xmpp_connection_peek_stanza_async (conn, NULL, enabled, test); +} + +static gboolean +start_test (gpointer data) +{ + test_t *test = data; + + if (test->client.setup != NULL) + (test->client.setup) (test); + + g_debug ("Start %d", test->server_parameters.problem.conn.sm); + wocky_connector_connect_async (test->connector, NULL, connected, data); + return FALSE; +} + +static void +run_test (gpointer data) +{ + WockyConnector *wcon = NULL; + WockyTLSHandler *handler; + test_t *test = data; + struct stat dummy; + gchar *base; + char *path; + const gchar *ca; + + /* clean up any leftover messes from previous tests */ + /* unlink the sasl db tmpfile, it will cause a deadlock */ + base = g_get_current_dir (); + path = g_strdup_printf ("%s/__db.%s", base, SASL_DB_NAME); + g_free (base); + g_assert ((g_stat (path, &dummy) != 0) || (g_unlink (path) == 0)); + g_free (path); + /* end of cleanup block */ + + start_dummy_xmpp_server (&test->server_parameters); + setup_dummy_dns_entries (test); + + ca = test->client.options.ca ? test->client.options.ca : TLS_CA_CRT_FILE; + + if (test->client.options.host && test->client.options.port == 0) + test->client.options.port = test->server_parameters.port; + + /* insecure tls cert/etc not yet implemented */ + handler = wocky_tls_handler_new (test->client.options.lax_ssl); + + wcon = g_object_new ( WOCKY_TYPE_CONNECTOR, + "jid" , test->client.auth.jid, + "password" , test->client.auth.pass, + "xmpp-server" , test->client.options.host, + "xmpp-port" , test->client.options.port, + "tls-required" , test->client.require_tls, + "encrypted-plain-auth-ok" , !test->client.auth.secure, + /* this refers to PLAINTEXT vs CRYPT, not PLAIN vs DIGEST */ + "plaintext-auth-allowed" , !test->client.auth.tls, + "legacy" , test->client.options.jabber, + "old-ssl" , test->client.options.ssl, + "tls-handler" , handler, + NULL); + + /* Make sure we only use the test CAs, not system-wide ones. */ + wocky_tls_handler_forget_cas (handler); + g_assert (wocky_tls_handler_get_cas (handler) == NULL); + + /* check if the cert paths are valid */ + g_assert (g_file_test (TLS_CA_CRT_FILE, G_FILE_TEST_EXISTS)); + + wocky_tls_handler_add_ca (handler, ca); + + /* not having a CRL can expose a bug in the openssl error handling + * (basically we get 'CRL not fetched' instead of 'Expired'): + * The bug has been fixed, but we can keep checking for it by + * dropping the CRLs when the test is for an expired cert */ + if (test->server_parameters.cert != CERT_EXPIRED) + wocky_tls_handler_add_crl (handler, TLS_CRL_DIR); + + g_object_unref (handler); + + test->connector = wcon; + g_idle_add (start_test, test); + +#ifdef G_OS_UNIX + /* set TCP_NODELAY as soon as possible */ + g_signal_connect (test->connector, "connection-established", + G_CALLBACK (connection_established_cb), NULL); +#endif + + g_main_loop_run (mainloop); + + if (test->result.domain == S_NO_ERROR) + { + if (error != NULL) + fprintf (stderr, "Error: %s.%d: %s\n", + g_quark_to_string (error->domain), + error->code, + error->message); + g_assert_no_error (error); + + if (test->client.op >= 0) + { + g_assert (test->result.xmpp != NULL); + + /* make sure we selected the right auth mechanism */ + if (test->result.mech != NULL) + { + g_assert_cmpstr (test->result.mech, ==, + test->result.used_mech); + } + + /* we got a JID back, I hope */ + g_assert (test->result.jid != NULL); + g_assert (*test->result.jid != '\0'); + g_free (test->result.jid); + + /* we got a SID back, I hope */ + g_assert (test->result.sid != NULL); + g_assert (*test->result.sid != '\0'); + g_free (test->result.sid); + } + } + else + { + g_assert (test->result.xmpp == NULL); + + if (test->result.domain != S_ANY_ERROR) + { + /* We want the error to match either of result.code or + * result.fallback_code, but don't care which. + * The expected error domain is the same for either code. + */ + if (error->code == test->result.fallback_code) + g_assert_error (error, map_static_domain (test->result.domain), + test->result.fallback_code); + else + g_assert_error (error, map_static_domain (test->result.domain), + test->result.code); + } + } + + if (wcon != NULL) + g_object_unref (wcon); + + if (error != NULL) + g_error_free (error); + + if (test->result.xmpp != NULL) + g_object_unref (test->result.xmpp); + + g_free (test->result.used_mech); + error = NULL; +} + +int +main (int argc, + char **argv) +{ + int i; + gchar *base; + gchar *path = NULL; + struct stat dummy; + int result; + + test_init (argc, argv); + + /* hook up the fake DNS resolver that lets us divert A and SRV queries * + * into our local cache before asking the real DNS */ + original = g_resolver_get_default (); + kludged = g_object_new (TEST_TYPE_RESOLVER, "real-resolver", original, NULL); + g_resolver_set_default (kludged); + + /* unlink the sasl db, we want to test against a fresh one */ + base = g_get_current_dir (); + path = g_strdup_printf ("%s/%s", base, SASL_DB_NAME); + g_free (base); + g_assert ((g_stat (path, &dummy) != 0) || (g_unlink (path) == 0)); + g_free (path); + + mainloop = g_main_loop_new (NULL, FALSE); + +#ifdef HAVE_LIBSASL2 + + for (i = 0; tests[i].desc != NULL; i++) + g_test_add_data_func (tests[i].desc, &tests[i], (test_func)run_test); + +#else + + g_message ("libsasl2 not found: skipping SCRAM SASL tests"); + for (i = 0; tests[i].desc != NULL; i++) + { + if (!wocky_strdiff (tests[i].result.mech, DEFAULT_SASL_MECH)) + continue; + g_test_add_data_func (tests[i].desc, &tests[i], (test_func)run_test); + } + +#endif + + result = g_test_run (); + test_deinit (); + return result; +} diff --git a/tests/wocky-test-connector-server.c b/tests/wocky-test-connector-server.c index 9d46810f..adc29443 100644 --- a/tests/wocky-test-connector-server.c +++ b/tests/wocky-test-connector-server.c @@ -108,6 +108,7 @@ struct _TestConnectorServerPrivate GCancellable *cancellable; gint outstanding; + gint rcv_count;; GTask *teardown_task; struct { ServerProblem sasl; ConnectorProblem *connector; } problem; @@ -179,6 +180,7 @@ test_connector_server_init (TestConnectorServer *self) priv->tls_started = FALSE; priv->authed = FALSE; priv->cancellable = g_cancellable_new (); + priv->rcv_count = -1; } static void @@ -199,6 +201,14 @@ static void handle_auth (TestConnectorServer *self, WockyStanza *xml); static void handle_starttls (TestConnectorServer *self, WockyStanza *xml); +static void handle_enable (TestConnectorServer *self, + WockyStanza *xml); +static void handle_a (TestConnectorServer *self, + WockyStanza *xml); +static void handle_r (TestConnectorServer *self, + WockyStanza *xml); +static void handle_error (TestConnectorServer *self, + WockyStanza *xml); static void after_auth (GObject *source, @@ -237,6 +247,10 @@ static stanza_handler handlers[] = { HANDLER (SASL_AUTH, auth), HANDLER (TLS, starttls), + HANDLER (SM3, enable), + HANDLER (SM3, a), + HANDLER (SM3, r), + HANDLER (STREAM, error), { NULL, NULL, NULL } }; @@ -881,6 +895,8 @@ iq_set_session_XMPP_SESSION (TestConnectorServer *self, iq = wocky_stanza_build (WOCKY_STANZA_TYPE_IQ, WOCKY_STANZA_SUB_TYPE_RESULT, NULL, NULL, + '@', "id", wocky_node_get_attribute (wocky_stanza_get_top_node (xml), + "id"), '(', "session", ':', WOCKY_XMPP_NS_SESSION, ')', NULL); @@ -1032,6 +1048,189 @@ handle_starttls (TestConnectorServer *self, g_object_unref (xml); } +static void +sm_ack (GObject *source, + GAsyncResult *result, + gpointer data) +{ + WockyXmppConnection *conn = WOCKY_XMPP_CONNECTION (source); + TestConnectorServer *self = TEST_CONNECTOR_SERVER (data); + TestConnectorServerPrivate *priv = self->priv; + WockyStanza *ack; + WockyNode *top; + const gchar *h; + + DEBUG ("Validating porter R handler"); + + ack = wocky_xmpp_connection_recv_stanza_finish (conn, result, NULL); + g_assert_nonnull (ack); + + top = wocky_stanza_get_top_node (ack); + g_assert_cmpstr (top->name, ==, "a"); + + h = wocky_node_get_attribute (top, "h"); + g_assert_nonnull (h); + g_assert_cmpstr (h, ==, "0"); + + server_enc_outstanding (self); + DEBUG ("waiting for next stanza from client"); + wocky_xmpp_connection_recv_stanza_async (priv->conn, + priv->cancellable, xmpp_handler, self); +} + +static void +sm_get_ack (GObject *source, + GAsyncResult *result, + gpointer data) +{ + WockyXmppConnection *conn = WOCKY_XMPP_CONNECTION (source); + TestConnectorServer *self = TEST_CONNECTOR_SERVER (data); + TestConnectorServerPrivate *priv = self->priv; + + DEBUG (""); + + g_assert_true ( + wocky_xmpp_connection_send_stanza_finish (conn, result, NULL)); + DEBUG ("waiting for ack from client"); + wocky_xmpp_connection_recv_stanza_async (conn, priv->cancellable, sm_ack, + data); +} + +static void +sm_req (GObject *source, + GAsyncResult *result, + gpointer data) +{ + WockyXmppConnection *conn = WOCKY_XMPP_CONNECTION (source); + TestConnectorServer *self = TEST_CONNECTOR_SERVER (data); + TestConnectorServerPrivate *priv = self->priv; + WockyStanza *reply = wocky_stanza_new ("r", WOCKY_XMPP_NS_SM3); + + DEBUG (""); + g_assert_true ( + wocky_xmpp_connection_send_stanza_finish (conn, result, NULL)); + + wocky_xmpp_connection_send_stanza_async (priv->conn, reply, NULL, sm_get_ack, + self); + g_object_unref (reply); +} + +static void +handle_enable (TestConnectorServer *self, + WockyStanza *xml) +{ + TestConnectorServerPrivate *priv = self->priv; + WockyStanza *reply = wocky_stanza_new ("enabled", WOCKY_XMPP_NS_SM3); + WockyNode *top = wocky_stanza_get_top_node (reply); + + DEBUG (""); + /* we simply and blindly respond to this */ + wocky_node_set_attribute (top, "id", "deadbeef"); + wocky_node_set_attribute (top, "resume", "true"); + priv->rcv_count = 0; + + /* if we have other SM tests - proceed with r/a */ + if (priv->problem.connector->sm > SM_PROBLEM_ENABLED) + wocky_xmpp_connection_send_stanza_async (priv->conn, reply, NULL, + sm_req, self); + else + wocky_xmpp_connection_send_stanza_async (priv->conn, reply, NULL, + finished, self); + + g_object_unref (xml); + g_object_unref (reply); +} + +static void +handle_a (TestConnectorServer *self, + WockyStanza *xml) +{ + TestConnectorServerPrivate *priv = self->priv; + WockyNode *top = wocky_stanza_get_top_node (xml); + const gchar *h = wocky_node_get_attribute (top, "h"); + + DEBUG ("%s", h); + g_assert_nonnull (h); + + g_object_unref (xml); + + server_enc_outstanding (self); + DEBUG ("waiting for next stanza from client"); + wocky_xmpp_connection_recv_stanza_async (priv->conn, + priv->cancellable, xmpp_handler, self); +} + +static void +handle_r (TestConnectorServer *self, + WockyStanza *xml) +{ + TestConnectorServerPrivate *priv = self->priv; + WockyStanza *reply = wocky_stanza_new ("a", WOCKY_XMPP_NS_SM3); + WockyNode *top = wocky_stanza_get_top_node (reply); + g_autofree gchar *h = NULL; + guint32 hn; + + g_object_unref (xml); + + if (priv->problem.connector->sm == SM_PROBLEM_WRAP0) + hn = G_MAXUINT32; + else if (priv->problem.connector->sm & SM_PROBLEM_ACK1) + { + if (priv->problem.connector->sm & SM_PROBLEM_WRAP) + { + if (priv->rcv_count == 0) + hn = G_MAXUINT32; + else if (priv->rcv_count == 1) + hn = 0; + else + hn = 1; + } + else + hn = priv->rcv_count; + } + else if (priv->problem.connector->sm & SM_PROBLEM_OVER) + hn = 2; + else + hn = 0; + + h = g_strdup_printf ("%u", hn); + DEBUG ("Acking %s", h); + wocky_node_set_attribute (top, "h", h); + + if (priv->problem.connector->sm & SM_PROBLEM_ACK1 && priv->rcv_count < 1) + { + wocky_xmpp_connection_send_stanza_async (priv->conn, reply, NULL, + NULL, self); + server_enc_outstanding (self); + DEBUG ("waiting for next stanza from client"); + wocky_xmpp_connection_recv_stanza_async (priv->conn, + priv->cancellable, xmpp_handler, self); + } + else + wocky_xmpp_connection_send_stanza_async (priv->conn, reply, NULL, + finished, self); + + g_object_unref (reply); +} + +static void +handle_error (TestConnectorServer *self, + WockyStanza *xml) +{ + TestConnectorServerPrivate *priv = self->priv; + WockyNode *top = wocky_stanza_get_top_node (xml); + WockyNode *err = wocky_node_get_first_child_ns (top, WOCKY_XMPP_NS_SM3); + + if (priv->problem.connector->sm & SM_PROBLEM_OVER) + g_assert_nonnull (err); + else + g_assert_not_reached (); + + DEBUG ("%s", err->name); + g_object_unref (xml); + wocky_xmpp_connection_send_close_async (priv->conn, NULL, finished, self); +} + static void finished (GObject *source, GAsyncResult *result, @@ -1177,6 +1376,8 @@ xmpp_handler (GObject *source, ns = wocky_node_get_ns (wocky_stanza_get_top_node (xml)); name = wocky_stanza_get_top_node (xml)->name; wocky_stanza_get_type_info (xml, &type, &subtype); + if (type == WOCKY_STANZA_TYPE_IQ && priv->rcv_count >= 0) + priv->rcv_count++; /* if we find a handler, the handler is responsible for listening for the next stanza and setting up the next callback in the chain: */ @@ -1267,6 +1468,13 @@ after_auth (GObject *source, if (!(priv->problem.connector->xmpp & XMPP_PROBLEM_CANNOT_BIND)) wocky_node_add_child_ns (node, "bind", WOCKY_XMPP_NS_BIND); + if (priv->problem.connector->sm) + { + wocky_node_add_child_ns (node, "sm", WOCKY_XMPP_NS_SM3); + /* wait for enable */ + server_enc_outstanding (tcs); + } + priv->state = SERVER_STATE_FEATURES_SENT; server_enc_outstanding (tcs); diff --git a/tests/wocky-test-connector-server.h b/tests/wocky-test-connector-server.h index 31787aaa..ed65acdc 100644 --- a/tests/wocky-test-connector-server.h +++ b/tests/wocky-test-connector-server.h @@ -112,6 +112,20 @@ typedef enum XEP77_PROBLEM_CANCEL_STREAM = CONNPROBLEM(12), } XEP77Problem; +typedef enum +{ + SM_PROBLEM_NONE = 0, + SM_PROBLEM_ENABLED = CONNPROBLEM(0), + SM_PROBLEM_ACK0 = CONNPROBLEM(1), + SM_PROBLEM_ACK1 = CONNPROBLEM(2), + SM_PROBLEM_OVER = CONNPROBLEM(3), + SM_PROBLEM_WRAP = CONNPROBLEM(4), + SM_PROBLEM_ACK0_OVER = (CONNPROBLEM(3) | CONNPROBLEM(1)), + SM_PROBLEM_WRAP0 = (CONNPROBLEM(4) | CONNPROBLEM(1)), + SM_PROBLEM_WRAP1 = (CONNPROBLEM(4) | CONNPROBLEM(2)), + SM_PROBLEM_WRAP0_OVER = (CONNPROBLEM(4) | CONNPROBLEM(3) | CONNPROBLEM(1)), +} SMProblem; + typedef enum { CERT_STANDARD, @@ -134,6 +148,7 @@ typedef struct ServerDeath death; JabberProblem jabber; XEP77Problem xep77; + SMProblem sm; } ConnectorProblem; typedef struct _TestConnectorServer TestConnectorServer; From 39b7811aa430e6b793f1a9f77db5a9c558c3cf7e Mon Sep 17 00:00:00 2001 From: "Ruslan N. Marchenko" Date: Fri, 27 Nov 2020 22:40:50 +0100 Subject: [PATCH 10/14] Make libsasl2 optional even for explicit scram mech tests --- tests/wocky-test-sasl-auth-server.c | 35 ++++++++++++++++++++--------- 1 file changed, 25 insertions(+), 10 deletions(-) diff --git a/tests/wocky-test-sasl-auth-server.c b/tests/wocky-test-sasl-auth-server.c index 82d10e0f..17af9e56 100644 --- a/tests/wocky-test-sasl-auth-server.c +++ b/tests/wocky-test-sasl-auth-server.c @@ -1415,6 +1415,7 @@ test_sasl_auth_server_set_mechs (GObject *obj, TestSaslAuthServer *self = TEST_SASL_AUTH_SERVER (obj); TestSaslAuthServerPrivate *priv = self->priv; WockyNode *mechnode = NULL; + const gchar *mech = (must) ? must : priv->mech; gboolean hazmech = FALSE; if (priv->problem != SERVER_PROBLEM_NO_SASL) @@ -1426,11 +1427,6 @@ test_sasl_auth_server_set_mechs (GObject *obj, { /* lalala */ } - else if (priv->mech != NULL) - { - wocky_node_add_child_with_content (mechnode, "mechanism", - priv->mech); - } else { const gchar *mechs; @@ -1448,18 +1444,22 @@ test_sasl_auth_server_set_mechs (GObject *obj, mechlist = g_strsplit (mechs, "\n", -1); for (tmp = mechlist; *tmp != NULL; tmp++) { + if (priv->mech && wocky_strdiff (priv->mech, *tmp)) + continue; + wocky_node_add_child_with_content (mechnode, "mechanism", *tmp); - if (!hazmech && !wocky_strdiff (*tmp, must)) + + if (!hazmech && !wocky_strdiff (*tmp, mech)) hazmech = TRUE; } g_strfreev (mechlist); - if (!hazmech && must != NULL - && g_str_has_prefix (must, "SCRAM-SHA-")) + if (!hazmech && mech != NULL + && g_str_has_prefix (mech, "SCRAM-SHA-")) { /* as said before, this is ridiculous so let's fix that */ - if (g_str_has_prefix (must, "SCRAM-SHA-256")) + if (g_str_has_prefix (mech, "SCRAM-SHA-256")) { if (priv->scram == NULL) priv->scram = g_object_new (WOCKY_TYPE_SASL_SCRAM, @@ -1467,9 +1467,24 @@ test_sasl_auth_server_set_mechs (GObject *obj, "hash-algo", G_CHECKSUM_SHA256, NULL); wocky_node_add_child_with_content (mechnode, - "mechanism", must); + "mechanism", mech); + } + else if (g_str_has_prefix (mech, "SCRAM-SHA-512")) + { + if (priv->scram == NULL) + priv->scram = g_object_new (WOCKY_TYPE_SASL_SCRAM, + "server", "whatever", + "hash-algo", G_CHECKSUM_SHA512, + NULL); + wocky_node_add_child_with_content (mechnode, + "mechanism", mech); } } + else if (!hazmech && priv->mech) + { + wocky_node_add_child_with_content (mechnode, + "mechanism", priv->mech); + } } } return ret; From 7ee2c7cd6ea9db9907a0841a518ab41ab5f9c612 Mon Sep 17 00:00:00 2001 From: "Ruslan N. Marchenko" Date: Wed, 2 Dec 2020 21:54:10 +0100 Subject: [PATCH 11/14] Bump wocky api version to 0.2 due to SM changes --- wocky/wocky.h | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/wocky/wocky.h b/wocky/wocky.h index 7bb8580d..2c6540d5 100644 --- a/wocky/wocky.h +++ b/wocky/wocky.h @@ -90,7 +90,8 @@ #define WOCKY_API_VER_0_0 0x0 #define WOCKY_API_VER_0_1 (G_ENCODE_VERSION(0,1)) -#define WOCKY_API_VERSION WOCKY_API_VER_0_1 +#define WOCKY_API_VER_0_2 (G_ENCODE_VERSION(0,2)) +#define WOCKY_API_VERSION WOCKY_API_VER_0_2 G_BEGIN_DECLS From d98a15d3d90741ec4ea7e02aefa3a3e395fe99ad Mon Sep 17 00:00:00 2001 From: "Ruslan N. Marchenko" Date: Sat, 19 Dec 2020 16:53:44 +0100 Subject: [PATCH 12/14] Allow replacing channel managers in wocky-connector This is required to allow gabble-connection to replace connection= bound managers in existing connector if resume fails and we want to continue reusing the connector for future resumptions. --- wocky/wocky-connector.c | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/wocky/wocky-connector.c b/wocky/wocky-connector.c index e4ac9107..9662e9c8 100644 --- a/wocky/wocky-connector.c +++ b/wocky/wocky-connector.c @@ -494,9 +494,13 @@ wocky_connector_set_property (GObject *object, priv->session_id = g_value_dup_string (value); break; case PROP_AUTH_REGISTRY: + if (priv->auth_registry != NULL) + g_object_unref (priv->auth_registry); priv->auth_registry = g_value_dup_object (value); break; case PROP_TLS_HANDLER: + if (priv->tls_handler != NULL) + g_object_unref (priv->tls_handler); priv->tls_handler = g_value_dup_object (value); break; default: @@ -747,7 +751,7 @@ wocky_connector_class_init (WockyConnectorClass *klass) */ spec = g_param_spec_object ("auth-registry", "Authentication Registry", "Authentication Registry", WOCKY_TYPE_AUTH_REGISTRY, - (G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); + (G_PARAM_CONSTRUCT | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); g_object_class_install_property (oclass, PROP_AUTH_REGISTRY, spec); /** @@ -758,7 +762,7 @@ wocky_connector_class_init (WockyConnectorClass *klass) */ spec = g_param_spec_object ("tls-handler", "TLS Handler", "TLS Handler", WOCKY_TYPE_TLS_HANDLER, - (G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); + (G_PARAM_CONSTRUCT | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); g_object_class_install_property (oclass, PROP_TLS_HANDLER, spec); /** From 48050c3b55a140304bb0cf2ca3baae88e6e566ac Mon Sep 17 00:00:00 2001 From: "Ruslan N. Marchenko" Date: Tue, 16 Feb 2021 23:09:56 +0100 Subject: [PATCH 13/14] Force reconnect on hb timeout --- wocky/wocky-c2s-porter.c | 45 +++++++++++++++++++++++++++++++++++----- 1 file changed, 40 insertions(+), 5 deletions(-) diff --git a/wocky/wocky-c2s-porter.c b/wocky/wocky-c2s-porter.c index 2ecdfd36..683ac350 100644 --- a/wocky/wocky-c2s-porter.c +++ b/wocky/wocky-c2s-porter.c @@ -2653,6 +2653,33 @@ send_whitespace_ping_cb (GObject *source, g_object_unref (task); } +static void +force_close_conn_cb (GObject *source, + GAsyncResult *res, + gpointer user_data) +{ + WockyXmppConnection *conn = WOCKY_XMPP_CONNECTION (source); + GTask *task = G_TASK (user_data); + WockyC2SPorter *self = WOCKY_C2S_PORTER (g_task_get_source_object (task)); + WockyC2SPorterPrivate *priv = wocky_c2s_porter_get_instance_private (self); + GError *error = NULL; + + priv->sending_blocked = FALSE; + + if (wocky_xmpp_connection_force_close_finish (conn, res, &error)) + g_task_return_boolean (task, TRUE); + else + { + DEBUG ("Error closing connection: %s", error->message); + g_task_return_error (task, error); + } + + g_object_unref (task); + + /* reset unacked request number, those are lost */ + priv->sm->reqs = 0; +} + /** * wocky_c2s_porter_send_whitespace_ping_async: * @self: a #WockyC2SPorter @@ -2698,11 +2725,19 @@ wocky_c2s_porter_send_whitespace_ping_async (WockyC2SPorter *self, * acks hence need to catch-up the latest acks when idle. */ if (priv->sm && priv->sm->enabled) { - WockyStanza *r = wocky_stanza_new ("r", WOCKY_XMPP_NS_SM3); - wocky_xmpp_connection_send_stanza_async (priv->connection, r, - cancellable, send_whitespace_ping_cb, g_object_ref (task)); - g_object_unref (r); - priv->sm->reqs++; + if (priv->sm->reqs++ < 2) + { + WockyStanza *r = wocky_stanza_new ("r", WOCKY_XMPP_NS_SM3); + DEBUG ("Ack pressure: %d", priv->sm->reqs); + wocky_xmpp_connection_send_stanza_async (priv->connection, r, + cancellable, send_whitespace_ping_cb, g_object_ref (task)); + g_object_unref (r); + } + else + { + wocky_xmpp_connection_force_close_async (priv->connection, + cancellable, force_close_conn_cb, g_object_ref (task)); + } } else { From bf6796fe7ddd2419e8dd02629c3afd88b82090cf Mon Sep 17 00:00:00 2001 From: Ferdinand Stehle Date: Sat, 20 Feb 2021 15:21:30 +0100 Subject: [PATCH 14/14] Fix warnings when compiling for Sailfish OS * use portable integer format specifiers: - tests/wocky-sm-test.c - wocky/wocky-c2s-porter.c - wocky/wocky-sasl-scram.c * tests/wocky-test-sasl-auth-server.c: - avoid non init warning - add missing define statement --- tests/wocky-sm-test.c | 2 +- tests/wocky-test-sasl-auth-server.c | 9 +++++---- wocky/wocky-c2s-porter.c | 10 +++++----- wocky/wocky-sasl-scram.c | 2 +- 4 files changed, 12 insertions(+), 11 deletions(-) diff --git a/tests/wocky-sm-test.c b/tests/wocky-sm-test.c index 15affc2b..26dc8aab 100644 --- a/tests/wocky-sm-test.c +++ b/tests/wocky-sm-test.c @@ -622,7 +622,7 @@ iq_result (GObject *source, g_assert_cmpint (smc->snt_count, ==, 1); g_assert_cmpint (smc->rcv_count, ==, 1); - g_debug ("Acked: %lu", smc->snt_acked); + g_debug ("Acked: %zu", smc->snt_acked); wocky_c2s_porter_send_whitespace_ping_async (WOCKY_C2S_PORTER (test->porter), NULL, smreqd, test); } diff --git a/tests/wocky-test-sasl-auth-server.c b/tests/wocky-test-sasl-auth-server.c index 17af9e56..a28a52a0 100644 --- a/tests/wocky-test-sasl-auth-server.c +++ b/tests/wocky-test-sasl-auth-server.c @@ -55,6 +55,7 @@ typedef int (*sasl_callback_ft)(void); #else #define SASL_OK 0 +#define SASL_BADPROT -5 #define SASL_BADAUTH -13 #define SASL_NOUSER -20 #define CHECK_SASL_RETURN(x) \ @@ -720,8 +721,8 @@ handle_auth (TestSaslAuthServer *self, WockyStanza *stanza) { TestSaslAuthServerPrivate *priv = self->priv; guchar *response = NULL; - const gchar *challenge; - unsigned challenge_len; + const gchar *challenge = NULL; + unsigned challenge_len = 0; gsize response_len = 0; int ret; WockyNode *auth = wocky_stanza_get_top_node (stanza); @@ -1036,8 +1037,8 @@ handle_response (TestSaslAuthServer *self, WockyStanza *stanza) { TestSaslAuthServerPrivate * priv = self->priv; guchar *response = NULL; - const gchar *challenge; - unsigned challenge_len; + const gchar *challenge = NULL; + unsigned challenge_len = 0; gsize response_len = 0; int ret; diff --git a/wocky/wocky-c2s-porter.c b/wocky/wocky-c2s-porter.c index 683ac350..cbb9010d 100644 --- a/wocky/wocky-c2s-porter.c +++ b/wocky/wocky-c2s-porter.c @@ -1486,7 +1486,7 @@ resume_connection (WockyC2SPorter *self) WockyC2SPorterPrivate *priv = wocky_c2s_porter_get_instance_private (self); WockyStanza *resume = wocky_stanza_new ("resume", WOCKY_XMPP_NS_SM3); WockyNode *rn = wocky_stanza_get_top_node (resume); - gchar *val = g_strdup_printf ("%lu", priv->sm->rcv_count); + gchar *val = g_strdup_printf ("%zu", priv->sm->rcv_count); gboolean ret = TRUE; g_assert (priv->sm); @@ -1634,7 +1634,7 @@ wocky_porter_sm_handle_h (WockyC2SPorter *self, ':', WOCKY_XMPP_NS_SM3, ')', NULL); - DEBUG ("Invalid acknowledgement %lu, must be between %lu and %lu", + DEBUG ("Invalid acknowledgement %zu, must be between %zu and %zu", snt, priv->sm->snt_acked, priv->sm->snt_count); wocky_porter_send_async (WOCKY_PORTER (self), error, priv->receive_cancellable, wocky_porter_sm_error_cb, self); @@ -1643,7 +1643,7 @@ wocky_porter_sm_handle_h (WockyC2SPorter *self, return FALSE; } - DEBUG ("Acking %lu stanzas handled by the server", snt); + DEBUG ("Acking %zu stanzas handled by the server", snt); priv->sm->snt_acked = snt; /* now we can head-drop stanzas from unacked_queue till its length is * (snt_count - snt_acked) */ @@ -1654,7 +1654,7 @@ wocky_porter_sm_handle_h (WockyC2SPorter *self, g_assert (s); g_object_unref (s); } - DEBUG ("After ack %u(%lu) stanzas left in the queue", + DEBUG ("After ack %u(%zu) stanzas left in the queue", g_queue_get_length (priv->unacked_queue), ACK_WINDOW (priv->sm->snt_acked, priv->sm->snt_count)); return TRUE; @@ -1693,7 +1693,7 @@ wocky_porter_sm_handle (WockyC2SPorter *self, { WockyStanza *a = wocky_stanza_new ("a", WOCKY_XMPP_NS_SM3); WockyNode *an = wocky_stanza_get_top_node (a); - gchar *val = g_strdup_printf ("%lu", priv->sm->rcv_count); + gchar *val = g_strdup_printf ("%zu", priv->sm->rcv_count); wocky_node_set_attribute (an, "h", val); g_free (val); diff --git a/wocky/wocky-sasl-scram.c b/wocky/wocky-sasl-scram.c index b1ef2446..b0f51a0b 100644 --- a/wocky/wocky-sasl-scram.c +++ b/wocky/wocky-sasl-scram.c @@ -882,7 +882,7 @@ wocky_sasl_scram_server_start_finish (WockySaslScram *self, g_assert (priv->nonce == NULL); priv->nonce = sasl_generate_base64_nonce (); - priv->server_first_bare = g_strdup_printf ("r=%s%s,s=%s,i=%lu", priv->client_nonce, + priv->server_first_bare = g_strdup_printf ("r=%s%s,s=%s,i=%"G_GUINT64_FORMAT"", priv->client_nonce, priv->nonce, priv->salt, priv->iterations); priv->state = WOCKY_SASL_SCRAM_STATE_SERVER_FIRST_MESSAGE; }