WIP: add a stub IRC daemon named `kike'
As it is going to share a great lot of functionality with ZyklonB, I have decided to extract the common parts into `common.c' and make the two subprojects include this file. The Single Compile Unit concept has proven valuable (sub-second compile times, dead code warnings, almost no need for function declarations, whole-program optimizations), and the sources aren't that long so far anyway. I am probably going to add CMake support later but so far it's not a priority. This project is meant to be portable and freely relocatable (ie. no hardcoded paths if I can avoid it).
This commit is contained in:
		
							parent
							
								
									165a19da21
								
							
						
					
					
						commit
						705c0e943e
					
				
							
								
								
									
										11
									
								
								Makefile
									
									
									
									
									
								
							
							
						
						
									
										11
									
								
								Makefile
									
									
									
									
									
								
							| @ -1,17 +1,20 @@ | |||||||
| SHELL = /bin/sh | SHELL = /bin/sh | ||||||
| CC = clang | CC = clang | ||||||
| CFLAGS = -ggdb -Wall -Wextra -std=c99 | CFLAGS = -ggdb -Wall -Wextra -std=c99 | ||||||
| LDFLAGS = `pkg-config --libs libssl` | # -lpthread is only there for debugging (gdb & errno)
 | ||||||
|  | LDFLAGS = `pkg-config --libs libssl` -lpthread | ||||||
| 
 | 
 | ||||||
| .PHONY: all clean | .PHONY: all clean | ||||||
| 
 | 
 | ||||||
| targets = zyklonb | targets = zyklonb kike | ||||||
| 
 | 
 | ||||||
| all: $(targets) | all: $(targets) | ||||||
| 
 | 
 | ||||||
| clean: | clean: | ||||||
| 	rm -f $(targets) | 	rm -f $(targets) | ||||||
| 
 | 
 | ||||||
| zyklonb: src/zyklonb.c src/siphash.c | zyklonb: src/zyklonb.c src/common.c src/siphash.c | ||||||
| 	$(CC) $^ -o $@ $(CFLAGS) $(LDFLAGS) | 	$(CC) src/zyklonb.c src/siphash.c -o $@ $(CFLAGS) $(LDFLAGS) | ||||||
|  | kike: src/kike.c src/common.c src/siphash.c | ||||||
|  | 	$(CC) src/kike.c src/siphash.c -o $@ $(CFLAGS) $(LDFLAGS) | ||||||
| 
 | 
 | ||||||
|  | |||||||
							
								
								
									
										11
									
								
								README
									
									
									
									
									
								
							
							
						
						
									
										11
									
								
								README
									
									
									
									
									
								
							| @ -10,6 +10,17 @@ While originally intended to be a simple C99 rewrite of the original bot, which | |||||||
| was written in the GNU dialect of AWK, it fairly quickly became a playground | was written in the GNU dialect of AWK, it fairly quickly became a playground | ||||||
| where I added everything that seemed nice. | where I added everything that seemed nice. | ||||||
| 
 | 
 | ||||||
|  | kike | ||||||
|  | ---- | ||||||
|  | Also included is a simple IRC daemon that mostly follows the RFC's but is | ||||||
|  | limited to single-server networks, due to the protocol being incredibly ugly | ||||||
|  | and tricky to implement correctly.  Even so, it took me a ridiculous amount of | ||||||
|  | time to write.  (But it was a valuable exercise and I can reuse the code.) | ||||||
|  | 
 | ||||||
|  | Disclaimer | ||||||
|  | ---------- | ||||||
|  | I am not an antisemitist, I'm just being an offensive asshole with the naming. | ||||||
|  | 
 | ||||||
| License | License | ||||||
| ------- | ------- | ||||||
| `ZyklonB' is written by Přemysl Janouch <p.janouch@gmail.com>. | `ZyklonB' is written by Přemysl Janouch <p.janouch@gmail.com>. | ||||||
|  | |||||||
							
								
								
									
										1772
									
								
								src/common.c
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1772
									
								
								src/common.c
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										796
									
								
								src/kike.c
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										796
									
								
								src/kike.c
									
									
									
									
									
										Normal file
									
								
							| @ -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; | ||||||
|  | } | ||||||
							
								
								
									
										1661
									
								
								src/zyklonb.c
									
									
									
									
									
								
							
							
						
						
									
										1661
									
								
								src/zyklonb.c
									
									
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user