/* * connection.c * * Copyright (C) 2012, 2013 James Booth * * This file is part of Profanity. * * Profanity is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Profanity is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Profanity. If not, see . * */ #include #include #include #include #include "chat_session.h" #include "common.h" #include "config/preferences.h" #include "jid.h" #include "log.h" #include "muc.h" #include "profanity.h" #include "xmpp/bookmark.h" #include "xmpp/capabilities.h" #include "xmpp/connection.h" #include "xmpp/iq.h" #include "xmpp/message.h" #include "xmpp/presence.h" #include "xmpp/roster.h" #include "xmpp/stanza.h" #include "xmpp/xmpp.h" static struct _jabber_conn_t { xmpp_log_t *log; xmpp_ctx_t *ctx; xmpp_conn_t *conn; jabber_conn_status_t conn_status; char *presence_message; int priority; int tls_disabled; char *domain; } jabber_conn; static GHashTable *available_resources; // for auto reconnect static struct { char *name; char *passwd; } saved_account; static struct { char *name; char *jid; char *passwd; char *altdomain; } saved_details; static GTimer *reconnect_timer; static log_level_t _get_log_level(xmpp_log_level_t xmpp_level); static xmpp_log_level_t _get_xmpp_log_level(); static void _xmpp_file_logger(void * const userdata, const xmpp_log_level_t level, const char * const area, const char * const msg); static xmpp_log_t * _xmpp_get_file_logger(); static jabber_conn_status_t _jabber_connect(const char * const fulljid, const char * const passwd, const char * const altdomain); static void _jabber_reconnect(void); static void _connection_handler(xmpp_conn_t * const conn, const xmpp_conn_event_t status, const int error, xmpp_stream_error_t * const stream_error, void * const userdata); static int _ping_timed_handler(xmpp_conn_t * const conn, void * const userdata); void _connection_free_saved_account(void); void _connection_free_saved_details(void); void _connection_free_session_data(void); static void _jabber_init(const int disable_tls) { log_info("Initialising XMPP"); jabber_conn.conn_status = JABBER_STARTED; jabber_conn.presence_message = NULL; jabber_conn.conn = NULL; jabber_conn.ctx = NULL; jabber_conn.tls_disabled = disable_tls; jabber_conn.domain = NULL; presence_sub_requests_init(); caps_init(); available_resources = g_hash_table_new_full(g_str_hash, g_str_equal, free, (GDestroyNotify)resource_destroy); xmpp_initialize(); } void (*jabber_init)(const int) = _jabber_init; static jabber_conn_status_t _jabber_connect_with_account(const ProfAccount * const account) { assert(account != NULL); log_info("Connecting using account: %s", account->name); // save account name and password for reconnect saved_account.name = strdup(account->name); saved_account.passwd = strdup(account->password); // connect with fulljid Jid *jidp = jid_create_from_bare_and_resource(account->jid, account->resource); jabber_conn_status_t result = _jabber_connect(jidp->fulljid, account->password, account->server); jid_destroy(jidp); return result; } jabber_conn_status_t (*jabber_connect_with_account)(const ProfAccount * const) = _jabber_connect_with_account; static jabber_conn_status_t _jabber_connect_with_details(const char * const jid, const char * const passwd, const char * const altdomain) { assert(jid != NULL); assert(passwd != NULL); // save details for reconnect, remember name for account creating on success saved_details.name = strdup(jid); saved_details.passwd = strdup(passwd); if (altdomain != NULL) { saved_details.altdomain = strdup(altdomain); } else { saved_details.altdomain = NULL; } // use 'profanity' when no resourcepart in provided jid Jid *jidp = jid_create(jid); if (jidp->resourcepart == NULL) { jid_destroy(jidp); jidp = jid_create_from_bare_and_resource(jid, "profanity"); saved_details.jid = strdup(jidp->fulljid); } else { saved_details.jid = strdup(jid); } jid_destroy(jidp); // connect with fulljid log_info("Connecting without account, JID: %s", saved_details.jid); return _jabber_connect(saved_details.jid, passwd, saved_details.altdomain); } jabber_conn_status_t (*jabber_connect_with_details)(const char * const, const char * const, const char * const) = _jabber_connect_with_details; static void _jabber_disconnect(void) { // if connected, send end stream and wait for response if (jabber_conn.conn_status == JABBER_CONNECTED) { log_info("Closing connection"); jabber_conn.conn_status = JABBER_DISCONNECTING; xmpp_disconnect(jabber_conn.conn); while (jabber_get_connection_status() == JABBER_DISCONNECTING) { jabber_process_events(); } _connection_free_saved_account(); _connection_free_saved_details(); _connection_free_session_data(); if (jabber_conn.conn != NULL) { xmpp_conn_release(jabber_conn.conn); jabber_conn.conn = NULL; } if (jabber_conn.ctx != NULL) { xmpp_ctx_free(jabber_conn.ctx); jabber_conn.ctx = NULL; } } jabber_conn.conn_status = JABBER_STARTED; FREE_SET_NULL(jabber_conn.presence_message); FREE_SET_NULL(jabber_conn.domain); } void (*jabber_disconnect)(void) = _jabber_disconnect; static void _jabber_shutdown(void) { xmpp_shutdown(); } void (*jabber_shutdown)(void) = _jabber_shutdown; static void _jabber_process_events(void) { // run xmpp event loop if connected, connecting or disconnecting if (jabber_conn.conn_status == JABBER_CONNECTED || jabber_conn.conn_status == JABBER_CONNECTING || jabber_conn.conn_status == JABBER_DISCONNECTING) { xmpp_run_once(jabber_conn.ctx, 10); // check timer and reconnect if disconnected and timer set } else if (prefs_get_reconnect() != 0) { if ((jabber_conn.conn_status == JABBER_DISCONNECTED) && (reconnect_timer != NULL)) { if (g_timer_elapsed(reconnect_timer, NULL) > prefs_get_reconnect()) { _jabber_reconnect(); } } } } void (*jabber_process_events)(void) = _jabber_process_events; static void _jabber_set_autoping(const int seconds) { if (jabber_conn.conn_status == JABBER_CONNECTED) { xmpp_timed_handler_delete(jabber_conn.conn, _ping_timed_handler); if (seconds != 0) { int millis = seconds * 1000; xmpp_timed_handler_add(jabber_conn.conn, _ping_timed_handler, millis, jabber_conn.ctx); } } } void (*jabber_set_autoping)(const int) = _jabber_set_autoping; static GList * _jabber_get_available_resources(void) { return g_hash_table_get_values(available_resources); } GList * (*jabber_get_available_resources)(void) = _jabber_get_available_resources; static jabber_conn_status_t _jabber_get_connection_status(void) { return (jabber_conn.conn_status); } jabber_conn_status_t (*jabber_get_connection_status)(void) = _jabber_get_connection_status; xmpp_conn_t * connection_get_conn(void) { return jabber_conn.conn; } xmpp_ctx_t * connection_get_ctx(void) { return jabber_conn.ctx; } static const char * _jabber_get_fulljid(void) { return xmpp_conn_get_jid(jabber_conn.conn); } const char * (*jabber_get_fulljid)(void) = _jabber_get_fulljid; static const char * _jabber_get_domain(void) { return jabber_conn.domain; } const char * (*jabber_get_domain)(void) = _jabber_get_domain; static char * _jabber_get_presence_message(void) { return jabber_conn.presence_message; } char * (*jabber_get_presence_message)(void) = _jabber_get_presence_message; static char * _jabber_get_account_name(void) { return saved_account.name; } char * (*jabber_get_account_name)(void) = _jabber_get_account_name; void connection_set_presence_message(const char * const message) { FREE_SET_NULL(jabber_conn.presence_message); if (message != NULL) { jabber_conn.presence_message = strdup(message); } } void connection_set_priority(const int priority) { jabber_conn.priority = priority; } void connection_add_available_resource(Resource *resource) { g_hash_table_replace(available_resources, strdup(resource->name), resource); } void connection_remove_available_resource(const char * const resource) { g_hash_table_remove(available_resources, resource); } void _connection_free_saved_account(void) { FREE_SET_NULL(saved_account.name); FREE_SET_NULL(saved_account.passwd); } void _connection_free_saved_details(void) { FREE_SET_NULL(saved_details.name); FREE_SET_NULL(saved_details.jid); FREE_SET_NULL(saved_details.passwd); FREE_SET_NULL(saved_details.altdomain); } void _connection_free_session_data(void) { g_hash_table_remove_all(available_resources); chat_sessions_clear(); presence_clear_sub_requests(); } int connection_error_handler(xmpp_conn_t * const conn, xmpp_stanza_t * const stanza, void * const userdata) { xmpp_ctx_t *ctx = connection_get_ctx(); gchar *err_msg = NULL; gchar *from = xmpp_stanza_get_attribute(stanza, STANZA_ATTR_FROM); xmpp_stanza_t *error_stanza = xmpp_stanza_get_child_by_name(stanza, STANZA_NAME_ERROR); xmpp_stanza_t *text_stanza = xmpp_stanza_get_child_by_name(error_stanza, STANZA_NAME_TEXT); if (error_stanza == NULL) { log_debug("error message without received"); } else { // check for text if (text_stanza != NULL) { err_msg = xmpp_stanza_get_text(text_stanza); if (err_msg != NULL) { prof_handle_error_message(from, err_msg); xmpp_free(ctx, err_msg); } // TODO : process 'type' attribute from [RFC6120, 8.3.2] // otherwise show defined-condition } else { xmpp_stanza_t *err_cond = xmpp_stanza_get_children(error_stanza); if (err_cond == NULL) { log_debug("error message without or received"); } else { err_msg = xmpp_stanza_get_name(err_cond); prof_handle_error_message(from, err_msg); // TODO : process 'type' attribute from [RFC6120, 8.3.2] } } } return 1; } static jabber_conn_status_t _jabber_connect(const char * const fulljid, const char * const passwd, const char * const altdomain) { assert(fulljid != NULL); assert(passwd != NULL); Jid *jid = jid_create(fulljid); if (jid == NULL) { log_error("Malformed JID not able to connect: %s", fulljid); jabber_conn.conn_status = JABBER_DISCONNECTED; return jabber_conn.conn_status; } else if (jid->fulljid == NULL) { log_error("Full JID required to connect, received: %s", fulljid); jabber_conn.conn_status = JABBER_DISCONNECTED; return jabber_conn.conn_status; } jid_destroy(jid); log_info("Connecting as %s", fulljid); jabber_conn.log = _xmpp_get_file_logger(); if (jabber_conn.conn != NULL) { xmpp_conn_release(jabber_conn.conn); } if (jabber_conn.ctx != NULL) { xmpp_ctx_free(jabber_conn.ctx); } jabber_conn.ctx = xmpp_ctx_new(NULL, jabber_conn.log); if (jabber_conn.ctx == NULL) { log_warning("Failed to get libstrophe ctx during connect"); return JABBER_DISCONNECTED; } jabber_conn.conn = xmpp_conn_new(jabber_conn.ctx); if (jabber_conn.conn == NULL) { log_warning("Failed to get libstrophe conn during connect"); return JABBER_DISCONNECTED; } xmpp_conn_set_jid(jabber_conn.conn, fulljid); xmpp_conn_set_pass(jabber_conn.conn, passwd); if (jabber_conn.tls_disabled) { xmpp_conn_disable_tls(jabber_conn.conn); } int connect_status = xmpp_connect_client(jabber_conn.conn, altdomain, 0, _connection_handler, jabber_conn.ctx); if (connect_status == 0) jabber_conn.conn_status = JABBER_CONNECTING; else jabber_conn.conn_status = JABBER_DISCONNECTED; return jabber_conn.conn_status; } static void _jabber_reconnect(void) { // reconnect with account. ProfAccount *account = accounts_get_account(saved_account.name); if (account == NULL) { log_error("Unable to reconnect, account no longer exists: %s", saved_account.name); } else { char *fulljid = create_fulljid(account->jid, account->resource); log_debug("Attempting reconnect with account %s", account->name); _jabber_connect(fulljid, saved_account.passwd, account->server); free(fulljid); g_timer_start(reconnect_timer); } } static void _connection_handler(xmpp_conn_t * const conn, const xmpp_conn_event_t status, const int error, xmpp_stream_error_t * const stream_error, void * const userdata) { xmpp_ctx_t *ctx = (xmpp_ctx_t *)userdata; // login success if (status == XMPP_CONN_CONNECT) { log_debug("Connection handler: XMPP_CONN_CONNECT"); // logged in with account if (saved_account.name != NULL) { log_debug("Connection handler: logged in with account name: %s", saved_account.name); prof_handle_login_account_success(saved_account.name); // logged in without account, use details to create new account } else { log_debug("Connection handler: logged in with jid: %s", saved_details.name); accounts_add(saved_details.name, saved_details.altdomain); accounts_set_jid(saved_details.name, saved_details.jid); prof_handle_login_account_success(saved_details.name); saved_account.name = strdup(saved_details.name); saved_account.passwd = strdup(saved_details.passwd); _connection_free_saved_details(); } Jid *myJid = jid_create(jabber_get_fulljid()); jabber_conn.domain = strdup(myJid->domainpart); jid_destroy(myJid); chat_sessions_init(); xmpp_handler_add(conn, connection_error_handler, NULL, NULL, STANZA_TYPE_ERROR, ctx); roster_add_handlers(); message_add_handlers(); presence_add_handlers(); iq_add_handlers(); if (prefs_get_autoping() != 0) { int millis = prefs_get_autoping() * 1000; xmpp_timed_handler_add(conn, _ping_timed_handler, millis, ctx); } roster_request(); bookmark_request(); jabber_conn.conn_status = JABBER_CONNECTED; if (prefs_get_reconnect() != 0) { if (reconnect_timer != NULL) { g_timer_destroy(reconnect_timer); reconnect_timer = NULL; } } } else if (status == XMPP_CONN_DISCONNECT) { log_debug("Connection handler: XMPP_CONN_DISCONNECT"); // lost connection for unkown reason if (jabber_conn.conn_status == JABBER_CONNECTED) { log_debug("Connection handler: Lost connection for unknown reason"); prof_handle_lost_connection(); if (prefs_get_reconnect() != 0) { assert(reconnect_timer == NULL); reconnect_timer = g_timer_new(); // free resources but leave saved_user untouched _connection_free_session_data(); } else { _connection_free_saved_account(); _connection_free_saved_details(); _connection_free_session_data(); } // login attempt failed } else if (jabber_conn.conn_status != JABBER_DISCONNECTING) { log_debug("Connection handler: Login failed"); if (reconnect_timer == NULL) { log_debug("Connection handler: No reconnect timer"); prof_handle_failed_login(); _connection_free_saved_account(); _connection_free_saved_details(); _connection_free_session_data(); } else { log_debug("Connection handler: Restarting reconnect timer"); if (prefs_get_reconnect() != 0) { g_timer_start(reconnect_timer); } // free resources but leave saved_user untouched _connection_free_session_data(); } } // close stream response from server after disconnect is handled too jabber_conn.conn_status = JABBER_DISCONNECTED; } else if (status == XMPP_CONN_FAIL) { log_debug("Connection handler: XMPP_CONN_FAIL"); } else { log_error("Connection handler: Unknown status"); } } static int _ping_timed_handler(xmpp_conn_t * const conn, void * const userdata) { if (jabber_conn.conn_status == JABBER_CONNECTED) { xmpp_ctx_t *ctx = (xmpp_ctx_t *)userdata; xmpp_stanza_t *iq = stanza_create_ping_iq(ctx); xmpp_send(conn, iq); xmpp_stanza_release(iq); } return 1; } static log_level_t _get_log_level(const xmpp_log_level_t xmpp_level) { if (xmpp_level == XMPP_LEVEL_DEBUG) { return PROF_LEVEL_DEBUG; } else if (xmpp_level == XMPP_LEVEL_INFO) { return PROF_LEVEL_INFO; } else if (xmpp_level == XMPP_LEVEL_WARN) { return PROF_LEVEL_WARN; } else { return PROF_LEVEL_ERROR; } } static xmpp_log_level_t _get_xmpp_log_level() { log_level_t prof_level = log_get_filter(); if (prof_level == PROF_LEVEL_DEBUG) { return XMPP_LEVEL_DEBUG; } else if (prof_level == PROF_LEVEL_INFO) { return XMPP_LEVEL_INFO; } else if (prof_level == PROF_LEVEL_WARN) { return XMPP_LEVEL_WARN; } else { return XMPP_LEVEL_ERROR; } } static void _xmpp_file_logger(void * const userdata, const xmpp_log_level_t level, const char * const area, const char * const msg) { log_level_t prof_level = _get_log_level(level); log_msg(prof_level, area, msg); } static xmpp_log_t * _xmpp_get_file_logger() { xmpp_log_level_t level = _get_xmpp_log_level(); xmpp_log_t *file_log = malloc(sizeof(xmpp_log_t)); file_log->handler = _xmpp_file_logger; file_log->userdata = &level; return file_log; }