diff --git a/common.c b/common.c index 820863d..32ccc01 100644 --- a/common.c +++ b/common.c @@ -90,6 +90,239 @@ log_message_syslog (void *user_data, const char *quote, const char *fmt, syslog (prio, "%s%s", quote, buf); } +// --- Connector --------------------------------------------------------------- + +// This is a helper that tries to establish a connection with any address on +// a given list. Sadly it also introduces a bit of a callback hell. + +struct connector_target +{ + LIST_HEADER (struct connector_target) + + char *hostname; ///< Target hostname or address + char *service; ///< Target service name or port + + struct addrinfo *results; ///< Resolved target + struct addrinfo *iter; ///< Current endpoint +}; + +static struct connector_target * +connector_target_new (void) +{ + struct connector_target *self = xmalloc (sizeof *self); + return self; +} + +static void +connector_target_destroy (struct connector_target *self) +{ + free (self->hostname); + free (self->service); + freeaddrinfo (self->results); + free (self); +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +struct connector +{ + int socket; ///< Socket FD for the connection + struct poller_fd connected_event; ///< We've connected or failed + struct connector_target *targets; ///< Targets + struct connector_target *targets_t; ///< Tail of targets + + void *user_data; ///< User data for callbacks + + // You may destroy the connector object in these two main callbacks: + + /// Connection has been successfully established + void (*on_connected) (void *user_data, int socket); + /// Failed to establish a connection to either target + void (*on_failure) (void *user_data); + + // Optional: + + /// Connecting to a new address + void (*on_connecting) (void *user_data, const char *address); + /// Connecting to the last address has failed + void (*on_error) (void *user_data, const char *error); +}; + +static void +connector_notify_connecting (struct connector *self, + struct connector_target *target, struct addrinfo *gai_iter) +{ + if (!self->on_connecting) + return; + + const char *real_host = target->hostname; + + // We don't really need this, so we can let it quietly fail + char buf[NI_MAXHOST]; + int err = getnameinfo (gai_iter->ai_addr, gai_iter->ai_addrlen, + buf, sizeof buf, NULL, 0, NI_NUMERICHOST); + if (err) + LOG_FUNC_FAILURE ("getnameinfo", gai_strerror (err)); + else + real_host = buf; + + char *address = format_host_port_pair (real_host, target->service); + self->on_connecting (self->user_data, address); + free (address); +} + +static void +connector_notify_error (struct connector *self, const char *error) +{ + if (self->on_error) + self->on_error (self->user_data, error); +} + +static void +connector_prepare_next (struct connector *self) +{ + struct connector_target *target = self->targets; + if (!(target->iter = target->iter->ai_next)) + { + LIST_UNLINK_WITH_TAIL (self->targets, self->targets_t, target); + connector_target_destroy (target); + } +} + +static void +connector_step (struct connector *self) +{ + struct connector_target *target = self->targets; + if (!target) + { + // Total failure, none of the targets has succeeded + self->on_failure (self->user_data); + return; + } + + struct addrinfo *gai_iter = target->iter; + hard_assert (gai_iter != NULL); + + connector_notify_connecting (self, target, gai_iter); + + int fd = self->socket = socket (gai_iter->ai_family, + gai_iter->ai_socktype, gai_iter->ai_protocol); + if (fd == -1) + { + connector_notify_error (self, strerror (errno)); + + connector_prepare_next (self); + connector_step (self); + return; + } + + set_cloexec (fd); + set_blocking (fd, false); + + int yes = 1; + soft_assert (setsockopt (fd, SOL_SOCKET, SO_KEEPALIVE, + &yes, sizeof yes) != -1); + + if (!connect (fd, gai_iter->ai_addr, gai_iter->ai_addrlen)) + { + set_blocking (fd, true); + self->on_connected (self->user_data, fd); + return; + } + else if (errno != EINPROGRESS) + { + connector_notify_error (self, strerror (errno)); + xclose (fd); + + connector_prepare_next (self); + connector_step (self); + return; + } + + self->connected_event.fd = self->socket = fd; + poller_fd_set (&self->connected_event, POLLOUT); + + connector_prepare_next (self); +} + +static void +connector_on_ready (const struct pollfd *pfd, struct connector *self) +{ + // See http://cr.yp.to/docs/connect.html if this doesn't work. + // The second connect() method doesn't work with DragonflyBSD. + + int error = 0; + socklen_t error_len = sizeof error; + hard_assert (!getsockopt (pfd->fd, + SOL_SOCKET, SO_ERROR, &error, &error_len)); + + if (error) + { + connector_notify_error (self, strerror (errno)); + + xclose (self->socket); + self->socket = -1; + + connector_step (self); + } + else + { + poller_fd_reset (&self->connected_event); + self->socket = -1; + + set_blocking (pfd->fd, true); + self->on_connected (self->user_data, pfd->fd); + } +} + +static void +connector_init (struct connector *self, struct poller *poller) +{ + memset (self, 0, sizeof *self); + self->socket = -1; + poller_fd_init (&self->connected_event, poller, self->socket); + self->connected_event.user_data = self; + self->connected_event.dispatcher = (poller_fd_fn) connector_on_ready; +} + +static void +connector_free (struct connector *self) +{ + poller_fd_reset (&self->connected_event); + if (self->socket != -1) + xclose (self->socket); + + LIST_FOR_EACH (struct connector_target, iter, self->targets) + connector_target_destroy (iter); +} + +static bool +connector_add_target (struct connector *self, + const char *hostname, const char *service, struct error **e) +{ + struct addrinfo hints, *results; + memset (&hints, 0, sizeof hints); + hints.ai_socktype = SOCK_STREAM; + + // TODO: even this should be done asynchronously, most likely in + // a thread pool, similarly to how libuv does it + int err = getaddrinfo (hostname, service, &hints, &results); + if (err) + { + error_set (e, "%s: %s", "getaddrinfo", gai_strerror (err)); + return false; + } + + struct connector_target *target = connector_target_new (); + target->hostname = xstrdup (hostname); + target->service = xstrdup (service); + target->results = results; + target->iter = target->results; + + LIST_APPEND_WITH_TAIL (self->targets, self->targets_t, target); + return true; +} + // --- SOCKS 5/4a (blocking implementation) ------------------------------------ // These are awkward protocols. Note that the `username' is used differently diff --git a/degesch.c b/degesch.c index f4e1a81..7cffb3b 100644 --- a/degesch.c +++ b/degesch.c @@ -977,6 +977,7 @@ buffer_destroy (struct buffer *self) enum server_state { IRC_DISCONNECTED, ///< Not connected + IRC_CONNECTING, ///< Connecting to the server IRC_CONNECTED, ///< Trying to register IRC_REGISTERED ///< We can chat now }; @@ -984,8 +985,11 @@ enum server_state struct server { struct app_context *ctx; ///< Application context + bool reconnect; ///< Whether to reconnect on conn. fail. + unsigned long reconnect_delay; ///< Reconnect delay in seconds enum server_state state; ///< Connection state + struct connector *connector; ///< Connection establisher int socket; ///< Socket FD of the server struct str read_buffer; ///< Input yet to be processed @@ -1021,9 +1025,9 @@ struct server struct poller_timer reconnect_tmr; ///< We should reconnect now }; -static void on_irc_ping_timeout (void *user_data); static void on_irc_timeout (void *user_data); -static void on_irc_reconnect_timeout (void *user_data); +static void on_irc_ping_timeout (void *user_data); +static void irc_initiate_connect (struct server *s); static void server_init (struct server *self, struct poller *poller) @@ -1048,13 +1052,18 @@ server_init (struct server *self, struct poller *poller) self->ping_tmr.user_data = self; poller_timer_init (&self->reconnect_tmr, poller); - self->reconnect_tmr.dispatcher = on_irc_reconnect_timeout; + self->reconnect_tmr.dispatcher = (poller_timer_fn) irc_initiate_connect; self->reconnect_tmr.user_data = self; } static void server_free (struct server *self) { + if (self->connector) + { + connector_free (self->connector); + free (self->connector); + } if (self->socket != -1) { xclose (self->socket); @@ -1086,8 +1095,6 @@ struct app_context struct config config; ///< Program configuration char *attrs[ATTR_COUNT]; ///< Terminal attributes bool no_colors; ///< Colour output mode - bool reconnect; ///< Whether to reconnect on conn. fail. - unsigned long reconnect_delay; ///< Reconnect delay in seconds bool isolate_buffers; ///< Isolate global/server buffers struct server server; ///< Our only server so far @@ -2453,11 +2460,11 @@ irc_remove_user_from_channel (struct user *user, struct channel *channel) irc_channel_unlink_user (channel, iter); } -// --- Supporting code --------------------------------------------------------- +// --- Core functionality ------------------------------------------------------ -static bool irc_connect (struct server *s, bool *should_retry, struct error **); -static void irc_cancel_timers (struct server *s); -static void on_irc_reconnect_timeout (void *user_data); +// Most of the core IRC code comes from ZyklonB which is mostly blocking. +// While it's fairly easy to follow, it also stinks. It needs to be rewritten +// to be as asynchronous as possible. See kike.c for reference. static bool irc_is_connected (struct server *s) @@ -2466,117 +2473,36 @@ irc_is_connected (struct server *s) } static void -irc_shutdown (struct server *s) +irc_cancel_timers (struct server *s) { - // TODO: set a timer after which we cut the connection? - // Generally non-critical - if (s->ssl) - soft_assert (SSL_shutdown (s->ssl) != -1); - else - soft_assert (shutdown (s->socket, SHUT_WR) == 0); + poller_timer_reset (&s->timeout_tmr); + poller_timer_reset (&s->ping_tmr); + poller_timer_reset (&s->reconnect_tmr); } static void -try_finish_quit (struct app_context *ctx) +irc_reset_connection_timeouts (struct server *s) { - // TODO: multiserver - if (ctx->quitting && !irc_is_connected (&ctx->server)) - ctx->polling = false; + irc_cancel_timers (s); + poller_timer_set (&s->timeout_tmr, 3 * 60 * 1000); + poller_timer_set (&s->ping_tmr, (3 * 60 + 30) * 1000); } static void -initiate_quit (struct app_context *ctx) +irc_queue_reconnect (struct server *s) { - // Destroy the user interface - input_stop (&ctx->input); + // As long as the user wants us to, that is + if (!s->reconnect) + return; - buffer_send_status (ctx, ctx->global_buffer, "Shutting down"); - - // Initiate a connection close - // TODO: multiserver - struct server *s = &ctx->server; - if (irc_is_connected (s)) - // XXX: when we go async, we'll have to flush output buffers first - irc_shutdown (s); - - ctx->quitting = true; - try_finish_quit (ctx); + // TODO: exponentional backoff + hard_assert (s->socket == -1); + buffer_send_status (s->ctx, s->buffer, + "Trying to reconnect in %ld seconds...", s->reconnect_delay); + poller_timer_set (&s->reconnect_tmr, s->reconnect_delay * 1000); } -// As of 2015, everything should be in UTF-8. And if it's not, we'll decode it -// as ISO Latin 1. This function should not be called on the whole message. -static char * -irc_to_utf8 (struct app_context *ctx, const char *text) -{ - size_t len = strlen (text) + 1; - if (utf8_validate (text, len)) - return xstrdup (text); - return iconv_xstrdup (ctx->latin1_to_utf8, (char *) text, len, NULL); -} - -// This function is used to output debugging IRC traffic to the terminal. -// It's far from ideal, as any non-UTF-8 text degrades the entire line to -// ISO Latin 1. But it should work good enough most of the time. -static char * -irc_to_term (struct app_context *ctx, const char *text) -{ - char *utf8 = irc_to_utf8 (ctx, text); - char *term = iconv_xstrdup (ctx->term_from_utf8, utf8, -1, NULL); - free (utf8); - return term; -} - -static bool irc_send (struct server *s, - const char *format, ...) ATTRIBUTE_PRINTF (2, 3); - -static bool -irc_send (struct server *s, const char *format, ...) -{ - if (!soft_assert (irc_is_connected (s))) - { - print_debug ("tried sending a message to a dead server connection"); - return false; - } - - va_list ap; - va_start (ap, format); - struct str str; - str_init (&str); - str_append_vprintf (&str, format, ap); - va_end (ap); - - if (g_debug_mode) - { - input_hide (&s->ctx->input); - - char *term = irc_to_term (s->ctx, str.str); - fprintf (stderr, "[IRC] <== \"%s\"\n", term); - free (term); - - input_show (&s->ctx->input); - } - str_append (&str, "\r\n"); - - bool result = true; - if (s->ssl) - { - // TODO: call SSL_get_error() to detect if a clean shutdown has occured - if (SSL_write (s->ssl, str.str, str.len) != (int) str.len) - { - LOG_FUNC_FAILURE ("SSL_write", - ERR_error_string (ERR_get_error (), NULL)); - result = false; - } - } - else if (write (s->socket, str.str, str.len) != (ssize_t) str.len) - { - LOG_LIBC_FAILURE ("write"); - result = false; - } - - str_free (&str); - return result; -} +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - static bool irc_initialize_ssl_ctx (struct server *s, struct error **e) @@ -2685,70 +2611,491 @@ error_ssl_1: return false; } +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +// As of 2015, everything should be in UTF-8. And if it's not, we'll decode it +// as ISO Latin 1. This function should not be called on the whole message. +static char * +irc_to_utf8 (struct app_context *ctx, const char *text) +{ + size_t len = strlen (text) + 1; + if (utf8_validate (text, len)) + return xstrdup (text); + return iconv_xstrdup (ctx->latin1_to_utf8, (char *) text, len, NULL); +} + +// This function is used to output debugging IRC traffic to the terminal. +// It's far from ideal, as any non-UTF-8 text degrades the entire line to +// ISO Latin 1. But it should work good enough most of the time. +static char * +irc_to_term (struct app_context *ctx, const char *text) +{ + char *utf8 = irc_to_utf8 (ctx, text); + char *term = iconv_xstrdup (ctx->term_from_utf8, utf8, -1, NULL); + free (utf8); + return term; +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +static void irc_send (struct server *s, + const char *format, ...) ATTRIBUTE_PRINTF (2, 3); + +static void +irc_send (struct server *s, const char *format, ...) +{ + if (!soft_assert (irc_is_connected (s))) + { + print_debug ("tried sending a message to a dead server connection"); + return; + } + + va_list ap; + va_start (ap, format); + struct str str; + str_init (&str); + str_append_vprintf (&str, format, ap); + va_end (ap); + + if (g_debug_mode) + { + input_hide (&s->ctx->input); + + char *term = irc_to_term (s->ctx, str.str); + fprintf (stderr, "[IRC] <== \"%s\"\n", term); + free (term); + + input_show (&s->ctx->input); + } + str_append (&str, "\r\n"); + + if (s->ssl) + { + // TODO: call SSL_get_error() to detect if a clean shutdown has occured + if (SSL_write (s->ssl, str.str, str.len) != (int) str.len) + LOG_FUNC_FAILURE ("SSL_write", + ERR_error_string (ERR_get_error (), NULL)); + } + else if (write (s->socket, str.str, str.len) != (ssize_t) str.len) + LOG_LIBC_FAILURE ("write"); + + str_free (&str); +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +static void +irc_shutdown (struct server *s) +{ + // TODO: set a timer after which we cut the connection? + // Generally non-critical + if (s->ssl) + soft_assert (SSL_shutdown (s->ssl) != -1); + else + soft_assert (shutdown (s->socket, SHUT_WR) == 0); +} + +static void +irc_destroy_connector (struct server *s) +{ + connector_free (s->connector); + free (s->connector); + s->connector = NULL; + + // Not connecting anymore + s->state = IRC_DISCONNECTED; +} + +static void +try_finish_quit (struct app_context *ctx) +{ + // TODO: multiserver + if (ctx->quitting && !irc_is_connected (&ctx->server)) + ctx->polling = false; +} + +static void +initiate_quit (struct app_context *ctx) +{ + // Destroy the user interface + input_stop (&ctx->input); + + buffer_send_status (ctx, ctx->global_buffer, "Shutting down"); + + // Initiate a connection close + // TODO: multiserver + struct server *s = &ctx->server; + if (irc_is_connected (s)) + // XXX: when we go async, we'll have to flush output buffers first + irc_shutdown (s); + else if (s->state == IRC_CONNECTING) + irc_destroy_connector (s); + + ctx->quitting = true; + try_finish_quit (ctx); +} + +static void +on_irc_disconnected (struct server *s) +{ + hard_assert (irc_is_connected (s)); + + // Get rid of the dead socket and related things + if (s->ssl) + { + SSL_free (s->ssl); + s->ssl = NULL; + SSL_CTX_free (s->ssl_ctx); + s->ssl_ctx = NULL; + } + + xclose (s->socket); + s->socket = -1; + s->state = IRC_DISCONNECTED; + + user_unref (s->irc_user); + s->irc_user = NULL; + + free (s->irc_user_mode); + s->irc_user_mode = NULL; + free (s->irc_user_host); + s->irc_user_host = NULL; + + s->read_event.closed = true; + poller_fd_reset (&s->read_event); + + // All of our timers have lost their meaning now + irc_cancel_timers (s); + + if (s->ctx->quitting) + try_finish_quit (s->ctx); + else + irc_queue_reconnect (s); +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +static void +on_irc_ping_timeout (void *user_data) +{ + struct server *s = user_data; + buffer_send_error (s->ctx, s->buffer, "Connection timeout"); + on_irc_disconnected (s); +} + +static void +on_irc_timeout (void *user_data) +{ + // Provoke a response from the server + struct server *s = user_data; + irc_send (s, "PING :%s", get_config_string (s->ctx, s->irc_user->nickname)); +} + +// --- Processing server output ------------------------------------------------ + +static void irc_process_message + (const struct irc_message *msg, const char *raw, void *user_data); + +enum irc_read_result +{ + IRC_READ_OK, ///< Some data were read successfully + IRC_READ_EOF, ///< The server has closed connection + IRC_READ_AGAIN, ///< No more data at the moment + IRC_READ_ERROR ///< General connection failure +}; + +static enum irc_read_result +irc_fill_read_buffer_ssl (struct server *s, struct str *buf) +{ + int n_read; +start: + n_read = SSL_read (s->ssl, buf->str + buf->len, + buf->alloc - buf->len - 1 /* null byte */); + + const char *error_info = NULL; + switch (xssl_get_error (s->ssl, n_read, &error_info)) + { + case SSL_ERROR_NONE: + buf->str[buf->len += n_read] = '\0'; + return IRC_READ_OK; + case SSL_ERROR_ZERO_RETURN: + return IRC_READ_EOF; + case SSL_ERROR_WANT_READ: + return IRC_READ_AGAIN; + case SSL_ERROR_WANT_WRITE: + { + // Let it finish the handshake as we don't poll for writability; + // any errors are to be collected by SSL_read() in the next iteration + struct pollfd pfd = { .fd = s->socket, .events = POLLOUT }; + soft_assert (poll (&pfd, 1, 0) > 0); + goto start; + } + case XSSL_ERROR_TRY_AGAIN: + goto start; + default: + LOG_FUNC_FAILURE ("SSL_read", error_info); + return IRC_READ_ERROR; + } +} + +static enum irc_read_result +irc_fill_read_buffer (struct server *s, struct str *buf) +{ + ssize_t n_read; +start: + n_read = recv (s->socket, buf->str + buf->len, + buf->alloc - buf->len - 1 /* null byte */, 0); + + if (n_read > 0) + { + buf->str[buf->len += n_read] = '\0'; + return IRC_READ_OK; + } + if (n_read == 0) + return IRC_READ_EOF; + + if (errno == EAGAIN) + return IRC_READ_AGAIN; + if (errno == EINTR) + goto start; + + LOG_LIBC_FAILURE ("recv"); + return IRC_READ_ERROR; +} + +static void +on_irc_readable (const struct pollfd *fd, struct server *s) +{ + if (fd->revents & ~(POLLIN | POLLHUP | POLLERR)) + print_debug ("fd %d: unexpected revents: %d", fd->fd, fd->revents); + + (void) set_blocking (s->socket, false); + + struct str *buf = &s->read_buffer; + enum irc_read_result (*fill_buffer)(struct server *, struct str *) + = s->ssl + ? irc_fill_read_buffer_ssl + : irc_fill_read_buffer; + bool disconnected = false; + while (true) + { + str_ensure_space (buf, 512); + switch (fill_buffer (s, buf)) + { + case IRC_READ_AGAIN: + goto end; + case IRC_READ_ERROR: + buffer_send_error (s->ctx, s->buffer, + "Reading from the IRC server failed"); + disconnected = true; + goto end; + case IRC_READ_EOF: + buffer_send_error (s->ctx, s->buffer, + "The IRC server closed the connection"); + disconnected = true; + goto end; + case IRC_READ_OK: + break; + } + + if (buf->len >= (1 << 20)) + { + buffer_send_error (s->ctx, s->buffer, + "The IRC server seems to spew out data frantically"); + irc_shutdown (s); + goto end; + } + } +end: + (void) set_blocking (s->socket, true); + irc_process_buffer (buf, irc_process_message, s); + + if (disconnected) + on_irc_disconnected (s); + else + irc_reset_connection_timeouts (s); +} + +// --- Connection establishment ------------------------------------------------ + +static void +irc_register (struct server *s) +{ + const char *nickname = get_config_string (s->ctx, "server.nickname"); + const char *username = get_config_string (s->ctx, "server.username"); + const char *realname = get_config_string (s->ctx, "server.realname"); + + // These are filled automatically if needed + hard_assert (nickname && username && realname); + + irc_send (s, "NICK %s", nickname); + // IRC servers may ignore the last argument if it's empty + irc_send (s, "USER %s 8 * :%s", username, *realname ? realname : " "); + + // XXX: maybe we should wait for the first message from the server + // FIXME: the user may exist already after we've reconnected. Either + // make sure that there's no reference of this nick upon disconnection, + // or search in "irc_users" first... or something. + s->irc_user = irc_make_user (s, xstrdup (nickname)); + s->irc_user_mode = xstrdup (""); + s->irc_user_host = NULL; +} + +static void +irc_finish_connection (struct server *s, int socket) +{ + struct app_context *ctx = s->ctx; + + s->socket = socket; + + struct error *e = NULL; + bool use_ssl = get_config_boolean (ctx, "server.ssl"); + if (use_ssl && !irc_initialize_ssl (s, &e)) + { + buffer_send_error (ctx, s->buffer, "Connection failed: %s", e->message); + error_free (e); + + xclose (s->socket); + s->socket = -1; + + irc_queue_reconnect (s); + return; + } + + buffer_send_status (ctx, s->buffer, "Connection established"); + s->state = IRC_CONNECTED; + + poller_fd_init (&s->read_event, &ctx->poller, s->socket); + s->read_event.dispatcher = (poller_fd_fn) on_irc_readable; + s->read_event.user_data = s; + + poller_fd_set (&s->read_event, POLLIN); + irc_reset_connection_timeouts (s); + + irc_register (s); +} + +static void +irc_on_connector_connecting (void *user_data, const char *address) +{ + struct server *s = user_data; + buffer_send_status (s->ctx, s->buffer, "Connecting to %s...", address); +} + +static void +irc_on_connector_error (void *user_data, const char *error) +{ + struct server *s = user_data; + buffer_send_error (s->ctx, s->buffer, "Connection failed: %s", error); +} + +static void +irc_on_connector_failure (void *user_data) +{ + struct server *s = user_data; + irc_destroy_connector (s); + irc_queue_reconnect (s); +} + +static void +irc_on_connector_connected (void *user_data, int socket) +{ + struct server *s = user_data; + irc_destroy_connector (s); + irc_finish_connection (s, socket); +} + static bool -irc_establish_connection (struct server *s, +irc_setup_connector (struct server *s, const char *host, const char *port, struct error **e) { - struct addrinfo gai_hints, *gai_result, *gai_iter; - memset (&gai_hints, 0, sizeof gai_hints); - gai_hints.ai_socktype = SOCK_STREAM; + struct connector *connector = xmalloc (sizeof *connector); + connector_init (connector, &s->ctx->poller); - int err = getaddrinfo (host, port, &gai_hints, &gai_result); - if (err) + connector->user_data = s; + connector->on_connecting = irc_on_connector_connecting; + connector->on_error = irc_on_connector_error; + connector->on_connected = irc_on_connector_connected; + connector->on_failure = irc_on_connector_failure; + + s->state = IRC_CONNECTING; + s->connector = connector; + + if (!connector_add_target (connector, host, port, e)) { - error_set (e, "%s: %s: %s", - "connection failed", "getaddrinfo", gai_strerror (err)); + irc_destroy_connector (s); return false; } - int sockfd; - for (gai_iter = gai_result; gai_iter; gai_iter = gai_iter->ai_next) - { - sockfd = socket (gai_iter->ai_family, - gai_iter->ai_socktype, gai_iter->ai_protocol); - if (sockfd == -1) - continue; - set_cloexec (sockfd); - - int yes = 1; - soft_assert (setsockopt (sockfd, SOL_SOCKET, SO_KEEPALIVE, - &yes, sizeof yes) != -1); - - const char *real_host = host; - - // Let's try to resolve the address back into a real hostname; - // we don't really need this, so we can let it quietly fail - char buf[NI_MAXHOST]; - err = getnameinfo (gai_iter->ai_addr, gai_iter->ai_addrlen, - buf, sizeof buf, NULL, 0, NI_NUMERICHOST); - if (err) - LOG_FUNC_FAILURE ("getnameinfo", gai_strerror (err)); - else - real_host = buf; - - char *address = format_host_port_pair (real_host, port); - buffer_send_status (s->ctx, s->buffer, "Connecting to %s...", address); - free (address); - - if (!connect (sockfd, gai_iter->ai_addr, gai_iter->ai_addrlen)) - break; - - xclose (sockfd); - } - - freeaddrinfo (gai_result); - - if (!gai_iter) - { - error_set (e, "connection failed"); - return false; - } - - s->socket = sockfd; + connector_step (connector); return true; } -// --- More readline funky stuff ----------------------------------------------- +static void +irc_initiate_connect (struct server *s) +{ + hard_assert (s->state == IRC_DISCONNECTED); + struct app_context *ctx = s->ctx; + + const char *irc_host = get_config_string (ctx, "server.irc_host"); + int64_t irc_port_int = get_config_integer (ctx, "server.irc_port"); + + if (!get_config_string (ctx, "server.irc_host")) + { + // No sense in trying to reconnect + buffer_send_error (ctx, s->buffer, + "No hostname specified in configuration"); + return; + } + + const char *socks_host = get_config_string (ctx, "server.socks_host"); + int64_t socks_port_int = get_config_integer (ctx, "server.socks_port"); + + const char *socks_username = + get_config_string (ctx, "server.socks_username"); + const char *socks_password = + get_config_string (ctx, "server.socks_password"); + + char *irc_port = xstrdup_printf ("%" PRIi64, irc_port_int); + char *socks_port = xstrdup_printf ("%" PRIi64, socks_port_int); + + // TODO: the SOCKS code needs a rewrite so that we don't block on it either + struct error *e = NULL; + if (socks_host) + { + char *address = format_host_port_pair (irc_host, irc_port); + char *socks_address = format_host_port_pair (socks_host, socks_port); + buffer_send_status (ctx, s->buffer, + "Connecting to %s via %s...", address, socks_address); + free (socks_address); + free (address); + + struct error *error = NULL; + int fd = socks_connect (socks_host, socks_port, irc_host, irc_port, + socks_username, socks_password, &error); + if (fd != -1) + irc_finish_connection (s, fd); + else + { + error_set (&e, "%s: %s", "SOCKS connection failed", error->message); + error_free (error); + } + } + else + irc_setup_connector (s, irc_host, irc_port, &e); + + free (irc_port); + free (socks_port); + + if (e) + { + buffer_send_error (s->ctx, s->buffer, "%s", e->message); + error_free (e); + irc_queue_reconnect (s); + } +} + +// --- Input prompt ------------------------------------------------------------ static char * make_unseen_prefix (struct app_context *ctx) @@ -4428,7 +4775,7 @@ handle_command_connect (struct app_context *ctx, char *arguments) } irc_cancel_timers (s); - on_irc_reconnect_timeout (s); + irc_initiate_connect (s); return true; } @@ -4725,318 +5072,6 @@ process_input (struct app_context *ctx, char *user_input) free (input); } -// --- Supporting code (continued) --------------------------------------------- - -enum irc_read_result -{ - IRC_READ_OK, ///< Some data were read successfully - IRC_READ_EOF, ///< The server has closed connection - IRC_READ_AGAIN, ///< No more data at the moment - IRC_READ_ERROR ///< General connection failure -}; - -static enum irc_read_result -irc_fill_read_buffer_ssl (struct server *s, struct str *buf) -{ - int n_read; -start: - n_read = SSL_read (s->ssl, buf->str + buf->len, - buf->alloc - buf->len - 1 /* null byte */); - - const char *error_info = NULL; - switch (xssl_get_error (s->ssl, n_read, &error_info)) - { - case SSL_ERROR_NONE: - buf->str[buf->len += n_read] = '\0'; - return IRC_READ_OK; - case SSL_ERROR_ZERO_RETURN: - return IRC_READ_EOF; - case SSL_ERROR_WANT_READ: - return IRC_READ_AGAIN; - case SSL_ERROR_WANT_WRITE: - { - // Let it finish the handshake as we don't poll for writability; - // any errors are to be collected by SSL_read() in the next iteration - struct pollfd pfd = { .fd = s->socket, .events = POLLOUT }; - soft_assert (poll (&pfd, 1, 0) > 0); - goto start; - } - case XSSL_ERROR_TRY_AGAIN: - goto start; - default: - LOG_FUNC_FAILURE ("SSL_read", error_info); - return IRC_READ_ERROR; - } -} - -static enum irc_read_result -irc_fill_read_buffer (struct server *s, struct str *buf) -{ - ssize_t n_read; -start: - n_read = recv (s->socket, buf->str + buf->len, - buf->alloc - buf->len - 1 /* null byte */, 0); - - if (n_read > 0) - { - buf->str[buf->len += n_read] = '\0'; - return IRC_READ_OK; - } - if (n_read == 0) - return IRC_READ_EOF; - - if (errno == EAGAIN) - return IRC_READ_AGAIN; - if (errno == EINTR) - goto start; - - LOG_LIBC_FAILURE ("recv"); - return IRC_READ_ERROR; -} - -static void -irc_cancel_timers (struct server *s) -{ - poller_timer_reset (&s->timeout_tmr); - poller_timer_reset (&s->ping_tmr); - poller_timer_reset (&s->reconnect_tmr); -} - -static void -irc_queue_reconnect (struct server *s) -{ - // TODO: exponentional backoff - hard_assert (s->socket == -1); - buffer_send_status (s->ctx, s->buffer, - "Trying to reconnect in %ld seconds...", s->ctx->reconnect_delay); - poller_timer_set (&s->reconnect_tmr, s->ctx->reconnect_delay * 1000); -} - -static void -on_irc_reconnect_timeout (void *user_data) -{ - struct server *s = user_data; - - struct error *e = NULL; - bool should_retry = false; - if (irc_connect (s, &should_retry, &e)) - return; - - buffer_send_error (s->ctx, s->buffer, "%s", e->message); - error_free (e); - - if (should_retry) - irc_queue_reconnect (s); -} - -static void -on_irc_disconnected (struct server *s) -{ - // Get rid of the dead socket and related things - if (s->ssl) - { - SSL_free (s->ssl); - s->ssl = NULL; - SSL_CTX_free (s->ssl_ctx); - s->ssl_ctx = NULL; - } - - xclose (s->socket); - s->socket = -1; - s->state = IRC_DISCONNECTED; - - user_unref (s->irc_user); - s->irc_user = NULL; - - free (s->irc_user_mode); - s->irc_user_mode = NULL; - free (s->irc_user_host); - s->irc_user_host = NULL; - - s->read_event.closed = true; - poller_fd_reset (&s->read_event); - - // All of our timers have lost their meaning now - irc_cancel_timers (s); - - if (s->ctx->quitting) - try_finish_quit (s->ctx); - else if (!s->ctx->reconnect) - // XXX: not sure if we want this in a client - // FIXME: no, we don't, would need to be changed for multiserver anyway - initiate_quit (s->ctx); - else - irc_queue_reconnect (s); -} - -static void -on_irc_ping_timeout (void *user_data) -{ - struct server *s = user_data; - buffer_send_error (s->ctx, s->buffer, "Connection timeout"); - on_irc_disconnected (s); -} - -static void -on_irc_timeout (void *user_data) -{ - // Provoke a response from the server - struct server *s = user_data; - irc_send (s, "PING :%s", get_config_string (s->ctx, "server.nickname")); -} - -static void -irc_reset_connection_timeouts (struct server *s) -{ - irc_cancel_timers (s); - poller_timer_set (&s->timeout_tmr, 3 * 60 * 1000); - poller_timer_set (&s->ping_tmr, (3 * 60 + 30) * 1000); -} - -static void -on_irc_readable (const struct pollfd *fd, struct server *s) -{ - if (fd->revents & ~(POLLIN | POLLHUP | POLLERR)) - print_debug ("fd %d: unexpected revents: %d", fd->fd, fd->revents); - - (void) set_blocking (s->socket, false); - - struct str *buf = &s->read_buffer; - enum irc_read_result (*fill_buffer)(struct server *, struct str *) - = s->ssl - ? irc_fill_read_buffer_ssl - : irc_fill_read_buffer; - bool disconnected = false; - while (true) - { - str_ensure_space (buf, 512); - switch (fill_buffer (s, buf)) - { - case IRC_READ_AGAIN: - goto end; - case IRC_READ_ERROR: - buffer_send_error (s->ctx, s->buffer, - "Reading from the IRC server failed"); - disconnected = true; - goto end; - case IRC_READ_EOF: - buffer_send_error (s->ctx, s->buffer, - "The IRC server closed the connection"); - disconnected = true; - goto end; - case IRC_READ_OK: - break; - } - - if (buf->len >= (1 << 20)) - { - buffer_send_error (s->ctx, s->buffer, - "The IRC server seems to spew out data frantically"); - irc_shutdown (s); - goto end; - } - } -end: - (void) set_blocking (s->socket, true); - irc_process_buffer (buf, irc_process_message, s); - - if (disconnected) - on_irc_disconnected (s); - else - irc_reset_connection_timeouts (s); -} - -static bool -irc_connect (struct server *s, bool *should_retry, struct error **e) -{ - // TODO: connect asynchronously so that we don't freeze - struct app_context *ctx = s->ctx; - *should_retry = true; - - const char *irc_host = get_config_string (ctx, "server.irc_host"); - int64_t irc_port_int = get_config_integer (ctx, "server.irc_port"); - - if (!get_config_string (ctx, "server.irc_host")) - { - error_set (e, "No hostname specified in configuration"); - *should_retry = false; - return false; - } - - const char *socks_host = get_config_string (ctx, "server.socks_host"); - int64_t socks_port_int = get_config_integer (ctx, "server.socks_port"); - const char *socks_username = - get_config_string (ctx, "server.socks_username"); - const char *socks_password = - get_config_string (ctx, "server.socks_password"); - - // FIXME: use it as a number everywhere, there's no need for named services - // FIXME: memory leak - char *irc_port = xstrdup_printf ("%" PRIi64, irc_port_int); - char *socks_port = xstrdup_printf ("%" PRIi64, socks_port_int); - - const char *nickname = get_config_string (ctx, "server.nickname"); - const char *username = get_config_string (ctx, "server.username"); - const char *realname = get_config_string (ctx, "server.realname"); - - // These are filled automatically if needed - hard_assert (nickname && username && realname); - - bool use_ssl = get_config_boolean (ctx, "server.ssl"); - if (socks_host) - { - char *address = format_host_port_pair (irc_host, irc_port); - char *socks_address = format_host_port_pair (socks_host, socks_port); - buffer_send_status (ctx, s->buffer, - "Connecting to %s via %s...", address, socks_address); - free (socks_address); - free (address); - - struct error *error = NULL; - int fd = socks_connect (socks_host, socks_port, irc_host, irc_port, - socks_username, socks_password, &error); - if (fd == -1) - { - error_set (e, "%s: %s", "SOCKS connection failed", error->message); - error_free (error); - return false; - } - s->socket = fd; - } - else if (!irc_establish_connection (s, irc_host, irc_port, e)) - return false; - - if (use_ssl && !irc_initialize_ssl (s, e)) - { - xclose (s->socket); - s->socket = -1; - return false; - } - - s->state = IRC_CONNECTED; - buffer_send_status (ctx, s->buffer, "Connection established"); - - poller_fd_init (&s->read_event, &ctx->poller, s->socket); - s->read_event.dispatcher = (poller_fd_fn) on_irc_readable; - s->read_event.user_data = s; - - poller_fd_set (&s->read_event, POLLIN); - irc_reset_connection_timeouts (s); - - irc_send (s, "NICK %s", nickname); - // IRC servers may ignore the last argument if it's empty - irc_send (s, "USER %s 8 * :%s", username, *realname ? realname : " "); - - // XXX: maybe we should wait for the first message from the server - // FIXME: the user may exist already after we've reconnected. Either - // make sure that there's no reference of this nick upon disconnection, - // or search in "irc_users" first... or something. - s->irc_user = irc_make_user (s, xstrdup (nickname)); - s->irc_user_mode = xstrdup (""); - s->irc_user_host = NULL; - return true; -} - // --- Word completion --------------------------------------------------------- // The amount of crap that goes into this is truly insane. @@ -5854,11 +5889,11 @@ load_configuration (struct app_context *ctx) error_free (e); } - ctx->reconnect = - get_config_boolean (ctx, "server.reconnect"); ctx->isolate_buffers = get_config_boolean (ctx, "behaviour.isolate_buffers"); - ctx->reconnect_delay = + ctx->server.reconnect = + get_config_boolean (ctx, "server.reconnect"); + ctx->server.reconnect_delay = get_config_integer (ctx, "server.reconnect_delay"); }