diff options
author | defanor <defanor@uberspace.net> | 2020-02-29 07:50:32 +0300 |
---|---|---|
committer | defanor <defanor@uberspace.net> | 2020-02-29 07:50:32 +0300 |
commit | 239248f77bb21139b158950a548ff84ada4b3bf1 (patch) | |
tree | c489e2c16703bff37b984772a5ab87dd57015594 /src/rexmpp.c |
Add the draft
Diffstat (limited to 'src/rexmpp.c')
-rw-r--r-- | src/rexmpp.c | 1580 |
1 files changed, 1580 insertions, 0 deletions
diff --git a/src/rexmpp.c b/src/rexmpp.c new file mode 100644 index 0000000..04ec798 --- /dev/null +++ b/src/rexmpp.c @@ -0,0 +1,1580 @@ +/** + @file rexmpp.c + @brief rexmpp, a reusable XMPP IM client library. + @author defanor <defanor@uberspace.net> + @date 2020 + @copyright MIT license. +*/ + +#include <string.h> +#include <sys/time.h> +#include <errno.h> +#include <syslog.h> +#include <arpa/nameser.h> + +#include <ares.h> +#include <libxml/tree.h> +#include <libxml/xmlsave.h> +#include <gnutls/gnutls.h> +#include <gnutls/x509.h> +#include <gsasl.h> + +#include "rexmpp.h" +#include "rexmpp_tcp.h" + + +void rexmpp_sax_start_elem_ns (rexmpp_t *s, + const char *localname, + const char *prefix, + const char *URI, + int nb_namespaces, + const char **namespaces, + int nb_attributes, + int nb_defaulted, + const char **attributes); + +void rexmpp_sax_end_elem_ns(rexmpp_t *s, + const char *localname, + const char *prefix, + const char *URI); + +void rexmpp_sax_characters (rexmpp_t *s, const char * ch, int len); + +void rexmpp_log (rexmpp_t *s, int priority, const char *format, ...) +{ + va_list args; + if (s->log_function != NULL) { + va_start(args, format); + s->log_function (s, priority, format, args); + va_end(args); + } +} + +rexmpp_err_t rexmpp_init (rexmpp_t *s, + const char *jid, + log_function_t log_function, + sasl_property_cb_t sasl_property_cb, + xml_in_cb_t xml_in_cb, + xml_out_cb_t xml_out_cb) +{ + int err; + xmlSAXHandler sax = { + .initialized = XML_SAX2_MAGIC, + .characters = (charactersSAXFunc)rexmpp_sax_characters, + .startElementNs = (startElementNsSAX2Func)rexmpp_sax_start_elem_ns, + .endElementNs = (endElementNsSAX2Func)rexmpp_sax_end_elem_ns, + }; + + s->tcp_state = REXMPP_TCP_NONE; + s->resolver_state = REXMPP_RESOLVER_NONE; + s->stream_state = REXMPP_STREAM_NONE; + s->tls_state = REXMPP_TLS_INACTIVE; + s->sasl_state = REXMPP_SASL_INACTIVE; + s->sm_state = REXMPP_SM_INACTIVE; + s->carbons_state = REXMPP_CARBONS_INACTIVE; + s->send_buffer = NULL; + s->send_queue = NULL; + s->server_srv = NULL; + s->server_srv_cur = NULL; + s->server_srv_tls = NULL; + s->server_srv_tls_cur = NULL; + s->server_socket = -1; + s->current_element_root = NULL; + s->current_element = NULL; + s->stream_features = NULL; + s->stanza_queue = NULL; + s->stream_id = NULL; + s->active_iq = NULL; + s->tls_session_data = NULL; + s->tls_session_data_size = 0; + s->id_counter = 0; + s->reconnect_number = 0; + s->next_reconnect_time.tv_sec = 0; + s->next_reconnect_time.tv_usec = 0; + s->initial_jid = jid; + s->assigned_jid = NULL; + s->stanza_queue_size = 1024; + s->send_queue_size = 1024; + s->iq_queue_size = 1024; + s->log_function = log_function; + s->sasl_property_cb = sasl_property_cb; + s->xml_in_cb = xml_in_cb; + s->xml_out_cb = xml_out_cb; + + s->xml_parser = xmlCreatePushParserCtxt(&sax, s, "", 0, NULL); + + if (s->xml_parser == NULL) { + rexmpp_log(s, LOG_CRIT, "Failed to create an XML parser context."); + return REXMPP_E_XML; + } + + err = ares_library_init(ARES_LIB_INIT_ALL); + if (err != 0) { + rexmpp_log(s, LOG_CRIT, "ares library initialisation error: %s", + ares_strerror(err)); + xmlFreeParserCtxt(s->xml_parser); + return REXMPP_E_DNS; + } + + err = ares_init(&(s->resolver_channel)); + if (err) { + rexmpp_log(s, LOG_CRIT, "ares channel initialisation error: %s", + ares_strerror(err)); + ares_library_cleanup(); + xmlFreeParserCtxt(s->xml_parser); + return REXMPP_E_DNS; + } + + err = gnutls_certificate_allocate_credentials(&(s->gnutls_cred)); + if (err) { + rexmpp_log(s, LOG_CRIT, "gnutls credentials allocation error: %s", + gnutls_strerror(err)); + ares_destroy(s->resolver_channel); + ares_library_cleanup(); + xmlFreeParserCtxt(s->xml_parser); + return REXMPP_E_TLS; + } + + err = gsasl_init(&(s->sasl_ctx)); + if (err) { + rexmpp_log(s, LOG_CRIT, "gsasl initialisation error: %s", + gsasl_strerror(err)); + gnutls_certificate_free_credentials(s->gnutls_cred); + ares_destroy(s->resolver_channel); + ares_library_cleanup(); + xmlFreeParserCtxt(s->xml_parser); + return REXMPP_E_SASL; + } + gsasl_callback_set(s->sasl_ctx, s->sasl_property_cb); + + return REXMPP_SUCCESS; +} + +/* Prepares for a reconnect: cleans up some things (e.g., SASL and TLS + structures), but keeps others (e.g., stanza queue and stream ID, + since we may resume the stream afterwards). */ +void rexmpp_cleanup (rexmpp_t *s) { + if (s->tls_state != REXMPP_TLS_INACTIVE) { + gnutls_deinit(s->gnutls_session); + s->tls_state = REXMPP_TLS_INACTIVE; + } + if (s->sasl_state != REXMPP_SASL_INACTIVE) { + gsasl_finish(s->sasl_session); + s->sasl_session = NULL; + s->sasl_state = REXMPP_SASL_INACTIVE; + } + if (s->tcp_state == REXMPP_TCP_CONNECTING) { + int sock = rexmpp_tcp_conn_finish(&s->server_connection); + if (sock != -1) { + close(sock); + } + s->tcp_state = REXMPP_TCP_NONE; + } + if (s->server_socket != -1) { + close(s->server_socket); + s->server_socket = -1; + } + if (s->send_buffer != NULL) { + free(s->send_buffer); + s->send_buffer = NULL; + } + if (s->stream_features != NULL) { + xmlFreeNode(s->stream_features); + s->stream_features = NULL; + } + while (s->send_queue != NULL) { + xmlNodePtr next = xmlNextElementSibling(s->send_queue); + xmlFreeNode(s->send_queue); + s->send_queue = next; + } + if (s->current_element_root != NULL) { + xmlFreeNode(s->current_element_root); + s->current_element_root = NULL; + s->current_element = NULL; + } + if (s->server_srv != NULL) { + ares_free_data(s->server_srv); + s->server_srv = NULL; + s->server_srv_cur = NULL; + } + if (s->server_srv_tls != NULL) { + ares_free_data(s->server_srv_tls); + s->server_srv_tls = NULL; + s->server_srv_tls_cur = NULL; + } + s->sm_state = REXMPP_SM_INACTIVE; + if (s->carbons_state != REXMPP_CARBONS_DISABLED) { + s->carbons_state = REXMPP_CARBONS_INACTIVE; + } +} + +/* Frees the things that persist through reconnects. */ +void rexmpp_done (rexmpp_t *s) { + rexmpp_cleanup(s); + gsasl_done(s->sasl_ctx); + gnutls_certificate_free_credentials(s->gnutls_cred); + ares_destroy(s->resolver_channel); + ares_library_cleanup(); + xmlFreeParserCtxt(s->xml_parser); + if (s->stream_id != NULL) { + free(s->stream_id); + s->stream_id = NULL; + } + while (s->stanza_queue != NULL) { + xmlNodePtr next = xmlNextElementSibling(s->stanza_queue); + xmlFreeNode(s->send_queue); + s->send_queue = next; + } + while (s->active_iq != NULL) { + rexmpp_iq_t *next = s->active_iq->next; + xmlFreeNode(s->active_iq->request); + free(s->active_iq); + s->active_iq = next; + } + if (s->tls_session_data != NULL) { + free(s->tls_session_data); + } +} + +void rexmpp_schedule_reconnect(rexmpp_t *s) { + gettimeofday(&(s->next_reconnect_time), NULL); + if (s->reconnect_number < 12) { + s->next_reconnect_time.tv_sec += (2 << s->reconnect_number); + } else { + s->next_reconnect_time.tv_sec += 3600; + } + rexmpp_log(s, LOG_DEBUG, "Scheduled reconnect number %d, for %u.%u", + s->reconnect_number, + s->next_reconnect_time.tv_sec, s->next_reconnect_time.tv_usec); + s->reconnect_number++; +} + + +const char *jid_bare_to_host (const char *jid_bare) { + char *jid_host; + jid_host = strchr(jid_bare, '@'); + if (jid_host != NULL) { + return jid_host + 1; + } + return NULL; +} + +xmlNodePtr rexmpp_xml_add_id (rexmpp_t *s, xmlNodePtr node) { + char buf[11]; + snprintf(buf, 11, "%u", s->id_counter); + s->id_counter++; + xmlNewProp(node, "id", buf); + return node; +} + +unsigned int rexmpp_xml_siblings_count (xmlNodePtr node) { + unsigned int i; + for (i = 0; node != NULL; i++) { + node = xmlNextElementSibling(node); + } + return i; +} + +int rexmpp_xml_match (xmlNodePtr node, + const char *namespace, + const char *name) +{ + if (node == NULL) { + return 0; + } + if (name != NULL) { + if (strcmp(name, node->name) != 0) { + return 0; + } + } + if (namespace != NULL) { + if (node->ns == NULL) { + if (strcmp(namespace, "jabber:client") != 0) { + return 0; + } + } else { + if (strcmp(namespace, node->ns->href) != 0) { + return 0; + } + } + } + return 1; +} + +xmlNodePtr rexmpp_xml_find_child (xmlNodePtr node, + const char *namespace, + const char *name) +{ + if (node == NULL) { + return NULL; + } + xmlNodePtr child; + for (child = xmlFirstElementChild(node); + child != NULL; + child = xmlNextElementSibling(child)) + { + if (rexmpp_xml_match(child, namespace, name)) { + return child; + } + } + return NULL; +} + +xmlNodePtr rexmpp_xml_set_delay (rexmpp_t *s, xmlNodePtr node) { + if (rexmpp_xml_find_child (node, NULL, "delay")) { + return node; + } + char buf[42]; + time_t t = time(NULL); + struct tm *local_time = localtime(&t); + strftime(buf, 42, "%FT%T%z", local_time); + xmlNodePtr delay = xmlNewChild(node, NULL, "delay", NULL); + xmlNewProp(delay, "stamp", buf); + if (s != NULL && s->assigned_jid != NULL) { + xmlNewProp(delay, "from", s->assigned_jid); + } + return node; +} + +char *rexmpp_xml_serialize(xmlNodePtr node) { + xmlBufferPtr buf = xmlBufferCreate(); + xmlSaveCtxtPtr ctx = xmlSaveToBuffer(buf, "utf-8", 0); + xmlSaveTree(ctx, node); + xmlSaveFlush(ctx); + unsigned char *out = xmlBufferDetach(buf); + xmlBufferFree(buf); + return out; +} + +int rexmpp_xml_is_stanza (xmlNodePtr node) { + return rexmpp_xml_match(node, "jabber:client", "message") || + rexmpp_xml_match(node, "jabber:client", "iq") || + rexmpp_xml_match(node, "jabber:client", "presence"); +} + + +rexmpp_err_t rexmpp_send_start (rexmpp_t *s, const void *data, size_t data_len) +{ + int sasl_err; + if (s->send_buffer != NULL) { + rexmpp_log(s, LOG_CRIT, "send buffer is not empty: %s", s->send_buffer); + return REXMPP_E_SEND_BUFFER_NOT_EMPTY; + } + if (s->sasl_state == REXMPP_SASL_ACTIVE) { + sasl_err = gsasl_encode (s->sasl_session, data, data_len, + &(s->send_buffer), &(s->send_buffer_len)); + if (sasl_err != GSASL_OK) { + rexmpp_log(s, LOG_ERR, "SASL encoding error: %s", + gsasl_strerror(sasl_err)); + s->sasl_state = REXMPP_SASL_ERROR; + return REXMPP_E_SASL; + } + } else { + s->send_buffer = malloc(data_len); + if (s->send_buffer == NULL) { + return REXMPP_E_MALLOC; + } + memcpy(s->send_buffer, data, data_len); + s->send_buffer_len = data_len; + } + s->send_buffer_sent = 0; + return REXMPP_E_AGAIN; +} + +rexmpp_err_t rexmpp_send_continue (rexmpp_t *s) +{ + if (s->send_buffer == NULL) { + rexmpp_log(s, LOG_ERR, "nothing to send"); + return REXMPP_E_SEND_BUFFER_EMPTY; + } + int ret; + while (1) { + if (s->tls_state == REXMPP_TLS_ACTIVE) { + ret = gnutls_record_send (s->gnutls_session, + s->send_buffer, + s->send_buffer_len); + } else { + ret = send (s->server_socket, + s->send_buffer + s->send_buffer_sent, + s->send_buffer_len - s->send_buffer_sent, + 0); + } + if (ret > 0) { + s->send_buffer_sent += ret; + if (s->send_buffer_sent == s->send_buffer_len) { + free(s->send_buffer); + s->send_buffer = NULL; + if (s->send_queue != NULL) { + xmlNodePtr node = s->send_queue; + unsigned char *buf = rexmpp_xml_serialize(node); + ret = rexmpp_send_start(s, buf, strlen(buf)); + free(buf); + if (ret != REXMPP_E_AGAIN) { + return ret; + } + s->send_queue = xmlNextElementSibling(s->send_queue); + xmlFreeNode(node); + } else { + return REXMPP_SUCCESS; + } + } + } else { + if (s->tls_state == REXMPP_TLS_ACTIVE) { + if (ret != GNUTLS_E_AGAIN) { + s->tls_state = REXMPP_TLS_ERROR; + /* Assume a TCP error for now as well. */ + s->tcp_state = REXMPP_TCP_ERROR; + rexmpp_log(s, LOG_ERR, "TLS send error: %s", gnutls_strerror(ret)); + rexmpp_cleanup(s); + rexmpp_schedule_reconnect(s); + return REXMPP_E_TLS; + } + } else { + if (errno != EAGAIN) { + s->tcp_state = REXMPP_TCP_ERROR; + rexmpp_log(s, LOG_ERR, "TCP send error: %s", strerror(errno)); + rexmpp_cleanup(s); + rexmpp_schedule_reconnect(s); + return REXMPP_E_TCP; + } + } + return REXMPP_E_AGAIN; + } + } +} + +rexmpp_err_t rexmpp_send_raw (rexmpp_t *s, const void *data, size_t data_len) +{ + int ret = rexmpp_send_start(s, data, data_len); + if (ret != REXMPP_E_AGAIN) { + return ret; + } + return rexmpp_send_continue(s); +} + +rexmpp_err_t rexmpp_sm_send_req (rexmpp_t *s); + +rexmpp_err_t rexmpp_send (rexmpp_t *s, xmlNodePtr node) +{ + int need_ack = 0; + int ret; + + if (s->xml_out_cb != NULL && s->xml_out_cb(s, node) == 1) { + xmlFreeNode(node); + rexmpp_log(s, LOG_WARNING, "Message sending was cancelled by xml_out_cb."); + return REXMPP_E_CANCELLED; + } + + if (rexmpp_xml_siblings_count(s->send_queue) >= s->send_queue_size) { + xmlFreeNode(node); + rexmpp_log(s, LOG_ERR, "The send queue is full, not sending."); + return REXMPP_E_SEND_QUEUE_FULL; + } + + if (rexmpp_xml_is_stanza(node)) { + if (s->sm_state == REXMPP_SM_ACTIVE) { + if (s->stanzas_out_count - s->stanzas_out_acknowledged >= + s->stanza_queue_size) { + xmlFreeNode(node); + rexmpp_log(s, LOG_ERR, "The stanza queue is full, not sending."); + return REXMPP_E_STANZA_QUEUE_FULL; + } + need_ack = 1; + xmlNodePtr queued_stanza = rexmpp_xml_set_delay(s, xmlCopyNode(node, 1)); + if (s->stanza_queue == NULL) { + s->stanza_queue = queued_stanza; + } else { + xmlNodePtr last = s->stanza_queue; + while (xmlNextElementSibling(last) != NULL) { + last = xmlNextElementSibling(last); + } + xmlAddNextSibling(last, queued_stanza); + } + } + if (s->sm_state != REXMPP_SM_INACTIVE) { + s->stanzas_out_count++; + } + } + + if (s->send_buffer == NULL) { + unsigned char *buf = rexmpp_xml_serialize(node); + ret = rexmpp_send_raw(s, buf, strlen(buf)); + free(buf); + xmlFreeNode(node); + if (ret != REXMPP_SUCCESS && ret != REXMPP_E_AGAIN) { + return ret; + } + } else { + if (s->send_queue == NULL) { + s->send_queue = node; + } else { + xmlNodePtr last = s->send_queue; + while (xmlNextElementSibling(last) != NULL) { + last = xmlNextElementSibling(last); + } + xmlAddNextSibling(last, node); + } + ret = REXMPP_E_AGAIN; + } + if (need_ack) { + return rexmpp_sm_send_req(s); + } + return ret; +} + + +void rexmpp_iq_new (rexmpp_t *s, + const char *type, + const char *to, + xmlNodePtr payload, + rexmpp_iq_callback_t cb) +{ + unsigned int i; + rexmpp_iq_t *prev = NULL, *last = s->active_iq; + for (i = 0; last != NULL && last->next != NULL; i++) { + prev = last; + last = last->next; + } + if (i >= s->iq_queue_size && s->iq_queue_size > 0) { + rexmpp_log(s, LOG_WARNING, + "The IQ queue limit is reached, giving up on the oldest IQ."); + prev->next = NULL; + if (last->cb != NULL) { + last->cb(s, last->request, NULL); + } + xmlFreeNode(last->request); + free(last); + } + + xmlNodePtr iq_stanza = rexmpp_xml_add_id(s, xmlNewNode(NULL, "iq")); + xmlNewNs(iq_stanza, "jabber:client", NULL); + xmlNewProp(iq_stanza, "type", type); + if (to != NULL) { + xmlNewProp(iq_stanza, "to", to); + } + if (s->assigned_jid != NULL) { + xmlNewProp(iq_stanza, "from", s->assigned_jid); + } + xmlAddChild(iq_stanza, payload); + rexmpp_iq_t *iq = malloc(sizeof(rexmpp_iq_t)); + iq->request = xmlCopyNode(iq_stanza, 1); + iq->cb = cb; + iq->next = s->active_iq; + s->active_iq = iq; + rexmpp_send(s, iq_stanza); +} + +rexmpp_err_t rexmpp_sm_ack (rexmpp_t *s) { + char buf[11]; + xmlNodePtr ack = xmlNewNode(NULL, "a"); + xmlNewNs(ack, "urn:xmpp:sm:3", NULL); + snprintf(buf, 11, "%u", s->stanzas_in_count); + xmlNewProp(ack, "h", buf); + return rexmpp_send(s, ack); +} + +rexmpp_err_t rexmpp_sm_send_req (rexmpp_t *s) { + xmlNodePtr ack = xmlNewNode(NULL, "r"); + xmlNewNs(ack, "urn:xmpp:sm:3", NULL); + return rexmpp_send(s, ack); +} + +void rexmpp_recv (rexmpp_t *s) { + char chunk_raw[4096], *chunk; + ssize_t chunk_raw_len, chunk_len; + int sasl_err; + /* Loop here in order to consume data from TLS buffers, which + wouldn't show up on select(). */ + do { + if (s->tls_state == REXMPP_TLS_ACTIVE) { + chunk_raw_len = gnutls_record_recv(s->gnutls_session, chunk_raw, 4096); + } else { + chunk_raw_len = recv(s->server_socket, chunk_raw, 4096, 0); + } + if (chunk_raw_len > 0) { + if (s->sasl_state == REXMPP_SASL_ACTIVE) { + sasl_err = gsasl_decode(s->sasl_session, chunk_raw, chunk_raw_len, + &chunk, &chunk_len); + if (sasl_err != GSASL_OK) { + rexmpp_log(s, LOG_ERR, "SASL decoding error: %s", + gsasl_strerror(sasl_err)); + s->sasl_state = REXMPP_SASL_ERROR; + return; + } + } else { + chunk = chunk_raw; + chunk_len = chunk_raw_len; + } + xmlParseChunk(s->xml_parser, chunk, chunk_len, 0); + } else if (chunk_raw_len == 0) { + if (s->tls_state == REXMPP_TLS_ACTIVE) { + s->tls_state = REXMPP_TLS_CLOSED; + rexmpp_log(s, LOG_INFO, "TLS disconnected"); + } + s->tcp_state = REXMPP_TCP_CLOSED; + rexmpp_log(s, LOG_INFO, "TCP disconnected"); + rexmpp_cleanup(s); + if (s->stream_state == REXMPP_STREAM_READY) { + rexmpp_schedule_reconnect(s); + } + } else { + if (s->tls_state == REXMPP_TLS_ACTIVE) { + if (chunk_raw_len != GNUTLS_E_AGAIN) { + s->tls_state = REXMPP_TLS_ERROR; + /* Assume a TCP error for now as well. */ + s->tcp_state = REXMPP_TCP_ERROR; + rexmpp_log(s, LOG_ERR, "TLS recv error: %s", + gnutls_strerror(chunk_raw_len)); + rexmpp_cleanup(s); + rexmpp_schedule_reconnect(s); + } + } else if (errno != EAGAIN) { + s->tcp_state = REXMPP_TCP_ERROR; + rexmpp_log(s, LOG_ERR, "TCP recv error: %s", strerror(errno)); + rexmpp_cleanup(s); + rexmpp_schedule_reconnect(s); + } + } + } while (chunk_raw_len > 0); +} + +int rexmpp_stream_open (rexmpp_t *s) { + char buf[2048]; + snprintf(buf, 2048, + "<?xml version='1.0'?>\n" + "<stream:stream to='%s' version='1.0' " + "xml:lang='en' xmlns='jabber:client' " + "xmlns:stream='http://etherx.jabber.org/streams'>", + jid_bare_to_host(s->initial_jid)); + s->stream_state = REXMPP_STREAM_OPENING; + rexmpp_send_raw(s, buf, strlen(buf)); + + return 0; +} + +void rexmpp_process_conn_err (rexmpp_t *s, int err); + +void rexmpp_try_next_host (rexmpp_t *s) { + const char *host; + int port; + /* todo: check priorities and weights */ + s->tls_state = REXMPP_TLS_INACTIVE; + if (s->server_srv_tls != NULL && s->server_srv_tls_cur == NULL) { + /* We have xmpps-client records available, but haven't tried any + of them yet. */ + s->server_srv_tls_cur = s->server_srv_tls; + host = s->server_srv_tls_cur->host; + port = s->server_srv_tls_cur->port; + s->tls_state = REXMPP_TLS_AWAITING_DIRECT; + } else if (s->server_srv_tls_cur != NULL && + s->server_srv_tls_cur->next != NULL) { + /* We have tried some xmpps-client records, but there is more. */ + s->server_srv_tls_cur = s->server_srv_tls_cur->next; + host = s->server_srv_tls_cur->host; + port = s->server_srv_tls_cur->port; + s->tls_state = REXMPP_TLS_AWAITING_DIRECT; + } else if (s->server_srv != NULL && s->server_srv_cur == NULL) { + /* Starting with xmpp-client records. */ + s->server_srv_cur = s->server_srv; + host = s->server_srv_cur->host; + port = s->server_srv_cur->port; + } else if (s->server_srv_tls_cur != NULL && + s->server_srv_tls_cur->next != NULL) { + /* Advancing in xmpp-client records. */ + s->server_srv_cur = s->server_srv_cur->next; + host = s->server_srv_cur->host; + port = s->server_srv_cur->port; + } else { + /* No candidate records left to try. Schedule a reconnect. */ + rexmpp_cleanup(s); + rexmpp_schedule_reconnect(s); + return; + } + rexmpp_log(s, LOG_DEBUG, "Connecting to %s:%d", host, port); + rexmpp_process_conn_err(s, + rexmpp_tcp_conn_init(&s->server_connection, + host, port)); +} + +void rexmpp_tls_handshake (rexmpp_t *s) { + s->tls_state = REXMPP_TLS_HANDSHAKE; + int ret = gnutls_handshake(s->gnutls_session); + if (ret == GNUTLS_E_AGAIN) { + rexmpp_log(s, LOG_DEBUG, "Waiting for TLS handshake to complete"); + } else if (ret == 0) { + rexmpp_log(s, LOG_DEBUG, "TLS ready"); + s->tls_state = REXMPP_TLS_ACTIVE; + + if (gnutls_session_is_resumed(s->gnutls_session)) { + rexmpp_log(s, LOG_INFO, "TLS session is resumed"); + } else { + if (s->tls_session_data != NULL) { + rexmpp_log(s, LOG_DEBUG, "TLS session is not resumed"); + free(s->tls_session_data); + s->tls_session_data = NULL; + } + gnutls_session_get_data(s->gnutls_session, NULL, + &s->tls_session_data_size); + s->tls_session_data = malloc(s->tls_session_data_size); + ret = gnutls_session_get_data(s->gnutls_session, s->tls_session_data, + &s->tls_session_data_size); + if (ret != GNUTLS_E_SUCCESS) { + rexmpp_log(s, LOG_ERR, "Failed to get TLS session data: %s", + gnutls_strerror(ret)); + } + } + + if (s->stream_state == REXMPP_STREAM_NONE) { + /* It's a direct TLS connection, so open a stream after + connecting. */ + rexmpp_stream_open(s); + } else { + /* A STARTTLS connection, restart the stream. */ + s->stream_state = REXMPP_STREAM_RESTART; + } + + } else { + rexmpp_log(s, LOG_ERR, "Unexpected TLS handshake error: %s", + gnutls_strerror(ret)); + if (s->stream_state == REXMPP_STREAM_NONE) { + /* It was a direct TLS connection attempt: cleanup the session, + continue connection attempts. */ + gnutls_deinit(s->gnutls_session); + s->tls_state = REXMPP_TLS_INACTIVE; + rexmpp_try_next_host(s); + } else { + rexmpp_cleanup(s); + } + } +} + +void rexmpp_tls_start (rexmpp_t *s) { + gnutls_datum_t xmpp_client_protocol = {"xmpp-client", strlen("xmpp-client")}; + rexmpp_log(s, LOG_DEBUG, "starting TLS"); + gnutls_init(&s->gnutls_session, GNUTLS_CLIENT); + gnutls_session_set_ptr(s->gnutls_session, s); + gnutls_alpn_set_protocols(s->gnutls_session, &xmpp_client_protocol, 1, 0); + gnutls_server_name_set(s->gnutls_session, GNUTLS_NAME_DNS, + jid_bare_to_host(s->initial_jid), + strlen(jid_bare_to_host(s->initial_jid))); + gnutls_set_default_priority(s->gnutls_session); + gnutls_credentials_set(s->gnutls_session, GNUTLS_CRD_CERTIFICATE, + s->gnutls_cred); + gnutls_transport_set_int(s->gnutls_session, s->server_socket); + gnutls_handshake_set_timeout(s->gnutls_session, + GNUTLS_DEFAULT_HANDSHAKE_TIMEOUT); + if (s->tls_session_data != NULL) { + int ret = gnutls_session_set_data(s->gnutls_session, + s->tls_session_data, + s->tls_session_data_size); + if (ret != GNUTLS_E_SUCCESS) { + rexmpp_log(s, LOG_ERR, "Failed to set TLS session data: %s", + gnutls_strerror(ret)); + } + } + s->tls_state = REXMPP_TLS_HANDSHAKE; + rexmpp_tls_handshake(s); +} + + +void rexmpp_process_conn_err (rexmpp_t *s, int err) { + s->tcp_state = REXMPP_TCP_CONNECTING; + if (err == REXMPP_CONN_DONE) { + rexmpp_log(s, LOG_INFO, "Established a TCP connection"); + s->reconnect_number = 0; + xmlCtxtResetPush(s->xml_parser, "", 0, "", "utf-8"); + s->server_socket = rexmpp_tcp_conn_finish(&s->server_connection); + s->tcp_state = REXMPP_TCP_CONNECTED; + if (s->tls_state == REXMPP_TLS_AWAITING_DIRECT) { + rexmpp_tls_start(s); + } else { + rexmpp_stream_open(s); + } + } else if (err != REXMPP_CONN_IN_PROGRESS) { + rexmpp_log(s, LOG_WARNING, "Failed to connect"); + if (err == REXMPP_CONN_ERROR) { + s->tcp_state = REXMPP_TCP_NONE; + } else { + s->tcp_state = REXMPP_TCP_CONNECTION_FAILURE; + } + rexmpp_tcp_conn_finish(&s->server_connection); + rexmpp_try_next_host(s); + } +} + +void rexmpp_after_srv (rexmpp_t *s) { + if (s->resolver_state == REXMPP_RESOLVER_SRV) { + s->resolver_state = REXMPP_RESOLVER_SRV_2; + } else if (s->resolver_state == REXMPP_RESOLVER_SRV_2) { + s->resolver_state = REXMPP_RESOLVER_READY; + } + if (s->resolver_state != REXMPP_RESOLVER_READY) { + return; + } + + /* todo: sort the records */ + + if (s->server_srv == NULL && s->server_srv_tls == NULL) { + /* Failed to resolve anything: a fallback. */ + const char *host = jid_bare_to_host(s->initial_jid); + int port = 5222; + rexmpp_log(s, LOG_DEBUG, "Connecting to %s:%d", host, port); + rexmpp_process_conn_err(s, rexmpp_tcp_conn_init(&s->server_connection, + host, port)); + } else { + rexmpp_try_next_host(s); + } +} + +void rexmpp_srv_tls_cb (void *s_ptr, + int status, + int timeouts, + unsigned char *abuf, + int alen) +{ + rexmpp_t *s = s_ptr; + if (status == ARES_SUCCESS) { + ares_parse_srv_reply(abuf, alen, &(s->server_srv_tls)); + } else { + rexmpp_log(s, LOG_WARNING, "Failed to query an xmpps-client SRV record: %s", + ares_strerror(status)); + } + if (status != ARES_EDESTRUCTION) { + rexmpp_after_srv(s); + } +} + +void rexmpp_srv_cb (void *s_ptr, + int status, + int timeouts, + unsigned char *abuf, + int alen) +{ + rexmpp_t *s = s_ptr; + if (status == ARES_SUCCESS) { + ares_parse_srv_reply(abuf, alen, &(s->server_srv)); + } else { + rexmpp_log(s, LOG_WARNING, "Failed to query an xmpp-client SRV record: %s", + ares_strerror(status)); + } + if (status != ARES_EDESTRUCTION) { + rexmpp_after_srv(s); + } +} + + +/* Should be called after reconnect, and after rexmpp_sm_handle_ack in + case of resumption. */ +rexmpp_err_t rexmpp_resend_stanzas (rexmpp_t *s) { + uint32_t i, count; + rexmpp_err_t ret = REXMPP_SUCCESS; + xmlNodePtr sq; + count = s->stanzas_out_count - s->stanzas_out_acknowledged; + for (i = 0; i < count && s->stanza_queue != NULL; i++) { + sq = xmlNextElementSibling(s->stanza_queue); + ret = rexmpp_send(s, s->stanza_queue); + if (ret != REXMPP_SUCCESS && ret != REXMPP_E_AGAIN) { + return ret; + } + s->stanza_queue = sq; + } + if (i != count) { + rexmpp_log(s, LOG_ERR, + "not enough stanzas in the queue: needed %u, had %u", + count, i); + } + /* Don't count these stanzas twice. */ + s->stanzas_out_count -= i; + return ret; +} + +void rexmpp_sm_handle_ack (rexmpp_t *s, xmlNodePtr elem) { + char *h = xmlGetProp(elem, "h"); + if (h != NULL) { + uint32_t prev_ack = s->stanzas_out_acknowledged; + s->stanzas_out_acknowledged = strtoul(h, NULL, 10); + xmlFree(h); + rexmpp_log(s, LOG_DEBUG, + "server acknowledged %u out of %u sent stanzas", + s->stanzas_out_acknowledged, + s->stanzas_out_count); + if (s->stanzas_out_count >= s->stanzas_out_acknowledged) { + if (prev_ack <= s->stanzas_out_acknowledged) { + uint32_t i; + for (i = prev_ack; i < s->stanzas_out_acknowledged; i++) { + xmlNodePtr sq = xmlNextElementSibling(s->stanza_queue); + xmlFreeNode(s->stanza_queue); + s->stanza_queue = sq; + } + } else { + rexmpp_log(s, LOG_ERR, + "the server acknowledged %u stanzas previously, and %u now", + prev_ack, s->stanzas_out_acknowledged); + } + } else { + rexmpp_log(s, LOG_ERR, + "the server acknowledged more stanzas than we have sent"); + } + } else { + rexmpp_log(s, LOG_ERR, "no 'h' attribute in <a>"); + } +} + +void rexmpp_carbons_enabled (rexmpp_t *s, xmlNodePtr req, xmlNodePtr response) { + char *type = xmlGetProp(response, "type"); + if (strcmp(type, "result") == 0) { + rexmpp_log(s, LOG_INFO, "carbons enabled"); + s->carbons_state = REXMPP_CARBONS_ACTIVE; + } else { + rexmpp_log(s, LOG_WARNING, "failed to enable carbons"); + s->carbons_state = REXMPP_CARBONS_INACTIVE; + } + free(type); +} + +void rexmpp_discovery_info (rexmpp_t *s, xmlNodePtr req, xmlNodePtr response) { + xmlNodePtr query = xmlFirstElementChild(response); + if (rexmpp_xml_match(query, "http://jabber.org/protocol/disco#info", + "query")) { + xmlNodePtr child; + for (child = xmlFirstElementChild(query); + child != NULL; + child = xmlNextElementSibling(child)) + { + if (rexmpp_xml_match(child, "http://jabber.org/protocol/disco#info", + "feature")) { + char *var = xmlGetProp(child, "var"); + if (s->carbons_state != REXMPP_CARBONS_DISABLED && + strcmp(var, "urn:xmpp:carbons:2") == 0) { + xmlNodePtr carbons_enable = xmlNewNode(NULL, "enable"); + xmlNewNs(carbons_enable, "urn:xmpp:carbons:2", NULL); + s->carbons_state = REXMPP_CARBONS_NEGOTIATION; + rexmpp_iq_new(s, "set", NULL, carbons_enable, + rexmpp_carbons_enabled); + } + free(var); + } + } + } +} + +void rexmpp_stream_is_ready(rexmpp_t *s) { + s->stream_state = REXMPP_STREAM_READY; + rexmpp_resend_stanzas(s); + rexmpp_send(s, xmlNewNode(NULL, "presence")); + xmlNodePtr disco_query = xmlNewNode(NULL, "query"); + xmlNewNs(disco_query, "http://jabber.org/protocol/disco#info", NULL); + rexmpp_iq_new(s, "get", jid_bare_to_host(s->initial_jid), + disco_query, rexmpp_discovery_info); +} + +/* Resource binding, + https://tools.ietf.org/html/rfc6120#section-7 */ +void rexmpp_bound (rexmpp_t *s, xmlNodePtr req, xmlNodePtr response) { + /* todo: handle errors */ + xmlNodePtr child = xmlFirstElementChild(response); + if (rexmpp_xml_match(child, "urn:ietf:params:xml:ns:xmpp-bind", "bind")) { + xmlNodePtr jid = xmlFirstElementChild(child); + if (rexmpp_xml_match(jid, "urn:ietf:params:xml:ns:xmpp-bind", "jid")) { + rexmpp_log(s, LOG_INFO, "jid: %s", xmlNodeGetContent(jid)); + s->assigned_jid = malloc(strlen(xmlNodeGetContent(jid)) + 1); + strcpy(s->assigned_jid, xmlNodeGetContent(jid)); + } + if (s->stream_id == NULL && + (child = rexmpp_xml_find_child(s->stream_features, "urn:xmpp:sm:3", + "sm"))) { + /* Try to resume a stream. */ + s->sm_state = REXMPP_SM_NEGOTIATION; + s->stream_state = REXMPP_STREAM_SM_FULL; + xmlNodePtr sm_enable = xmlNewNode(NULL, "enable"); + xmlNewNs(sm_enable, "urn:xmpp:sm:3", NULL); + xmlNewProp(sm_enable, "resume", "true"); + rexmpp_send(s, sm_enable); + s->stanzas_out_count = 0; + s->stanzas_out_acknowledged = 0; + s->stanzas_in_count = 0; + } else { + s->sm_state = REXMPP_SM_INACTIVE; + rexmpp_stream_is_ready(s); + } + } +} + +void rexmpp_stream_bind (rexmpp_t *s) { + /* Issue a bind request. */ + s->stream_state = REXMPP_STREAM_BIND; + xmlNodePtr bind_cmd = xmlNewNode(NULL, "bind"); + xmlNewNs(bind_cmd, "urn:ietf:params:xml:ns:xmpp-bind", NULL); + rexmpp_iq_new(s, "set", NULL, bind_cmd, rexmpp_bound); +} + +void rexmpp_process_element(rexmpp_t *s) { + xmlNodePtr elem = s->current_element; + + /* IQ responses */ + if (rexmpp_xml_match(elem, "jabber:client", "iq")) { + char *type = xmlGetProp(elem, "type"); + if (strcmp(type, "result") == 0 || strcmp(type, "error") == 0) { + char *id = xmlGetProp(elem, "id"); + rexmpp_iq_t *req = s->active_iq; + int found = 0; + while (req != NULL && found == 0) { + char *req_id = xmlGetProp(req->request, "id"); + if (strcmp(id, req_id) == 0) { + found = 1; + if (req->cb != NULL) { + req->cb(s, req->request, elem); + } + /* Remove the callback from the list, but keep in mind that + it could have added more entries. */ + if (s->active_iq == req) { + s->active_iq = req->next; + } else { + rexmpp_iq_t *prev_req = s->active_iq; + for (prev_req = s->active_iq; + prev_req != NULL; + prev_req = prev_req->next) + { + if (prev_req->next == req) { + prev_req->next = req->next; + break; + } + } + } + xmlFreeNode(req->request); + free(req); + } + free(req_id); + req = req->next; + } + free(id); + } + free(type); + } + + /* Stream negotiation, + https://tools.ietf.org/html/rfc6120#section-4.3 */ + if (s->stream_state == REXMPP_STREAM_NEGOTIATION && + rexmpp_xml_match(elem, "http://etherx.jabber.org/streams", "features")) { + + /* Remember features. */ + if (s->stream_features != NULL) { + xmlFreeNode(s->stream_features); + } + s->stream_features = xmlCopyNode(elem, 1); + + /* Nothing to negotiate. */ + if (xmlFirstElementChild(elem) == NULL) { + rexmpp_stream_is_ready(s); + return; + } + + /* TODO: check for required features properly here. Currently + assuming that STARTTLS, SASL, and BIND (with an exception for + SM) are always required if they are present. */ + xmlNodePtr child = + rexmpp_xml_find_child(elem, "urn:ietf:params:xml:ns:xmpp-tls", + "starttls"); + if (child != NULL) { + s->stream_state = REXMPP_STREAM_STARTTLS; + xmlNodePtr starttls_cmd = xmlNewNode(NULL, "starttls"); + xmlNewNs(starttls_cmd, "urn:ietf:params:xml:ns:xmpp-tls", NULL); + rexmpp_send(s, starttls_cmd); + return; + } + + child = rexmpp_xml_find_child(elem, "urn:ietf:params:xml:ns:xmpp-sasl", + "mechanisms"); + if (child != NULL) { + s->stream_state = REXMPP_STREAM_SASL; + s->sasl_state = REXMPP_SASL_NEGOTIATION; + char mech_list[2048]; /* todo: perhaps grow it dynamically */ + mech_list[0] = '\0'; + xmlNodePtr mechanism; + for (mechanism = xmlFirstElementChild(child); + mechanism != NULL; + mechanism = xmlNextElementSibling(mechanism)) { + if (rexmpp_xml_match(mechanism, "urn:ietf:params:xml:ns:xmpp-sasl", + "mechanism")) { + snprintf(mech_list + strlen(mech_list), + 2048 - strlen(mech_list), + "%s ", + xmlNodeGetContent(mechanism)); + } + } + const char *mech = + gsasl_client_suggest_mechanism(s->sasl_ctx, mech_list); + rexmpp_log(s, LOG_INFO, "Selected SASL mechanism: %s", mech); + int sasl_err; + char *sasl_buf; + sasl_err = gsasl_client_start(s->sasl_ctx, mech, &(s->sasl_session)); + if (sasl_err != GSASL_OK) { + rexmpp_log(s, LOG_CRIT, "Failed to initialise SASL session: %s", + gsasl_strerror(sasl_err)); + s->sasl_state = REXMPP_SASL_ERROR; + return; + } + sasl_err = gsasl_step64 (s->sasl_session, "", (char**)&sasl_buf); + if (sasl_err != GSASL_OK) { + if (sasl_err == GSASL_NEEDS_MORE) { + rexmpp_log(s, LOG_DEBUG, "SASL needs more data"); + } else { + rexmpp_log(s, LOG_ERR, "SASL error: %s", + gsasl_strerror(sasl_err)); + s->sasl_state = REXMPP_SASL_ERROR; + return; + } + } + xmlNodePtr auth_cmd = xmlNewNode(NULL, "auth"); + xmlNewProp(auth_cmd, "mechanism", mech); + xmlNewNs(auth_cmd, "urn:ietf:params:xml:ns:xmpp-sasl", NULL); + xmlNodeAddContent(auth_cmd, sasl_buf); + free(sasl_buf); + rexmpp_send(s, auth_cmd); + return; + } + + child = rexmpp_xml_find_child(elem, "urn:xmpp:sm:3", "sm"); + if (s->stream_id != NULL && child != NULL) { + s->stream_state = REXMPP_STREAM_SM_RESUME; + char buf[11]; + snprintf(buf, 11, "%u", s->stanzas_in_count); + xmlNodePtr sm_resume = xmlNewNode(NULL, "resume"); + xmlNewNs(sm_resume, "urn:xmpp:sm:3", NULL); + xmlNewProp(sm_resume, "previd", s->stream_id); + xmlNewProp(sm_resume, "h", buf); + rexmpp_send(s, sm_resume); + return; + } + + child = + rexmpp_xml_find_child(elem, "urn:ietf:params:xml:ns:xmpp-bind", "bind"); + if (child != NULL) { + rexmpp_stream_bind(s); + return; + } + } + + /* Stream errors, https://tools.ietf.org/html/rfc6120#section-4.9 */ + if (rexmpp_xml_match(elem, "http://etherx.jabber.org/streams", + "error")) { + rexmpp_log(s, LOG_ERR, "stream error"); + s->stream_state = REXMPP_STREAM_ERROR; + return; + } + + /* STARTTLS negotiation, + https://tools.ietf.org/html/rfc6120#section-5 */ + if (s->stream_state == REXMPP_STREAM_STARTTLS) { + if (rexmpp_xml_match(elem, "urn:ietf:params:xml:ns:xmpp-tls", + "proceed")) { + rexmpp_tls_start(s); + return; + } else if (rexmpp_xml_match(elem, "urn:ietf:params:xml:ns:xmpp-tls", + "failure")) { + rexmpp_log(s, LOG_ERR, "STARTTLS failure"); + return; + } + } + + /* SASL negotiation, + https://tools.ietf.org/html/rfc6120#section-6 */ + if (s->stream_state == REXMPP_STREAM_SASL) { + char *sasl_buf; + int sasl_err; + if (rexmpp_xml_match(elem, "urn:ietf:params:xml:ns:xmpp-sasl", + "challenge")) { + sasl_err = gsasl_step64 (s->sasl_session, xmlNodeGetContent(elem), + (char**)&sasl_buf); + if (sasl_err != GSASL_OK) { + if (sasl_err == GSASL_NEEDS_MORE) { + rexmpp_log(s, LOG_DEBUG, "SASL needs more data"); + } else { + rexmpp_log(s, LOG_ERR, "SASL error: %s", + gsasl_strerror(sasl_err)); + s->sasl_state = REXMPP_SASL_ERROR; + return; + } + } + xmlNodePtr response = xmlNewNode(NULL, "response"); + xmlNewNs(response, "urn:ietf:params:xml:ns:xmpp-sasl", NULL); + xmlNodeAddContent(response, sasl_buf); + free(sasl_buf); + rexmpp_send(s, response); + return; + } else if (rexmpp_xml_match(elem, "urn:ietf:params:xml:ns:xmpp-sasl", + "success")) { + sasl_err = gsasl_step64 (s->sasl_session, xmlNodeGetContent(elem), + (char**)&sasl_buf); + free(sasl_buf); + if (sasl_err == GSASL_OK) { + rexmpp_log(s, LOG_DEBUG, "SASL success"); + } else { + rexmpp_log(s, LOG_ERR, "SASL error: %s", + gsasl_strerror(sasl_err)); + s->sasl_state = REXMPP_SASL_ERROR; + return; + } + s->sasl_state = REXMPP_SASL_ACTIVE; + s->stream_state = REXMPP_STREAM_RESTART; + return; + } else if (rexmpp_xml_match(elem, "urn:ietf:params:xml:ns:xmpp-sasl", + "failure")) { + /* todo: would be nice to retry here, but just giving up for now */ + rexmpp_log(s, LOG_ERR, "SASL failure"); + rexmpp_stop(s); + return; + } + } + + /* Stream management, https://xmpp.org/extensions/xep-0198.html */ + if (s->stream_state == REXMPP_STREAM_SM_FULL) { + if (rexmpp_xml_match(elem, "urn:xmpp:sm:3", "enabled")) { + s->sm_state = REXMPP_SM_ACTIVE; + char *resume = xmlGetProp(elem, "resume"); + if (resume != NULL) { + if (s->stream_id != NULL) { + free(s->stream_id); + } + s->stream_id = xmlGetProp(elem, "id"); + xmlFree(resume); + } + rexmpp_stream_is_ready(s); + } else if (rexmpp_xml_match(elem, "urn:xmpp:sm:3", "failed")) { + s->stream_state = REXMPP_STREAM_SM_ACKS; + s->sm_state = REXMPP_SM_NEGOTIATION; + xmlNodePtr sm_enable = xmlNewNode(NULL, "enable"); + xmlNewNs(sm_enable, "urn:xmpp:sm:3", NULL); + rexmpp_send(s, sm_enable); + } + } else if (s->stream_state == REXMPP_STREAM_SM_ACKS) { + if (rexmpp_xml_match(elem, "urn:xmpp:sm:3", "enabled")) { + s->sm_state = REXMPP_SM_ACTIVE; + if (s->stream_id != NULL) { + free(s->stream_id); + s->stream_id = NULL; + } + } else if (rexmpp_xml_match(elem, "urn:xmpp:sm:3", "failed")) { + s->sm_state = REXMPP_SM_INACTIVE; + xmlNodePtr sm_enable = xmlNewNode(NULL, "enable"); + xmlNewNs(sm_enable, "urn:xmpp:sm:3", NULL); + rexmpp_send(s, sm_enable); + } + rexmpp_stream_is_ready(s); + } else if (s->stream_state == REXMPP_STREAM_SM_RESUME) { + if (rexmpp_xml_match(elem, "urn:xmpp:sm:3", "resumed")) { + s->sm_state = REXMPP_SM_ACTIVE; + s->stream_state = REXMPP_STREAM_READY; + rexmpp_sm_handle_ack(s, elem); + rexmpp_resend_stanzas(s); + } else if (rexmpp_xml_match(elem, "urn:xmpp:sm:3", "failed")) { + /* Back to binding, but cleanup stream state first. */ + free(s->stream_id); + s->stream_id = NULL; + while (s->active_iq != NULL) { + /* todo: check that those are not queued for resending? */ + rexmpp_iq_t *next = s->active_iq->next; + xmlFreeNode(s->active_iq->request); + free(s->active_iq); + s->active_iq = next; + } + xmlNodePtr child = + rexmpp_xml_find_child(s->stream_features, + "urn:ietf:params:xml:ns:xmpp-bind", + "bind"); + if (child != NULL) { + rexmpp_stream_bind(s); + return; + } + } + } + + if (s->sm_state == REXMPP_SM_ACTIVE && rexmpp_xml_is_stanza(elem)) { + s->stanzas_in_count++; + } + if (rexmpp_xml_match(elem, "urn:xmpp:sm:3", "r")) { + rexmpp_sm_ack(s); + } else if (rexmpp_xml_match(elem, "urn:xmpp:sm:3", "a")) { + rexmpp_sm_handle_ack(s, elem); + } +} + + +void rexmpp_sax_characters (rexmpp_t *s, const char *ch, int len) +{ + if (s->current_element != NULL) { + xmlNodeAddContentLen(s->current_element, ch, len); + } +} + +void rexmpp_sax_start_elem_ns (rexmpp_t *s, + const char *localname, + const char *prefix, + const char *URI, + int nb_namespaces, + const char **namespaces, + int nb_attributes, + int nb_defaulted, + const char **attributes) +{ + int i; + if (s->stream_state == REXMPP_STREAM_OPENING && + strcmp(localname, "stream") == 0 && + strcmp(URI, "http://etherx.jabber.org/streams") == 0) { + rexmpp_log(s, LOG_DEBUG, "stream start"); + s->stream_state = REXMPP_STREAM_NEGOTIATION; + return; + } + + if (s->stream_state != REXMPP_STREAM_OPENING) { + if (s->current_element == NULL) { + s->current_element = xmlNewNode(NULL, localname); + s->current_element_root = s->current_element; + } else { + xmlNodePtr node = xmlNewNode(NULL, localname); + xmlAddChild(s->current_element, node); + s->current_element = node; + } + xmlNsPtr ns = xmlNewNs(s->current_element, URI, prefix); + s->current_element->ns = ns; + for (i = 0; i < nb_attributes; i++) { + size_t attr_len = attributes[i * 5 + 4] - attributes[i * 5 + 3]; + char *attr_val = malloc(attr_len + 1); + attr_val[attr_len] = '\0'; + strncpy(attr_val, attributes[i * 5 + 3], attr_len); + xmlNewProp(s->current_element, attributes[i * 5], attr_val); + free(attr_val); + } + } +} + +void rexmpp_sax_end_elem_ns (rexmpp_t *s, + const char *localname, + const char *prefix, + const char *URI) +{ + if ((s->stream_state == REXMPP_STREAM_CLOSING || + s->stream_state == REXMPP_STREAM_ERROR) && + strcmp(localname, "stream") == 0 && + strcmp(URI, "http://etherx.jabber.org/streams") == 0) { + rexmpp_log(s, LOG_DEBUG, "stream end"); + if (s->sasl_state == REXMPP_SASL_ACTIVE) { + gsasl_finish(s->sasl_session); + s->sasl_session = NULL; + s->sasl_state = REXMPP_SASL_INACTIVE; + } + s->stream_state = REXMPP_STREAM_CLOSED; + if (s->tls_state == REXMPP_TLS_ACTIVE) { + s->tls_state = REXMPP_TLS_CLOSING; + } else { + rexmpp_log(s, LOG_DEBUG, "closing the socket"); + close(s->server_socket); + s->server_socket = -1; + rexmpp_cleanup(s); + s->tcp_state = REXMPP_TCP_CLOSED; + } + return; + } + + if (s->current_element != s->current_element_root) { + s->current_element = s->current_element->parent; + } else { + if (s->xml_in_cb != NULL && s->xml_in_cb(s, s->current_element) != 0) { + rexmpp_log(s, LOG_WARNING, + "Message processing was cancelled by xml_in_cb."); + } else { + rexmpp_process_element(s); + } + + xmlFreeNode(s->current_element); + s->current_element = NULL; + s->current_element_root = NULL; + } +} + +rexmpp_err_t rexmpp_close (rexmpp_t *s) { + s->stream_state = REXMPP_STREAM_CLOSING; + char *close_stream = "</stream:stream>"; + return rexmpp_send_raw(s, close_stream, strlen(close_stream)); +} + +rexmpp_err_t rexmpp_stop (rexmpp_t *s) { + if (s->sm_state == REXMPP_SM_ACTIVE) { + int ret = rexmpp_sm_ack(s); + if (ret != REXMPP_SUCCESS && ret != REXMPP_E_AGAIN) { + return ret; + } + } + s->stream_state = REXMPP_STREAM_CLOSE_REQUESTED; + if (s->send_buffer == NULL) { + return rexmpp_close(s); + } else { + return REXMPP_E_AGAIN; + } +} + +rexmpp_err_t rexmpp_run (rexmpp_t *s, fd_set *read_fds, fd_set *write_fds) { + struct timeval now; + gettimeofday(&now, NULL); + + /* Inactive: start by querying SRV records. */ + if ((s->resolver_state == REXMPP_RESOLVER_NONE || + s->resolver_state == REXMPP_RESOLVER_READY) && + (s->tcp_state == REXMPP_TCP_NONE || + ((s->tcp_state == REXMPP_TCP_ERROR || + s->tcp_state == REXMPP_TCP_CONNECTION_FAILURE) && + s->reconnect_number > 0 && + s->next_reconnect_time.tv_sec <= now.tv_sec))) { + rexmpp_log(s, LOG_DEBUG, "start (or reconnect)"); + size_t srv_query_buf_len = strlen(jid_bare_to_host(s->initial_jid)) + + strlen("_xmpps-client._tcp..") + + 1; + char *srv_query = malloc(srv_query_buf_len); + snprintf(srv_query, srv_query_buf_len, + "_xmpps-client._tcp.%s.", jid_bare_to_host(s->initial_jid)); + ares_query(s->resolver_channel, srv_query, + ns_c_in, ns_t_srv, rexmpp_srv_tls_cb, s); + snprintf(srv_query, srv_query_buf_len, + "_xmpp-client._tcp.%s.", jid_bare_to_host(s->initial_jid)); + ares_query(s->resolver_channel, srv_query, + ns_c_in, ns_t_srv, rexmpp_srv_cb, s); + s->resolver_state = REXMPP_RESOLVER_SRV; + free(srv_query); + } + + /* Resolving SRV records. This continues in rexmpp_srv_tls_cb, + rexmpp_srv_cb, and rexmpp_after_srv, possibly leading to + connection initiation. */ + if (s->resolver_state != REXMPP_RESOLVER_NONE && + s->resolver_state != REXMPP_RESOLVER_READY) { + ares_process(s->resolver_channel, read_fds, write_fds); + } + + /* Connecting. Continues in rexmpp_process_conn_err, possibly + leading to stream opening. */ + if (s->tcp_state == REXMPP_TCP_CONNECTING) { + rexmpp_process_conn_err(s, + rexmpp_tcp_conn_proceed(&s->server_connection, + read_fds, write_fds)); + } + + /* The things we do while connected. */ + if (s->tcp_state == REXMPP_TCP_CONNECTED) { + + /* Sending queued data. */ + if (FD_ISSET(s->server_socket, write_fds) && + s->send_buffer != NULL) { + rexmpp_send_continue(s); + } + + /* Receiving data. Leads to all kinds of things. */ + if (FD_ISSET(s->server_socket, read_fds) && + s->stream_state != REXMPP_STREAM_NONE && + s->tcp_state == REXMPP_TCP_CONNECTED && + s->tls_state != REXMPP_TLS_HANDSHAKE) { + rexmpp_recv(s); + } + + /* Performing a TLS handshake. A stream restart happens after + this, if everything goes well. */ + if (s->tls_state == REXMPP_TLS_HANDSHAKE) { + rexmpp_tls_handshake(s); + } + + /* Restarting a stream if needed after the above actions. Since it + involves resetting the parser, functions called by that parser + can't do it on their own. */ + if (s->stream_state == REXMPP_STREAM_RESTART) { + xmlCtxtResetPush(s->xml_parser, "", 0, "", "utf-8"); + rexmpp_stream_open(s); + } + + /* Closing the stream once everything is sent. */ + if (s->stream_state == REXMPP_STREAM_CLOSE_REQUESTED && + s->send_buffer == NULL) { + rexmpp_close(s); + } + + /* Closing TLS and TCP connections once stream is closed. If + there's no TLS, the TCP connection is closed at once + elsewhere. */ + if (s->stream_state == REXMPP_STREAM_CLOSED && + s->tls_state == REXMPP_TLS_CLOSING) { + int ret = gnutls_bye(s->gnutls_session, GNUTLS_SHUT_RDWR); + if (ret == GNUTLS_E_SUCCESS) { + s->tls_state = REXMPP_TLS_INACTIVE; + rexmpp_cleanup(s); + s->tcp_state = REXMPP_TCP_CLOSED; + } + } + } + if (s->tcp_state == REXMPP_TCP_CLOSED) { + return REXMPP_SUCCESS; + } + return REXMPP_E_AGAIN; +} + +int rexmpp_fds(rexmpp_t *s, fd_set *read_fds, fd_set *write_fds) { + int conn_fd, max_fd = 0; + + if (s->resolver_state != REXMPP_RESOLVER_NONE && + s->resolver_state != REXMPP_RESOLVER_READY) { + max_fd = ares_fds(s->resolver_channel, read_fds, write_fds); + } + + if (s->tcp_state == REXMPP_TCP_CONNECTING) { + conn_fd = rexmpp_tcp_conn_fds(&s->server_connection, read_fds, write_fds); + if (conn_fd > max_fd) { + max_fd = conn_fd; + } + } + + if (s->tls_state == REXMPP_TLS_HANDSHAKE) { + if (gnutls_record_get_direction(s->gnutls_session) == 0) { + FD_SET(s->server_socket, read_fds); + } else { + FD_SET(s->server_socket, write_fds); + } + if (s->server_socket + 1 > max_fd) { + max_fd = s->server_socket + 1; + } + } + + if (s->tcp_state == REXMPP_TCP_CONNECTED) { + FD_SET(s->server_socket, read_fds); + if (s->send_buffer != NULL) { + FD_SET(s->server_socket, write_fds); + } + if (s->server_socket + 1 > max_fd) { + max_fd = s->server_socket + 1; + } + } + + return max_fd; +} + +struct timeval *rexmpp_timeout (rexmpp_t *s, + struct timeval *max_tv, + struct timeval *tv) +{ + struct timeval *ret = max_tv; + + if (s->resolver_state != REXMPP_RESOLVER_NONE && + s->resolver_state != REXMPP_RESOLVER_READY) { + ret = ares_timeout(s->resolver_channel, max_tv, tv); + } else if (s->tcp_state == REXMPP_TCP_CONNECTING) { + ret = rexmpp_tcp_conn_timeout(&s->server_connection, max_tv, tv); + } + struct timeval now; + gettimeofday(&now, NULL); + if (s->reconnect_number > 0 && + s->next_reconnect_time.tv_sec > now.tv_sec && + (ret == NULL || + s->next_reconnect_time.tv_sec - now.tv_sec < ret->tv_sec)) { + tv->tv_sec = s->next_reconnect_time.tv_sec - now.tv_sec; + tv->tv_usec = 0; + ret = tv; + } + + return ret; +} |