diff options
Diffstat (limited to 'src/kike.c')
-rw-r--r-- | src/kike.c | 796 |
1 files changed, 796 insertions, 0 deletions
diff --git a/src/kike.c b/src/kike.c new file mode 100644 index 0000000..bf13476 --- /dev/null +++ b/src/kike.c @@ -0,0 +1,796 @@ +/* + * kike.c: the experimental IRC daemon + * + * Copyright (c) 2014, Přemysl Janouch <p.janouch@gmail.com> + * All rights reserved. + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY + * SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION + * OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN + * CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + * + */ + +#define PROGRAM_NAME "kike" +#define PROGRAM_VERSION "alpha" + +#include "common.c" + +// --- Configuration (application-specific) ------------------------------------ + +static struct config_item g_config_table[] = +{ + { "server_name", NULL, "Server name" }, + { "bind_host", NULL, "Address of the IRC server" }, + { "bind_port", "6667", "Port of the IRC server" }, + { "ssl_cert", NULL, "Server SSL certificate (PEM)" }, + { "ssl_key", NULL, "Server SSL private key (PEM)" }, + + { "max_connections", NULL, "Maximum client connections" }, + { NULL, NULL, NULL } +}; + +// --- Signals ----------------------------------------------------------------- + +static int g_signal_pipe[2]; ///< A pipe used to signal... signals + +/// Program termination has been requested by a signal +static volatile sig_atomic_t g_termination_requested; + +static void +sigterm_handler (int signum) +{ + (void) signum; + + g_termination_requested = true; + + int original_errno = errno; + if (write (g_signal_pipe[1], "t", 1) == -1) + soft_assert (errno == EAGAIN); + errno = original_errno; +} + +static void +setup_signal_handlers (void) +{ + if (pipe (g_signal_pipe) == -1) + { + print_fatal ("pipe: %s", strerror (errno)); + exit (EXIT_FAILURE); + } + + set_cloexec (g_signal_pipe[0]); + set_cloexec (g_signal_pipe[1]); + + // So that the pipe cannot overflow; it would make write() block within + // the signal handler, which is something we really don't want to happen. + // The same holds true for read(). + set_blocking (g_signal_pipe[0], false); + set_blocking (g_signal_pipe[1], false); + + signal (SIGPIPE, SIG_IGN); + + struct sigaction sa; + sa.sa_flags = SA_RESTART; + sigemptyset (&sa.sa_mask); + sa.sa_handler = sigterm_handler; + if (sigaction (SIGINT, &sa, NULL) == -1 + || sigaction (SIGTERM, &sa, NULL) == -1) + { + print_error ("sigaction: %s", strerror (errno)); + exit (EXIT_FAILURE); + } +} + +// --- Application data -------------------------------------------------------- + +enum +{ + IRC_USER_MODE_INVISIBLE = (1 << 0), + IRC_USER_MODE_RX_WALLOPS = (1 << 1), + IRC_USER_MODE_RESTRICTED = (1 << 2), + IRC_USER_MODE_OPERATOR = (1 << 3), + IRC_USER_MODE_RX_SERVER_NOTICES = (1 << 4) +}; + +struct connection +{ + struct connection *next; ///< The next link in a chain + struct connection *prev; ///< The previous link in a chain + + struct server_context *ctx; ///< Server context + + int socket_fd; ///< The TCP socket + struct str read_buffer; ///< Unprocessed input + struct str write_buffer; ///< Output yet to be sent out + + unsigned initialized : 1; ///< Has any data been received yet? + unsigned ssl_rx_want_tx : 1; ///< SSL_read() wants to write + unsigned ssl_tx_want_rx : 1; ///< SSL_write() wants to read + + SSL_CTX *ssl_ctx; ///< SSL context + SSL *ssl; ///< SSL connection + + char *nickname; ///< IRC nickname (main identifier) + char *username; ///< IRC username + char *fullname; ///< IRC fullname (e-mail) + + char *hostname; ///< Hostname shown to the network + + unsigned mode; ///< User's mode + char *away_message; ///< Away message +}; + +static void +connection_init (struct connection *self) +{ + memset (self, 0, sizeof *self); + + self->socket_fd = -1; + str_init (&self->read_buffer); + str_init (&self->write_buffer); +} + +static void +connection_free (struct connection *self) +{ + if (!soft_assert (self->socket_fd == -1)) + xclose (self->socket_fd); + if (self->ssl_ctx) + SSL_CTX_free (self->ssl_ctx); + if (self->ssl) + SSL_free (self->ssl); + + str_free (&self->read_buffer); + str_free (&self->write_buffer); + + free (self->nickname); + free (self->username); + free (self->fullname); + + free (self->hostname); + free (self->away_message); +} + +enum +{ + IRC_CHAN_MODE_INVITE_ONLY = (1 << 0), + IRC_CHAN_MODE_MODERATED = (1 << 1), + IRC_CHAN_MODE_NO_OUTSIDE_MSGS = (1 << 2), + IRC_CHAN_MODE_SECRET = (1 << 3), + IRC_CHAN_MODE_PRIVATE = (1 << 4), + IRC_CHAN_MODE_PROTECTED_TOPIC = (1 << 5), + IRC_CHAN_MODE_QUIET = (1 << 6) +}; + +struct channel +{ + struct server_context *ctx; ///< Server context + + char *name; ///< Channel name + unsigned modes; ///< Channel modes + char *key; ///< Channel key + long user_limit; ///< User limit or -1 + + struct str_vector ban_list; ///< Ban list + struct str_vector exception_list; ///< Exceptions from bans + struct str_vector invite_list; ///< Exceptions from +I +}; + +static void +channel_init (struct channel *self) +{ + memset (self, 0, sizeof *self); + + str_vector_init (&self->ban_list); + str_vector_init (&self->exception_list); + str_vector_init (&self->invite_list); +} + +static void +channel_free (struct channel *self) +{ + free (self->name); + free (self->key); + + str_vector_free (&self->ban_list); + str_vector_free (&self->exception_list); + str_vector_free (&self->invite_list); +} + +struct server_context +{ + struct str_map config; ///< Server configuration + + int listen_fd; ///< Listening socket FD + struct connection *clients; ///< Client connections + + struct str_map users; ///< Maps nicknames to connections + struct str_map channels; ///< Maps channel names to data + + struct poller poller; ///< Manages polled description + bool polling; ///< The event loop is running +}; + +static void +server_context_init (struct server_context *self) +{ + str_map_init (&self->config); + self->config.free = free; + load_config_defaults (&self->config, g_config_table); + + self->listen_fd = -1; + self->clients = NULL; + + str_map_init (&self->users); + // TODO: set channel_free() as the free function? + str_map_init (&self->channels); + + poller_init (&self->poller); + self->polling = false; +} + +static void +server_context_free (struct server_context *self) +{ + str_map_free (&self->config); + + if (self->listen_fd != -1) + xclose (self->listen_fd); + + // TODO: terminate the connections properly before this is called + struct connection *link, *tmp; + for (link = self->clients; link; link = tmp) + { + tmp = link->next; + connection_free (link); + free (link); + } + + str_map_free (&self->users); + str_map_free (&self->channels); + poller_free (&self->poller); +} + +// --- Main program ------------------------------------------------------------ + +static size_t network_error_domain_tag; +#define NETWORK_ERROR (error_resolve_domain (&network_error_domain_tag)) + +enum +{ + NETWORK_ERROR_INVALID_CONFIGURATION, + NETWORK_ERROR_FAILED +}; + +static bool +irc_autodetect_ssl (struct connection *conn) +{ + // Trivial SSL/TLS autodetection. The first block of data returned by + // recv() must be at least three bytes long for this to work reliably, + // but that should not pose a problem in practice. + // + // SSL2: 1xxx xxxx | xxxx xxxx | <1> + // (message length) (client hello) + // SSL3/TLS: <22> | <3> | xxxx xxxx + // (handshake)| (protocol version) + // + // Such byte sequences should never occur at the beginning of regular IRC + // communication, which usually begins with USER/NICK/PASS/SERVICE. + + char buf[3]; +start: + switch (recv (conn->socket_fd, buf, sizeof buf, MSG_PEEK)) + { + case 3: + if ((buf[0] & 0x80) && buf[2] == 1) + return true; + case 2: + if (buf[0] == 22 && buf[1] == 3) + return true; + break; + case 1: + if (buf[0] == 22) + return true; + break; + case 0: + break; + default: + if (errno == EINTR) + goto start; + } + return false; +} + +static int +irc_ssl_verify_callback (int verify_ok, X509_STORE_CTX *ctx) +{ + (void) verify_ok; + (void) ctx; + + // We only want to provide additional privileges based on the client's + // certificate, so let's not terminate the connection because of a failure. + return 1; +} + +static bool +irc_initialize_ssl (struct connection *conn) +{ + struct server_context *ctx = conn->ctx; + + conn->ssl_ctx = SSL_CTX_new (SSLv23_server_method ()); + if (!conn->ssl_ctx) + goto error_ssl_1; + SSL_CTX_set_verify (conn->ssl_ctx, + SSL_VERIFY_PEER | SSL_VERIFY_CLIENT_ONCE, irc_ssl_verify_callback); + // XXX: maybe we should call SSL_CTX_set_options() for some workarounds + + conn->ssl = SSL_new (conn->ssl_ctx); + if (!conn->ssl) + goto error_ssl_2; + + const char *ssl_cert = str_map_find (&ctx->config, "ssl_cert"); + if (ssl_cert + && !SSL_CTX_use_certificate_chain_file (conn->ssl_ctx, ssl_cert)) + { + // XXX: perhaps we should read the file ourselves for better messages + print_error ("%s: %s", "setting the SSL client certificate failed", + ERR_error_string (ERR_get_error (), NULL)); + } + + const char *ssl_key = str_map_find (&ctx->config, "ssl_key"); + if (ssl_key + && !SSL_use_PrivateKey_file (conn->ssl, ssl_key, SSL_FILETYPE_PEM)) + { + // XXX: perhaps we should read the file ourselves for better messages + print_error ("%s: %s", "setting the SSL private key failed", + ERR_error_string (ERR_get_error (), NULL)); + } + + // TODO: SSL_check_private_key(conn->ssl)? It is has probably already been + // checked by SSL_use_PrivateKey_file() above. + + SSL_set_accept_state (conn->ssl); + if (!SSL_set_fd (conn->ssl, conn->socket_fd)) + goto error_ssl_3; + // Gah, spare me your awkward semantics, I just want to push data! + // XXX: do we want SSL_MODE_AUTO_RETRY as well? I guess not. + SSL_set_mode (conn->ssl, + SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER | SSL_MODE_ENABLE_PARTIAL_WRITE); + return true; + +error_ssl_3: + SSL_free (conn->ssl); + conn->ssl = NULL; +error_ssl_2: + SSL_CTX_free (conn->ssl_ctx); + conn->ssl_ctx = NULL; +error_ssl_1: + // XXX: these error strings are really nasty; also there could be + // multiple errors on the OpenSSL stack. + print_error ("%s: %s", "could not initialize SSL", + ERR_error_string (ERR_get_error (), NULL)); + return false; +} + +static void +connection_abort (struct connection *conn, const char *reason) +{ + // TODO: send a QUIT message with `reason' || "Client exited" + (void) reason; + + // TODO: do further cleanup if the client has successfully registered + + struct server_context *ctx = conn->ctx; + ssize_t i = poller_find_by_fd (&ctx->poller, conn->socket_fd); + if (i != -1) + poller_remove_at_index (&ctx->poller, i); + + xclose (conn->socket_fd); + conn->socket_fd = -1; + connection_free (conn); + LIST_UNLINK (ctx->clients, conn); + free (conn); +} + +static void +irc_process_message (const struct irc_message *msg, + const char *raw, void *user_data) +{ + struct connection *conn = user_data; + // TODO +} + +static bool +irc_try_read (struct connection *conn) +{ + // TODO + return false; +} + +static bool +irc_try_read_ssl (struct connection *conn) +{ + if (conn->ssl_tx_want_rx) + return true; + + struct str *buf = &conn->read_buffer; + conn->ssl_rx_want_tx = false; + while (true) + { + str_ensure_space (buf, 512); + int n_read = SSL_read (conn->ssl, buf->str + buf->len, + buf->alloc - buf->len - 1 /* null byte */); + + const char *error_info = NULL; + switch (xssl_get_error (conn->ssl, n_read, &error_info)) + { + case SSL_ERROR_NONE: + buf->str[buf->len += n_read] = '\0'; + // TODO: discard characters above the 512 character limit + irc_process_buffer (buf, irc_process_message, conn); + continue; + case SSL_ERROR_ZERO_RETURN: + connection_abort (conn, NULL); + return false; + case SSL_ERROR_WANT_READ: + return true; + case SSL_ERROR_WANT_WRITE: + conn->ssl_rx_want_tx = true; + return false; + case XSSL_ERROR_TRY_AGAIN: + continue; + default: + print_debug ("%s: %s: %s", __func__, "SSL_read", error_info); + connection_abort (conn, error_info); + return false; + } + } +} + +static bool +irc_try_write (struct connection *conn) +{ + // TODO + return false; +} + +static bool +irc_try_write_ssl (struct connection *conn) +{ + if (conn->ssl_rx_want_tx) + return true; + + struct str *buf = &conn->write_buffer; + conn->ssl_tx_want_rx = false; + while (buf->len) + { + int n_written = SSL_write (conn->ssl, buf->str, buf->len); + + const char *error_info = NULL; + switch (xssl_get_error (conn->ssl, n_written, &error_info)) + { + case SSL_ERROR_NONE: + str_remove_slice (buf, 0, n_written); + continue; + case SSL_ERROR_ZERO_RETURN: + connection_abort (conn, NULL); + return false; + case SSL_ERROR_WANT_WRITE: + return true; + case SSL_ERROR_WANT_READ: + conn->ssl_tx_want_rx = true; + return false; + case XSSL_ERROR_TRY_AGAIN: + continue; + default: + print_debug ("%s: %s: %s", __func__, "SSL_write", error_info); + connection_abort (conn, error_info); + return false; + } + } + return true; +} + +static void +on_irc_client_ready (const struct pollfd *pfd, void *user_data) +{ + // XXX: check/load `ssl_cert' and `ssl_key' earlier? + struct connection *conn = user_data; + if (!conn->initialized) + { + hard_assert (pfd->events == POLLIN); + // XXX: what with the error from irc_initialize_ssl()? + if (irc_autodetect_ssl (conn) && !irc_initialize_ssl (conn)) + { + connection_abort (conn, NULL); + return; + } + conn->initialized = true; + } + + // FIXME: aborting a connection inside try_read() will fuck things up + int new_events = 0; + if (conn->ssl) + { + // Reads may want to write, writes may want to read, poll() may + // return unexpected things in `revents'... let's try both + irc_try_read_ssl (conn) && irc_try_write_ssl (conn); + + new_events |= POLLIN; + if (conn->write_buffer.len || conn->ssl_rx_want_tx) + new_events |= POLLOUT; + + // While we're waiting for an opposite event, we ignore the original + if (conn->ssl_rx_want_tx) new_events &= ~POLLIN; + if (conn->ssl_tx_want_rx) new_events &= ~POLLOUT; + } + else + { + irc_try_read (conn) && irc_try_write (conn); + + new_events |= POLLIN; + if (conn->write_buffer.len) + new_events |= POLLOUT; + } + + hard_assert (new_events != 0); + if (pfd->events != new_events) + poller_set (&conn->ctx->poller, conn->socket_fd, new_events, + (poller_dispatcher_func) on_irc_client_ready, conn); +} + +static void +on_irc_connection_available (const struct pollfd *pfd, void *user_data) +{ + (void) pfd; + struct server_context *ctx = user_data; + + // TODO: stop accepting new connections when `max_connections' is reached + + while (true) + { + // XXX: `struct sockaddr_storage' is not the most portable thing + struct sockaddr_storage peer; + socklen_t peer_len = sizeof peer; + + int fd = accept (ctx->listen_fd, (struct sockaddr *) &peer, &peer_len); + if (fd == -1) + { + if (errno == EAGAIN) + break; + if (errno == EINTR) + continue; + if (errno == ECONNABORTED) + continue; + + // TODO: handle resource exhaustion (EMFILE, ENFILE) specially + // (stop accepting new connections and wait until we close some). + print_fatal ("%s: %s", "accept", strerror (errno)); + + // FIXME: handle this better, bring the server down cleanly. + exit (EXIT_FAILURE); + } + + char host[NI_MAXHOST] = "unknown", port[NI_MAXSERV] = "unknown"; + int err = getnameinfo ((struct sockaddr *) &peer, peer_len, + host, sizeof host, port, sizeof port, AI_NUMERICSERV); + if (err) + print_debug ("%s: %s", "getnameinfo", gai_strerror (err)); + print_debug ("accepted connection from %s:%s", host, port); + + struct connection *conn = xmalloc (sizeof *conn); + connection_init (conn); + conn->socket_fd = fd; + conn->hostname = xstrdup (host); + LIST_PREPEND (ctx->clients, conn); + + // TODO: set a timeout on the socket, something like 3 minutes, then we + // should terminate the connection. + poller_set (&ctx->poller, conn->socket_fd, POLLIN, + (poller_dispatcher_func) on_irc_client_ready, conn); + } +} + +static bool +irc_listen (struct server_context *ctx, struct error **e) +{ + const char *bind_host = str_map_find (&ctx->config, "bind_host"); + const char *bind_port = str_map_find (&ctx->config, "bind_port"); + hard_assert (bind_port != NULL); // We have a default value for this + + struct addrinfo gai_hints, *gai_result, *gai_iter; + memset (&gai_hints, 0, sizeof gai_hints); + + gai_hints.ai_socktype = SOCK_STREAM; + gai_hints.ai_flags = AI_PASSIVE; + + int err = getaddrinfo (bind_host, bind_port, &gai_hints, &gai_result); + if (err) + { + error_set (e, NETWORK_ERROR, NETWORK_ERROR_FAILED, "%s: %s: %s", + "network setup failed", "getaddrinfo", gai_strerror (err)); + return false; + } + + int sockfd; + char real_host[NI_MAXHOST], real_port[NI_MAXSERV]; + + 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); + soft_assert (setsockopt (sockfd, SOL_SOCKET, SO_REUSEADDR, + &yes, sizeof yes) != -1); + + real_host[0] = real_port[0] = '\0'; + err = getnameinfo (gai_iter->ai_addr, gai_iter->ai_addrlen, + real_host, sizeof real_host, real_port, sizeof real_port, + NI_NUMERICHOST | NI_NUMERICSERV); + if (err) + print_debug ("%s: %s", "getnameinfo", gai_strerror (err)); + + if (bind (sockfd, gai_iter->ai_addr, gai_iter->ai_addrlen)) + print_error ("bind() to %s:%s failed: %s", + real_host, real_port, strerror (errno)); + else if (listen (sockfd, 16 /* arbitrary number */)) + print_error ("listen() at %s:%s failed: %s", + real_host, real_port, strerror (errno)); + else + break; + + xclose (sockfd); + } + + freeaddrinfo (gai_result); + + if (!gai_iter) + { + error_set (e, NETWORK_ERROR, NETWORK_ERROR_FAILED, + "network setup failed"); + return false; + } + + ctx->listen_fd = sockfd; + poller_set (&ctx->poller, ctx->listen_fd, POLLIN, + (poller_dispatcher_func) on_irc_connection_available, ctx); + + print_status ("listening at %s:%s", real_host, real_port); + return true; +} + +static void +on_signal_pipe_readable (const struct pollfd *fd, struct server_context *ctx) +{ + char *dummy; + (void) read (fd->fd, &dummy, 1); + +#if 0 + // TODO + if (g_termination_requested && !ctx->quitting) + { + initiate_quit (ctx); + } +#endif +} + +static void +print_usage (const char *program_name) +{ + fprintf (stderr, + "Usage: %s [OPTION]...\n" + "Experimental IRC server.\n" + "\n" + " -d, --debug run in debug mode (do not daemonize)\n" + " -h, --help display this help and exit\n" + " -V, --version output version information and exit\n" + " --write-default-cfg [filename]\n" + " write a default configuration file and exit\n", + program_name); +} + +int +main (int argc, char *argv[]) +{ + const char *invocation_name = argv[0]; + + struct error *e = NULL; + static struct option opts[] = + { + { "debug", no_argument, NULL, 'd' }, + { "help", no_argument, NULL, 'h' }, + { "version", no_argument, NULL, 'V' }, + { "write-default-cfg", optional_argument, NULL, 'w' }, + { NULL, 0, NULL, 0 } + }; + + while (1) + { + int c, opt_index; + + c = getopt_long (argc, argv, "dhV", opts, &opt_index); + if (c == -1) + break; + + switch (c) + { + case 'd': + g_debug_mode = true; + break; + case 'h': + print_usage (invocation_name); + exit (EXIT_SUCCESS); + case 'V': + printf (PROGRAM_NAME " " PROGRAM_VERSION "\n"); + exit (EXIT_SUCCESS); + case 'w': + { + char *filename = write_default_config (optarg, g_config_table, &e); + if (!filename) + { + print_fatal ("%s", e->message); + error_free (e); + exit (EXIT_FAILURE); + } + print_status ("configuration written to `%s'", filename); + free (filename); + exit (EXIT_SUCCESS); + } + default: + print_fatal ("error in options"); + exit (EXIT_FAILURE); + } + } + + print_status (PROGRAM_NAME " " PROGRAM_VERSION " starting"); + setup_signal_handlers (); + + SSL_library_init (); + atexit (EVP_cleanup); + SSL_load_error_strings (); + // XXX: ERR_load_BIO_strings()? Anything else? + atexit (ERR_free_strings); + + struct server_context ctx; + server_context_init (&ctx); + + if (!read_config_file (&ctx.config, &e)) + { + print_fatal ("error loading configuration: %s", e->message); + error_free (e); + exit (EXIT_FAILURE); + } + + poller_set (&ctx.poller, g_signal_pipe[0], POLLIN, + (poller_dispatcher_func) on_signal_pipe_readable, &ctx); + + if (!irc_listen (&ctx, &e)) + { + print_error ("%s", e->message); + error_free (e); + exit (EXIT_FAILURE); + } + + // TODO: daemonize + + ctx.polling = true; + while (ctx.polling) + poller_run (&ctx.poller); + + server_context_free (&ctx); + return EXIT_SUCCESS; +} |