~rcr/rirc

fd394e9261925fedce40eccc00c482463c9b4248 — Richard Robbins 1 year, 8 months ago b299054
add ssl support
M .travis.yml => .travis.yml +0 -3
@@ 39,9 39,6 @@ before_install:

script:
  - set -e
  - gperf --version
  - cc --version
  - ld --version
  - make -e clean test rirc debug
  - test "${TRAVIS_JOB_NUMBER##*.}" = "1" || exit 0
  - build-wrapper-linux-x86-64 --out-dir bw-output make clean debug test

M CHANGELOG => CHANGELOG +7 -0
@@ 4,6 4,13 @@ Summary of notable changes and features
## Unreleased (dev)
### Features
 - add command :disconnect
 - add SSL support
    - add mbedtls git submodule
    - add CA_CERT_PATH define to config.h
    - changed default port to 6697 for SSL
 - changed standard versions
    - c99 -> c17,
    - POSIX.1-2001 -> POSIX.1-2008
### Fixes

## [0.1.2]

M Makefile => Makefile +32 -23
@@ 3,24 3,33 @@
VERSION := 0.1.2

# Release and debug build executable names
EXE_R := rirc
EXE_D := rirc.debug
BIN_R := rirc
BIN_D := rirc.debug

# Install paths
EXE_DIR = /usr/local/bin
BIN_DIR = /usr/local/bin
MAN_DIR = /usr/local/share/man/man1

STDS := \
 -std=c99 \
 -D_POSIX_C_SOURCE=200112L \
 -D_DARWIN_C_SOURCE=200112L \
 -D_BSD_VISIBLE=1
 -std=c17 \
 -D_BSD_VISIBLE \
 -D_DARWIN_C_SOURCE \
 -D_POSIX_C_SOURCE=200809L

TLS_INCLUDE := \
 -I./mbedtls/include \
 -I./mbedtls/crypto/include

TLS_LIBS := \
 ./mbedtls/library/libmbedtls.a \
 ./mbedtls/library/libmbedx509.a \
 ./mbedtls/crypto/library/libmbedcrypto.a

CC := cc
PP := cc -E
CFLAGS   := $(CC_EXT) -I. $(STDS) -DVERSION=\"$(VERSION)\" -Wall -Wextra -pedantic -O2 -flto
CFLAGS_D := $(CC_EXT) -I. $(STDS) -DVERSION=\"$(VERSION)\" -Wall -Wextra -pedantic -O0 -g -DDEBUG
LDFLAGS  := $(LD_EXT) -pthread
CFLAGS   := $(CC_EXT) -I. $(TLS_INCLUDE) $(STDS) -DVERSION=\"$(VERSION)\" -Wall -Wextra -pedantic -O2 -flto
CFLAGS_D := $(CC_EXT) -I. $(TLS_INCLUDE) $(STDS) -DVERSION=\"$(VERSION)\" -Wall -Wextra -pedantic -O0 -g -DDEBUG
LDFLAGS  := $(LD_EXT) -lpthread

# Build, source, test source directories
DIR_B := bld


@@ 43,14 52,14 @@ OBJS_T += $(DIR_B)/utils/tree.t # Header only file
OBJS_G := $(patsubst %.gperf, %.gperf.out, $(SRC_G))

# Release build executable
$(EXE_R): $(DIR_B) $(OBJS_G) $(OBJS_R)
$(BIN_R): $(DIR_B) $(OBJS_G) $(OBJS_R)
	@echo cc $@
	@$(CC) $(LDFLAGS) -o $@ $(OBJS_R)
	@$(CC) $(LDFLAGS) -o $@ $(OBJS_R) $(TLS_LIBS)

# Debug build executable
$(EXE_D): $(DIR_B) $(OBJS_G) $(OBJS_D)
$(BIN_D): $(DIR_B) $(OBJS_G) $(OBJS_D)
	@echo cc $@
	@$(CC) $(LDFLAGS) -o $@ $(OBJS_D)
	@$(CC) $(LDFLAGS) -o $@ $(OBJS_D) $(TLS_LIBS)

# Release build objects
$(DIR_B)/%.o: $(DIR_S)/%.c


@@ 79,24 88,24 @@ $(DIR_B):
	@for dir in $(patsubst $(DIR_S)/%, %, $(SUBDIRS)); do mkdir -p $(DIR_B)/$$dir; done

clean:
	rm -rf $(DIR_B) $(EXE_R) $(EXE_D)
	rm -rf $(DIR_B) $(BIN_R) $(BIN_D)
	find . -name "*gperf.out" -print0 | xargs -0 -I % rm %

install: $(EXE_R)
	@echo installing executable to $(EXE_DIR)
install: $(BIN_R)
	@echo installing executable to $(BIN_DIR)
	@echo installing manual page to $(MAN_DIR)
	@mkdir -p $(EXE_DIR)
	@mkdir -p $(BIN_DIR)
	@mkdir -p $(MAN_DIR)
	@cp -f rirc $(EXE_DIR)
	@chmod 755 $(EXE_DIR)/rirc
	@cp -f rirc $(BIN_DIR)
	@chmod 755 $(BIN_DIR)/rirc
	@sed "s/VERSION/$(VERSION)/g" < rirc.1 > $(MAN_DIR)/rirc.1

uninstall:
	rm -f $(EXE_DIR)/rirc
	rm -f $(BIN_DIR)/rirc
	rm -f $(MAN_DIR)/rirc.1

all:   $(EXE_R)
debug: $(EXE_D)
all:   $(BIN_R)
debug: $(BIN_D)
test:  $(DIR_B) $(OBJS_G) $(OBJS_T)

-include $(OBJS_R:.o=.d)

M README.md => README.md +12 -6
@@ 27,19 27,25 @@
# rirc
A minimalistic irc client written in C.

While still under development, it currently supports many
features which you would expect from a basic irc client.
rirc supports only TLS connections, the default port is 6697

## Configuring:

Configure rirc by editing `config.h`

## Building:
rirc requires the latest version of GNU gperf to compile.
    See: https://www.gnu.org/software/gperf/

See: https://www.gnu.org/software/gperf/
Initialize and build submodules:

    make
    git submodule init
    git submodule update --recursive
    make -C mbedtls

Or
Build rirc:

    make debug
    make

## Installing:
Default install path:

M config.h => config.h +2 -0
@@ 85,6 85,8 @@

/* [NETWORK] */

#define CA_CERT_PATH "/etc/ssl/"

/* Seconds before displaying ping
 *   Integer, [0, 150, 86400]
 *   (0: no ping handling) */

M rirc.1 => rirc.1 +5 -5
@@ 23,6 23,8 @@ rirc \- a minimalistic irc client written in C
.P
rirc is a lightweight Internet Relay Chat client.
.P
rirc only supports TLS connections, the default port is 6697
.P
Customization of rirc can be accomplished by editing the
.I config.h
file and (re)compiling the source code.


@@ 112,6 114,7 @@ Commands:
  :clear;
  :close;
  :connect;[host [port] [pass] [user] [real]]
  :disconnect
  :quit;
.TE



@@ 144,10 147,7 @@ See the
file in the source directory for the terms of redistribution.
.SH SEE ALSO
See
.UR http://rcr.io/rirc/
.UR
http://rcr.io/rirc/
.UE
for additional documentation.
.SH BUGS
See the
.I TODO
file in the source directory for current known issues.

M src/components/server.h => src/components/server.h +9 -0
@@ 6,6 6,9 @@
#include "src/components/mode.h"
#include "src/utils/utils.h"

// TODO: move this to utils
#define IRC_MESSAGE_LEN 510

struct server
{
	const char *host;


@@ 33,6 36,12 @@ struct server
	unsigned ping;
	unsigned quitting : 1;
	void *connection;
	// TODO: move this to utils
	struct {
		size_t i;
		char cl;
		char buf[IRC_MESSAGE_LEN + 1]; /* callback message buffer */
	} read;
};

struct server_list

M src/io.c => src/io.c +477 -419
@@ 1,6 1,4 @@
#include <ctype.h>
#include <errno.h>
#include <fcntl.h>
#include <netdb.h>
#include <pthread.h>
#include <signal.h>


@@ 9,18 7,21 @@
#include <stdlib.h>
#include <string.h>
#include <sys/ioctl.h>
#include <sys/socket.h>
#include <sys/time.h>
#include <sys/types.h>
#include <termios.h>
#include <unistd.h>

#include "mbedtls/ctr_drbg.h"
#include "mbedtls/entropy.h"
#include "mbedtls/net_sockets.h"
#include "mbedtls/ssl.h"
#include "mbedtls/x509.h"

#include "config.h"
#include "rirc.h"
#include "src/io.h"
#include "src/io_net.h"
#include "utils/utils.h"

#define IO_RECV_SIZE 4096

/* RFC 2812, section 2.3 */
#ifndef IO_MESG_LEN
#define IO_MESG_LEN 510


@@ 62,21 63,31 @@
#error "IO_RECONNECT_BACKOFF_MAX: [0, 86400]"
#endif

#if EAGAIN == EWOULDBLOCK
#define CHECK_BLOCK(X) ((X) == EAGAIN)
#else
#define CHECK_BLOCK(X) ((X) == EAGAIN || (X) == EWOULDBLOCK)
#endif

#define PT_CF(X) do { io_check_fatal((#X), (X)); } while (0)
#define PT_CF(X) \
	do {                           \
		int _ptcf = (X);           \
		if (_ptcf < 0) {           \
			io_fatal((#X), _ptcf); \
		}                          \
	} while (0)
#define PT_LK(X) PT_CF(pthread_mutex_lock((X)))
#define PT_UL(X) PT_CF(pthread_mutex_unlock((X)))
#define PT_CB(...) \
	do { PT_LK(&cb_mutex); \
	     io_cb(__VA_ARGS__); \
	     PT_UL(&cb_mutex); \
	do {                    \
		PT_LK(&cb_mutex);   \
		io_cb(__VA_ARGS__); \
		PT_UL(&cb_mutex);   \
	} while (0)

#define io_cb_cxed(C)        PT_CB(IO_CB_CXED, (C)->obj)
#define io_cb_dxed(C)        PT_CB(IO_CB_DXED, (C)->obj)
#define io_cb_err(C, ...)    PT_CB(IO_CB_ERR, (C)->obj, __VA_ARGS__)
#define io_cb_info(C, ...)   PT_CB(IO_CB_INFO, (C)->obj, __VA_ARGS__)
#define io_cb_ping_0(C, ...) PT_CB(IO_CB_PING_0, (C)->obj, __VA_ARGS__)
#define io_cb_ping_1(C, ...) PT_CB(IO_CB_PING_1, (C)->obj, __VA_ARGS__)
#define io_cb_ping_n(C, ...) PT_CB(IO_CB_PING_N, (C)->obj, __VA_ARGS__)
#define io_cb_signal(S)      PT_CB(IO_CB_SIGNAL, NULL, (S))

enum io_err_t
{
	IO_ERR_NONE,


@@ 84,17 95,11 @@ enum io_err_t
	IO_ERR_CXNG,
	IO_ERR_DXED,
	IO_ERR_FMT,
	IO_ERR_SEND,
	IO_ERR_SSL_WRITE,
	IO_ERR_THREAD,
	IO_ERR_TRUNC,
};

struct io_lock
{
	pthread_cond_t cnd;
	pthread_mutex_t mtx;
	volatile int predicate;
};

struct connection
{
	const void *obj;


@@ 107,135 112,139 @@ struct connection
		IO_ST_CXNG, /* Socket connection in progress */
		IO_ST_CXED, /* Socket connected */
		IO_ST_PING, /* Socket connected, network state in question */
	} st_c, /* current thread state */
	  st_f; /* forced thread state */
	char ip[INET6_ADDRSTRLEN];
	int soc;
	struct {
		size_t i;
		char cl;
		char buf[IO_MESG_LEN + 1]; /* callback message buffer */
		char tmp[IO_RECV_SIZE];    /* socket recv buffer */
	} read;
	struct io_lock lock;
	unsigned rx_backoff;
	pthread_t pt_tid;
	} st_cur, /* current thread state */
	  st_new; /* new thread state */
	mbedtls_net_context ssl_fd;
	mbedtls_ssl_config ssl_conf;
	mbedtls_ssl_context ssl_ctx;
	pthread_mutex_t mtx;
	pthread_t tid;
	unsigned rx_sleep;
};

static const char* io_strerror(struct connection*, int);
static enum io_state_t io_state_cxed(struct connection*);
static enum io_state_t io_state_cxng(struct connection*);
static enum io_state_t io_state_dxed(struct connection*);
static enum io_state_t io_state_ping(struct connection*);
static enum io_state_t io_state_rxng(struct connection*);
static void io_check_fatal(const char*, int);
static void io_lock_wait(struct io_lock*, struct timespec*);
static void io_net_set_timeout(struct connection*, unsigned);
static void io_recv(struct connection*, const char*, size_t);
static int io_cx_read(struct connection*);
static void io_fatal(const char*, int);
static void io_sig_handle(int);
static void io_sig_init(void);
static void io_soc_close(int*);
static void io_soc_shutdown(int);
static void io_state_force(struct connection*, enum io_state_t);
static void io_ssl_init(void);
static void io_ssl_term(void);
static void io_tty_init(void);
static void io_tty_term(void);
static void io_tty_winsize(void);
static void* io_thread(void*);
static unsigned io_cols;
static unsigned io_rows;

static int io_running;
static mbedtls_ctr_drbg_context ssl_ctr_drbg;
static mbedtls_entropy_context ssl_entropy;
static mbedtls_ssl_config ssl_conf;
static mbedtls_x509_crt ssl_cacert;
static pthread_mutex_t cb_mutex = PTHREAD_MUTEX_INITIALIZER;
static struct termios term;
static unsigned io_cols;
static unsigned io_rows;
static volatile sig_atomic_t flag_sigwinch_cb; /* sigwinch callback */
static volatile sig_atomic_t flag_tty_resized; /* sigwinch ws resize */

static void
io_check_fatal(const char *f, int ret)
struct connection*
connection(const void *obj, const char *host, const char *port)
{
	if (ret < 0)
		fatal("%s: %s", f, strerror(ret));
}
	struct connection *cx;

static const char*
io_strerror(struct connection *c, int errnum)
{
	PT_CF(strerror_r(errnum, c->read.tmp, sizeof(c->read.tmp)));
	return c->read.tmp;
}
	if ((cx = calloc(1U, sizeof(*cx))) == NULL)
		fatal("malloc: %s", strerror(errno));

static void
io_soc_close(int *soc)
{
	if (*soc >= 0 && close(*soc) < 0) {
		fatal("close: %s", strerror(errno));
	}
	*soc = -1;
	cx->obj = obj;
	cx->host = strdup(host);
	cx->port = strdup(port);
	cx->st_cur = IO_ST_DXED;
	cx->st_new = IO_ST_INVALID;
	PT_CF(pthread_mutex_init(&(cx->mtx), NULL));

	return cx;
}

static void
io_soc_shutdown(int soc)
void
connection_free(struct connection *cx)
{
	if (soc >= 0 && shutdown(soc, SHUT_RDWR) < 0 && errno != ENOTCONN) {
		fatal("shutdown: %s", strerror(errno));
	}
	PT_CF(pthread_mutex_destroy(&(cx->mtx)));
	free((void*)cx->host);
	free((void*)cx->port);
	free(cx);
}

static void
io_lock_wait(struct io_lock *lock, struct timespec *timeout)
int
io_cx(struct connection *cx)
{
	PT_LK(&(lock->mtx));
	enum io_err_t err = IO_ERR_NONE;
	enum io_state_t st;
	sigset_t sigset;
	sigset_t sigset_old;

	int ret = 0;
	PT_LK(&(cx->mtx));

	while (lock->predicate == 0 && ret == 0) {
		if (timeout) {
			ret = pthread_cond_timedwait(&(lock->cnd), &(lock->mtx), timeout);
		} else {
			ret = pthread_cond_wait(&(lock->cnd), &(lock->mtx));
		}
	switch ((st = cx->st_cur)) {
		case IO_ST_DXED:
			PT_CF(sigfillset(&sigset));
			PT_CF(pthread_sigmask(SIG_BLOCK, &sigset, &sigset_old));
			if (pthread_create(&(cx->tid), NULL, io_thread, cx) < 0)
				err = IO_ERR_THREAD;
			PT_CF(pthread_sigmask(SIG_SETMASK, &sigset_old, NULL));
			break;
		case IO_ST_CXNG:
			err = IO_ERR_CXNG;
			break;
		case IO_ST_CXED:
		case IO_ST_PING:
			err = IO_ERR_CXED;
			break;
		case IO_ST_RXNG:
			PT_CF(pthread_kill(cx->tid, SIGUSR1));
			break;
		default:
			fatal("unknown state");
	}

	if (ret && (timeout == NULL || ret != ETIMEDOUT))
		fatal("io_lock_wait: %s", strerror(ret));

	lock->predicate = 0;
	PT_UL(&(cx->mtx));

	PT_UL(&(lock->mtx));
	return err;
}

struct connection*
connection(const void *obj, const char *host, const char *port)
int
io_dx(struct connection *cx)
{
	struct connection *c;
	enum io_err_t err = IO_ERR_NONE;

	if ((c = calloc(1U, sizeof(*c))) == NULL)
		fatal("malloc: %s", strerror(errno));
	if (cx->st_cur == IO_ST_DXED)
		return IO_ERR_DXED;

	PT_LK(&(cx->mtx));
	cx->st_new = IO_ST_DXED;
	PT_UL(&(cx->mtx));

	c->obj = obj;
	c->host = strdup(host);
	c->port = strdup(port);
	c->st_c = IO_ST_DXED;
	c->st_f = IO_ST_INVALID;
	PT_CF(pthread_cond_init(&(c->lock.cnd), NULL));
	PT_CF(pthread_mutex_init(&(c->lock.mtx), NULL));
	PT_CF(pthread_create(&c->pt_tid, NULL, io_thread, c));
	PT_CF(pthread_detach(cx->tid));
	PT_CF(pthread_kill(cx->tid, SIGUSR1));

	return c;
	return err;
}

int
io_sendf(struct connection *c, const char *fmt, ...)
io_sendf(struct connection *cx, const char *fmt, ...)
{
	char sendbuf[IO_MESG_LEN + 2];
	unsigned char sendbuf[IO_MESG_LEN + 2];
	int ret;
	size_t len;
	size_t written;
	va_list ap;

	if (c->st_c != IO_ST_CXED && c->st_c != IO_ST_PING)
	if (cx->st_cur != IO_ST_CXED && cx->st_cur != IO_ST_PING)
		return IO_ERR_DXED;

	va_start(ap, fmt);
	ret = vsnprintf(sendbuf, sizeof(sendbuf) - 2, fmt, ap);
	ret = vsnprintf((char*)sendbuf, sizeof(sendbuf) - 2, fmt, ap);
	va_end(ap);

	if (ret <= 0)


@@ 251,370 260,503 @@ io_sendf(struct connection *c, const char *fmt, ...)
	sendbuf[len++] = '\r';
	sendbuf[len++] = '\n';

	if (send(c->soc, sendbuf, len, 0) < 0)
		return IO_ERR_SEND;
	ret = 0;
	written = 0;

	do {
		if ((ret = mbedtls_ssl_write(&(cx->ssl_ctx), sendbuf + ret, len - ret)) < 0) {
			switch (ret) {
				case MBEDTLS_ERR_SSL_WANT_READ:
				case MBEDTLS_ERR_SSL_WANT_WRITE:
					ret = 0;
					continue;
				default:
					io_dx(cx);
					io_cx(cx);
					return IO_ERR_SSL_WRITE;
			}
		}
	} while ((written += ret) < len);

	return IO_ERR_NONE;
}

int
io_cx(struct connection *c)
void
io_init(void)
{
	/* Force a socket thread into IO_ST_CXNG state */

	enum io_err_t err = IO_ERR_NONE;

	PT_LK(&(c->lock.mtx));

	switch (c->st_c) {
		case IO_ST_CXNG: err = IO_ERR_CXNG; break;
		case IO_ST_CXED: err = IO_ERR_CXED; break;
		case IO_ST_PING: err = IO_ERR_CXED; break;
		default:
			io_state_force(c, IO_ST_CXNG);
	}

	PT_UL(&(c->lock.mtx));

	return err;
	io_sig_init();
	io_tty_init();
	io_ssl_init();
}

int
io_dx(struct connection *c)
void
io_start(void)
{
	/* Force a socket thread into IO_ST_DXED state */
	io_running = 1;

	enum io_err_t err = IO_ERR_NONE;
	while (io_running) {

	PT_LK(&(c->lock.mtx));
		char buf[128];
		ssize_t ret = read(STDIN_FILENO, buf, sizeof(buf));

	switch (c->st_c) {
		case IO_ST_DXED: err = IO_ERR_DXED; break;
		default:
			io_state_force(c, IO_ST_DXED);
		if (ret > 0) {
			PT_LK(&cb_mutex);
			io_cb_read_inp(buf, ret);
			PT_UL(&cb_mutex);
		} else {
			if (errno == EINTR) {
				if (flag_sigwinch_cb) {
					flag_sigwinch_cb = 0;
					io_cb_signal(IO_SIGWINCH);
				}
			} else {
				fatal("read: %s", ret ? strerror(errno) : "EOF");
			}
		}
	}

	PT_UL(&(c->lock.mtx));

	return err;
}

void
io_free(struct connection *c)
io_stop(void)
{
	pthread_t pt_tid = c->pt_tid;

	PT_CF(pthread_cancel(pt_tid));
	PT_CF(pthread_join(pt_tid, NULL));
	PT_CF(pthread_cond_destroy(&(c->lock.cnd)));
	PT_CF(pthread_mutex_destroy(&(c->lock.mtx)));
	io_soc_close(&(c->soc));
	free((void*)c->host);
	free((void*)c->port);
	free(c);
	io_running = 0;
}

static void
io_state_force(struct connection *c, enum io_state_t st_f)
io_tty_winsize(void)
{
	/* Wake and force a connection thread's state */
	static struct winsize tty_ws;

	c->st_f = st_f;
	if (flag_tty_resized == 0) {
		flag_tty_resized = 1;

	switch (c->st_c) {
		case IO_ST_DXED: /* io_lock_wait() */
		case IO_ST_RXNG: /* io_lock_wait() */
			c->lock.predicate = 1;
			PT_CF(pthread_cond_signal(&(c->lock.cnd)));
			break;
		case IO_ST_CXNG: /* connect() */
		case IO_ST_CXED: /* recv() */
		case IO_ST_PING: /* recv() */
			io_soc_shutdown(c->soc);
			break;
		default:
			fatal("Unknown net state: %d", c->st_c);
		if (ioctl(0, TIOCGWINSZ, &tty_ws) < 0)
			fatal("ioctl: %s", strerror(errno));

		io_rows = tty_ws.ws_row;
		io_cols = tty_ws.ws_col;
	}
}

static enum io_state_t
io_state_dxed(struct connection *c)
unsigned
io_tty_cols(void)
{
	io_lock_wait(&c->lock, NULL);
	io_tty_winsize();
	return io_cols;
}

	return IO_ST_CXNG;
unsigned
io_tty_rows(void)
{
	io_tty_winsize();
	return io_rows;
}

static enum io_state_t
io_state_rxng(struct connection *c)
const char*
io_err(int err)
{
	struct timespec ts;
	switch (err) {
		case IO_ERR_NONE:      return "success";
		case IO_ERR_CXED:      return "socket connected";
		case IO_ERR_CXNG:      return "socket connection in progress";
		case IO_ERR_DXED:      return "socket not connected";
		case IO_ERR_FMT:       return "failed to format message";
		case IO_ERR_THREAD:    return "failed to create thread";
		case IO_ERR_SSL_WRITE: return "ssl write failure";
		case IO_ERR_TRUNC:     return "data truncated";
		default:
			return "unknown error";
	}
}

	if (c->rx_backoff == 0) {
		c->rx_backoff = IO_RECONNECT_BACKOFF_BASE;
static enum io_state_t
io_state_rxng(struct connection *cx)
{
	if (cx->rx_sleep == 0) {
		cx->rx_sleep = IO_RECONNECT_BACKOFF_BASE;
	} else {
		c->rx_backoff = MIN(
			IO_RECONNECT_BACKOFF_FACTOR * c->rx_backoff,
		cx->rx_sleep = MIN(
			IO_RECONNECT_BACKOFF_FACTOR * cx->rx_sleep,
			IO_RECONNECT_BACKOFF_MAX
		);
	}

	PT_CB(IO_CB_INFO, c->obj, "Attemping reconnect in %02u:%02u",
		(c->rx_backoff / 60),
		(c->rx_backoff % 60));

	if (clock_gettime(CLOCK_REALTIME, &ts) < 0)
		fatal("clock_gettime: %s", strerror(errno));
	io_cb_info(cx, "Attemping reconnect in %02u:%02u",
		(cx->rx_sleep / 60),
		(cx->rx_sleep % 60));

	ts.tv_sec += c->rx_backoff;

	io_lock_wait(&c->lock, &ts);
	sleep(cx->rx_sleep);

	return IO_ST_CXNG;
}

static enum io_state_t
io_state_cxng(struct connection *c)
io_state_cxng(struct connection *cx)
{
	/* TODO: handle shutdown() on socket at all points, will fatal for now */
	/* TODO: mutex should protect access to c->soc, else race condition
	 *       when the main thread tries to shutdown() for cancel */
	/* TODO: how to cancel getaddrinfo/getnameinfo? */
	/* FIXME: addrinfo leak if canceled during connection */

	int ret, soc = -1;
	char addr_buf[INET6_ADDRSTRLEN];
	char vrfy_buf[512];
	enum io_state_t st = IO_ST_RXNG;
	int ret;
	int soc;
	uint32_t cert_ret;

	io_cb_info(cx, "Connecting to %s:%s", cx->host, cx->port);

	if ((ret = io_net_connect(&soc, cx->host, cx->port)) != IO_NET_ERR_NONE) {
		switch (ret) {
			case IO_NET_ERR_EINTR:
				st = IO_ST_DXED;
				goto error_net;
			case IO_NET_ERR_SOCKET_FAILED:
				io_cb_err(cx, " ... Failed to obtain socket");
				goto error_net;
			case IO_NET_ERR_UNKNOWN_HOST:
				io_cb_err(cx, " ... Failed to resolve host");
				goto error_net;
			case IO_NET_ERR_CONNECT_FAILED:
				io_cb_err(cx, " ... Failed to connect to host");
				goto error_net;
			default:
				fatal("unknown net error");
		}
	}

	struct addrinfo *p, *res, hints = {
		.ai_family   = AF_UNSPEC,
		.ai_flags    = AI_PASSIVE,
		.ai_protocol = IPPROTO_TCP,
		.ai_socktype = SOCK_STREAM
	};
	if ((ret = io_net_ip_str(soc, addr_buf, sizeof(addr_buf))) != IO_NET_ERR_NONE) {
		if (ret == IO_NET_ERR_EINTR) {
			st = IO_ST_DXED;
			goto error_net;
		}
		io_cb_info(cx, " ... Connected (failed to optain IP address)");
	} else {
		io_cb_info(cx, " ... Connected to [%s]", addr_buf);
	}

	PT_CB(IO_CB_INFO, c->obj, "Connecting to %s:%s ...", c->host, c->port);
	io_cb_info(cx, " ... Establishing SSL");

	if ((ret = getaddrinfo(c->host, c->port, &hints, &res))) {
	mbedtls_net_init(&(cx->ssl_fd));
	mbedtls_ssl_init(&(cx->ssl_ctx));
	mbedtls_ssl_config_init(&(cx->ssl_conf));

		if (ret == EAI_SYSTEM)
			PT_CB(IO_CB_ERR, c->obj, "Error resolving host: %s", io_strerror(c, errno));
		else
			PT_CB(IO_CB_ERR, c->obj, "Error resolving host: %s", gai_strerror(ret));
	cx->ssl_conf = ssl_conf;
	cx->ssl_fd.fd = soc;

		return IO_ST_RXNG;
	if ((ret = mbedtls_net_set_block(&(cx->ssl_fd))) != 0) {
		io_cb_err(cx, " ... mbedtls_net_set_block failure");
		goto error_ssl;
	}

	for (p = res; p != NULL; p = p->ai_next) {

		if ((soc = socket(p->ai_family, p->ai_socktype, p->ai_protocol)) < 0)
			continue;
	if ((ret = mbedtls_ssl_setup(&(cx->ssl_ctx), &(cx->ssl_conf))) != 0) {
		io_cb_err(cx, " ... mbedtls_ssl_setup failure");
		goto error_ssl;
	}

		if (connect(soc, p->ai_addr, p->ai_addrlen) == 0)
			break;
	if ((ret = mbedtls_ssl_set_hostname(&(cx->ssl_ctx), cx->host)) != 0) {
		io_cb_err(cx, " ... mbedtls_ssl_set_hostname failure");
		goto error_ssl;
	}

		io_soc_close(&soc);
	mbedtls_ssl_set_bio(
		&(cx->ssl_ctx),
		&(cx->ssl_fd),
		mbedtls_net_send,
		NULL,
		mbedtls_net_recv_timeout);

	while ((ret = mbedtls_ssl_handshake(&(cx->ssl_ctx))) != 0) {
		if (ret != MBEDTLS_ERR_SSL_WANT_READ && ret != MBEDTLS_ERR_SSL_WANT_WRITE) {
			io_cb_err(cx, " ... mbedtls_ssl_handshake failure");
			goto error_ssl;
		}
	}

	if (p == NULL) {
		PT_CB(IO_CB_ERR, c->obj, "Error connecting: %s", io_strerror(c, errno));
		freeaddrinfo(res);
		return IO_ST_RXNG;
	if ((cert_ret = mbedtls_ssl_get_verify_result(&(cx->ssl_ctx))) != 0) {
		if (mbedtls_x509_crt_verify_info(vrfy_buf, sizeof(vrfy_buf), "", cert_ret) <= 0) {
			io_cb_err(cx, " ... failed to verify cert: unknown failure");
			goto error_ssl;
		} else {
			io_cb_err(cx, " ... failed to verify cert: %s", vrfy_buf);
			goto error_ssl;
		}
	}

	c->soc = soc;
	io_cb_info(cx, " ... SSL connection established");
	io_cb_info(cx, " ...   - version:     %s", mbedtls_ssl_get_version(&(cx->ssl_ctx)));
	io_cb_info(cx, " ...   - ciphersuite: %s", mbedtls_ssl_get_ciphersuite(&(cx->ssl_ctx)));

	if ((ret = getnameinfo(p->ai_addr, p->ai_addrlen, c->ip, sizeof(c->ip), NULL, 0, NI_NUMERICHOST))) {
	return IO_ST_CXED;

		if (ret == EAI_SYSTEM)
			PT_CB(IO_CB_ERR, c->obj, "Error resolving numeric host: %s", io_strerror(c, errno));
		else
			PT_CB(IO_CB_ERR, c->obj, "Error resolving numeric host: %s", gai_strerror(ret));
error_ssl:

		*c->ip = 0;
	}
	mbedtls_net_free(&(cx->ssl_fd));
	mbedtls_ssl_free(&(cx->ssl_ctx));
	mbedtls_ssl_config_free(&(cx->ssl_conf));

	freeaddrinfo(res);
	return IO_ST_CXED;
error_net:

	return st;
}

static enum io_state_t
io_state_cxed(struct connection *c)
io_state_cxed(struct connection *cx)
{
	io_net_set_timeout(c, IO_PING_MIN);
	ssize_t ret;
	int ret;
	enum io_state_t st = IO_ST_RXNG;

	while ((ret = recv(c->soc, c->read.tmp, sizeof(c->read.tmp), 0)) > 0)
		io_recv(c, c->read.tmp, (size_t) ret);
	mbedtls_ssl_conf_read_timeout(&(cx->ssl_conf), SEC_IN_MS(IO_PING_MIN));

	if (CHECK_BLOCK(errno)) {
		return IO_ST_PING;
	}
	while ((ret = io_cx_read(cx)) > 0)
		continue;

	// FIXME:
	// EINTR when coming back from sleep?

	if (ret == 0) {
		PT_CB(IO_CB_DXED, c->obj, "connection closed");
	} else if (errno == EPIPE || errno == ECONNRESET) {
		PT_CB(IO_CB_DXED, c->obj, "connection closed by peer");
	} else {
		PT_CB(IO_CB_DXED, c->obj, "recv error: %s", io_strerror(c, errno));
	switch (ret) {
		case MBEDTLS_ERR_SSL_WANT_READ:
		case MBEDTLS_ERR_SSL_WANT_WRITE:
			/* EINTR */
			break;
		case MBEDTLS_ERR_SSL_PEER_CLOSE_NOTIFY:
			/* Graceful termination */
			break;
		case MBEDTLS_ERR_SSL_TIMEOUT:
			return IO_ST_PING;
		case MBEDTLS_ERR_NET_CONN_RESET:
		case 0:
			io_cb_err(cx, "connection reset by peer");
			break;
		default:
			io_cb_err(cx, "connection ssl error");
			break;
	}

	io_soc_close(&(c->soc));
	mbedtls_net_free(&(cx->ssl_fd));
	mbedtls_ssl_free(&(cx->ssl_ctx));
	mbedtls_ssl_config_free(&(cx->ssl_conf));

	return (ret == 0 ? IO_ST_DXED : IO_ST_CXNG);
	return st;
}

static enum io_state_t
io_state_ping(struct connection *c)
io_state_ping(struct connection *cx)
{
	io_net_set_timeout(c, IO_PING_REFRESH);
	ssize_t ret;
	unsigned ping = IO_PING_MIN;
	int ping = IO_PING_MIN;
	int ret;
	enum io_state_t st = IO_ST_RXNG;

	for (;;) {
	mbedtls_ssl_conf_read_timeout(&(cx->ssl_conf), SEC_IN_MS(IO_PING_REFRESH));

		if ((ret = recv(c->soc, c->read.tmp, sizeof(c->read.tmp), 0)) > 0) {
			io_recv(c, c->read.tmp, (size_t) ret);
			return IO_ST_CXED;
	while ((ret = io_cx_read(cx)) <= 0 && ret == MBEDTLS_ERR_SSL_TIMEOUT) {
		if ((ping += IO_PING_REFRESH) < IO_PING_MAX) {
			io_cb_ping_n(cx, ping);
		}
	}

		if (ret == 0) {
			PT_CB(IO_CB_DXED, c->obj, "connection closed");
		} else if (CHECK_BLOCK(errno)) {
			if ((ping += IO_PING_REFRESH) < IO_PING_MAX) {
				PT_CB(IO_CB_PING_N, c->obj, ping);
				continue;
			}
			PT_CB(IO_CB_DXED, c->obj, "connection timeout (%u)", ping);
		} else {
			PT_CB(IO_CB_DXED, c->obj, "recv error: %s", io_strerror(c, errno));
		}
	if (ret > 0)
		return IO_ST_CXED;

		break;
	switch (ret) {
		case MBEDTLS_ERR_SSL_WANT_READ:         /* io_dx EINTR */
		case MBEDTLS_ERR_SSL_WANT_WRITE:        /* io_dx EINTR */
		case MBEDTLS_ERR_SSL_PEER_CLOSE_NOTIFY: /* Graceful termination */
			st = IO_ST_DXED;
			break;
		case MBEDTLS_ERR_SSL_TIMEOUT:
			io_cb_err(cx, "connection timeout (%u)", ping);
			break;
		case MBEDTLS_ERR_NET_CONN_RESET:
		case 0:
			io_cb_err(cx, "connection reset by peer");
			break;
		default:
			io_cb_err(cx, "connection ssl error");
			break;
	}

	io_soc_close(&(c->soc));
	mbedtls_net_free(&(cx->ssl_fd));
	mbedtls_ssl_free(&(cx->ssl_ctx));
	mbedtls_ssl_config_free(&(cx->ssl_conf));

	return (ret == 0 ? IO_ST_DXED : IO_ST_CXNG);
	return st;
}

static void*
io_thread(void *arg)
{
	struct connection *c = arg;
	struct connection *cx = arg;

	/* SIGUSR1 indicates to a thread that it should return
	 * to the state machine and check for a new state */

	sigset_t sigset;
	PT_CF(sigfillset(&sigset));
	PT_CF(pthread_sigmask(SIG_BLOCK, &sigset, NULL));

	for (;;) {
	PT_CF(sigaddset(&sigset, SIGUSR1));
	PT_CF(pthread_sigmask(SIG_UNBLOCK, &sigset, NULL));

		enum io_state_t st_f, /* transition state from */
		                st_t; /* transition state to */
	cx->st_new = IO_ST_CXNG;

		enum io_state_t (*st_fn)(struct connection*);
	for (;;) {

		enum io_state_t st_from;
		enum io_state_t st_to;

		switch (c->st_c) {
			case IO_ST_DXED: st_fn = io_state_dxed; break;
			case IO_ST_CXNG: st_fn = io_state_cxng; break;
			case IO_ST_RXNG: st_fn = io_state_rxng; break;
			case IO_ST_CXED: st_fn = io_state_cxed; break;
			case IO_ST_PING: st_fn = io_state_ping; break;
		switch (cx->st_cur) {
			case IO_ST_CXED: st_to = io_state_cxed(cx); break;
			case IO_ST_CXNG: st_to = io_state_cxng(cx); break;
			case IO_ST_PING: st_to = io_state_ping(cx); break;
			case IO_ST_RXNG: st_to = io_state_rxng(cx); break;
			case IO_ST_DXED: st_to = IO_ST_INVALID; break;
			default:
				fatal("invalid state: %d", c->st_c);
				fatal("invalid state: %d", cx->st_cur);
		}

		st_f = c->st_c;
		st_t = st_fn(c);
		st_from = cx->st_cur;

		PT_LK(&(c->lock.mtx));
		PT_LK(&(cx->mtx));

		if (c->st_f != IO_ST_INVALID) {
			c->st_c = c->st_f;
			c->st_f = IO_ST_INVALID;
		/* New state set by io_cx/io_dx */
		if (cx->st_new != IO_ST_INVALID) {
			cx->st_cur = st_to = cx->st_new;
			cx->st_new = IO_ST_INVALID;
		} else {
			c->st_c = st_t;
			cx->st_cur = st_to;
		}

		PT_UL(&(cx->mtx));

		if (st_from == IO_ST_CXNG && st_to == IO_ST_RXNG)
			io_cb_err(cx, " ... Connection failed -- retrying");

		if (st_from == IO_ST_CXNG && st_to == IO_ST_CXED) {
			io_cb_info(cx, " ... Connection successful");
			io_cb_cxed(cx);
			cx->rx_sleep = 0;
		}

		PT_UL(&(c->lock.mtx));
		if ((st_from == IO_ST_RXNG || st_from == IO_ST_CXNG) && st_to == IO_ST_DXED)
			io_cb_info(cx, "Connection cancelled");

		if (st_f == IO_ST_PING && st_t == IO_ST_CXED)
			PT_CB(IO_CB_PING_0, c->obj, 0);
		if ((st_from == IO_ST_CXED || st_from == IO_ST_PING) && st_to == IO_ST_DXED)
			io_cb_info(cx, "Connection closed");

		if (st_f == IO_ST_CXED && st_t == IO_ST_PING)
			PT_CB(IO_CB_PING_1, c->obj, IO_PING_MIN);
		if (st_from == IO_ST_PING && st_to == IO_ST_CXED)
			io_cb_ping_0(cx, 0);

		if (st_f == IO_ST_CXNG && st_t == IO_ST_CXED)
			PT_CB(IO_CB_CXED, c->obj, "Connected to %s [%s]", c->host, c->ip);
		if (st_from == IO_ST_CXED && st_to == IO_ST_PING)
			io_cb_ping_1(cx, IO_PING_MIN);

		if (st_t == IO_ST_DXED || (st_f == IO_ST_CXNG && st_t == IO_ST_CXED))
			c->rx_backoff = 0;
		/* Exit the thread */
		if (cx->st_cur == IO_ST_DXED) {
			io_cb_dxed(cx);
			cx->rx_sleep = 0;
			break;
		}
	}

	return NULL;
}

static void
io_recv(struct connection *c, const char *buf, size_t n)
static int
io_cx_read(struct connection *cx)
{
	size_t ci = c->read.i;

	for (size_t i = 0; i < n; i++) {

		char cc = buf[i];

		if (ci && cc == '\n' && ((i && buf[i - 1] == '\r') || (!i && c->read.cl == '\r'))) {

			c->read.buf[ci] = 0;

			debug(" recv: (%zu) %s", ci, c->read.buf);

			PT_LK(&cb_mutex);
			io_cb_read_soc(c->read.buf, ci, c->obj);
			PT_UL(&cb_mutex);
	int ret;
	unsigned char ssl_readbuf[1024];

			ci = 0;
		} else if (ci < IO_MESG_LEN && (isprint(cc) || cc == 0x01)) {
			c->read.buf[ci++] = cc;
		}
	if ((ret = mbedtls_ssl_read(&(cx->ssl_ctx), ssl_readbuf, sizeof(ssl_readbuf))) > 0) {
		PT_LK(&cb_mutex);
		io_cb_read_soc((char *)ssl_readbuf, (size_t)ret,  cx->obj);
		PT_UL(&cb_mutex);
	}

	c->read.cl = buf[n - 1];
	c->read.i = ci;
	return ret;
}

static void
io_net_set_timeout(struct connection *c, unsigned timeout)
io_fatal(const char *f, int errnum)
{
	struct timeval tv = {
		.tv_sec = timeout
	};
	char errbuf[512];

	if (setsockopt(c->soc, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv)) < 0)
		fatal("setsockopt: %s", strerror(errno));
	if (strerror_r(errnum, errbuf, sizeof(errbuf)) == 0) {
		fatal("%s: (%d): %s", f, errnum, errbuf);
	} else {
		fatal("%s: (%d): (failed to get error message)", f, errnum);
	}
}

static void
sigaction_sigwinch(int sig)
io_sig_handle(int sig)
{
	UNUSED(sig);

	flag_sigwinch_cb = 1;
	flag_tty_resized = 0;
	if (sig == SIGWINCH) {
		flag_sigwinch_cb = 1;
		flag_tty_resized = 0;
	}
}

static void
io_sig_init(void)
{
	struct sigaction sa;
	struct sigaction sa = {0};

	sa.sa_handler = sigaction_sigwinch;
	sa.sa_handler = io_sig_handle;
	sa.sa_flags = 0;
	sigemptyset(&sa.sa_mask);

	if (sigaction(SIGWINCH, &sa, NULL) < 0)
		fatal("sigaction - SIGWINCH: %s", strerror(errno));

	if (sigaction(SIGUSR1, &sa, NULL) < 0)
		fatal("sigaction - SIGUSR1: %s", strerror(errno));
}

static void
io_ssl_init(void)
{
	const char *tls_pers = "rirc-drbg-ctr-pers";
	int ret;

	mbedtls_ssl_config_init(&ssl_conf);
	mbedtls_ctr_drbg_init(&ssl_ctr_drbg);
	mbedtls_entropy_init(&ssl_entropy);
	mbedtls_x509_crt_init(&ssl_cacert);

	if ((ret = mbedtls_x509_crt_parse_path(&ssl_cacert, ca_cert_path)) <= 0) {
		if (ret == 0) {
			fatal("no certs found");
		} else {
			fatal("ssl init failed: mbedtls_x509_crt_parse_path");
		}
	}

	if ((ret = mbedtls_ctr_drbg_seed(
					&ssl_ctr_drbg,
					mbedtls_entropy_func,
					&ssl_entropy,
					(unsigned char *)tls_pers,
					strlen(tls_pers))) != 0) {
		fatal("ssl init failed: mbedtls_ctr_drbg_seed");
	}

	if ((ret = mbedtls_ssl_config_defaults(
					&ssl_conf,
					MBEDTLS_SSL_IS_CLIENT,
					MBEDTLS_SSL_TRANSPORT_STREAM,
					MBEDTLS_SSL_PRESET_DEFAULT)) != 0) {
		fatal("ssl init failed: mbedtls_ssl_config_defaults");
	}

	mbedtls_ssl_conf_ca_chain(&ssl_conf, &ssl_cacert, NULL);
	mbedtls_ssl_conf_read_timeout(&ssl_conf, SEC_IN_MS(IO_PING_MIN));
	mbedtls_ssl_conf_rng(&ssl_conf, mbedtls_ctr_drbg_random, &ssl_ctr_drbg);

	if (atexit(io_ssl_term) != 0)
		fatal("atexit");
}

static void
io_ssl_term(void)
{
	/* Exit handler, must return normally */

	mbedtls_ctr_drbg_free(&ssl_ctr_drbg);
	mbedtls_entropy_free(&ssl_entropy);
	mbedtls_ssl_config_free(&ssl_conf);
	mbedtls_x509_crt_free(&ssl_cacert);
}

static void


@@ 648,87 790,3 @@ io_tty_term(void)
	if (tcsetattr(STDIN_FILENO, TCSADRAIN, &term) < 0)
		fatal_noexit("tcsetattr: %s", strerror(errno));
}

void
io_init(void)
{
	io_sig_init();
	io_tty_init();

	io_running = 1;

	while (io_running) {

		char buf[128];
		ssize_t ret = read(STDIN_FILENO, buf, sizeof(buf));

		if (ret > 0) {
			PT_LK(&cb_mutex);
			io_cb_read_inp(buf, ret);
			PT_UL(&cb_mutex);
		}

		if (ret <= 0) {
			if (errno == EINTR) {
				if (flag_sigwinch_cb) {
					flag_sigwinch_cb = 0;
					PT_CB(IO_CB_SIGNAL, NULL, IO_SIGWINCH);
				}
			} else {
				fatal("read: %s", ret ? strerror(errno) : "EOF");
			}
		}
	}
}

void
io_term(void)
{
	io_running = 0;
}

static void
io_tty_winsize(void)
{
	static struct winsize tty_ws;

	if (flag_tty_resized == 0) {
		flag_tty_resized = 1;

		if (ioctl(0, TIOCGWINSZ, &tty_ws) < 0)
			fatal("ioctl: %s", strerror(errno));

		io_rows = tty_ws.ws_row;
		io_cols = tty_ws.ws_col;
	}
}

unsigned
io_tty_cols(void)
{
	io_tty_winsize();
	return io_cols;
}

unsigned
io_tty_rows(void)
{
	io_tty_winsize();
	return io_rows;
}

const char*
io_err(int err)
{
	switch (err) {
		case IO_ERR_NONE:  return "success";
		case IO_ERR_CXED:  return "socket connected";
		case IO_ERR_CXNG:  return "socket connection in progress";
		case IO_ERR_DXED:  return "socket not connected";
		case IO_ERR_FMT:   return "failed to format message";
		case IO_ERR_SEND:  return "failed to send message";
		case IO_ERR_TRUNC: return "data truncated";
		default:
			return "unknown error";
	}
}

M src/io.h => src/io.h +15 -15
@@ 69,11 69,10 @@
 *   t(n) = t(n - 1) * factor
 *   t(0) = base
 *
 * Calling io_init starts the io context and doesn't return until io_term
 * Calling io_start starts the io context and doesn't return until after
 * a call to io_stop
 */

#define IO_MAX_CONNECTIONS 8

struct connection;

enum io_sig_t


@@ 86,8 85,8 @@ enum io_sig_t
enum io_cb_t
{
	IO_CB_INVALID,
	IO_CB_CXED,   /* <const char *fmt>, [args, ...] */
	IO_CB_DXED,   /* <const char *fmt>, [args, ...] */
	IO_CB_CXED,   /* no args */
	IO_CB_DXED,   /* no args */
	IO_CB_ERR,    /* <const char *fmt>, [args, ...] */
	IO_CB_INFO,   /* <const char *fmt>, [args, ...] */
	IO_CB_PING_0, /* <unsigned ping> */


@@ 103,7 102,7 @@ struct connection* connection(
	const char*,  /* host */
	const char*); /* port */

void io_free(struct connection*);
void connection_free(struct connection*);

/* Explicit direction of net state */
int io_cx(struct connection*);


@@ 112,16 111,10 @@ int io_dx(struct connection*);
/* Formatted write to connection */
int io_sendf(struct connection*, const char*, ...);

/* IO state callback */
void io_cb(enum io_cb_t, const void*, ...);

/* IO data callback */
void io_cb_read_inp(char*, size_t);
void io_cb_read_soc(char*, size_t, const void*);

/* Start/stop IO context */
/* Init/start/stop IO context */
void io_init(void);
void io_term(void);
void io_start(void);
void io_stop(void);

/* Get tty dimensions */
unsigned io_tty_cols(void);


@@ 130,4 123,11 @@ unsigned io_tty_rows(void);
/* IO error string */
const char* io_err(int);

/* IO state callback */
void io_cb(enum io_cb_t, const void*, ...);

/* IO data callback */
void io_cb_read_inp(char*, size_t);
void io_cb_read_soc(char*, size_t, const void*);

#endif

M src/rirc.c => src/rirc.c +16 -6
@@ 10,6 10,8 @@
#include "src/io.h"
#include "src/state.h"

#define MAX_CLI_SERVERS 16

#define arg_error(...) \
	do { fprintf(stderr, "%s: ", runtime_name); \
	     fprintf(stderr, __VA_ARGS__); \


@@ 27,6 29,12 @@ const char *runtime_name = "rirc";
const char *runtime_name = "rirc.debug";
#endif

#ifdef CA_CERT_PATH
const char *ca_cert_path = CA_CERT_PATH;
#else
#error "CA_CERT_PATH required"
#endif

#ifndef DEFAULT_NICK_SET
const char *default_nick_set = DEFAULT_NICK_SET;
#else


@@ 139,7 147,7 @@ parse_args(int argc, char **argv)
		const char *username;
		const char *realname;
		struct server *s;
	} cli_servers[IO_MAX_CONNECTIONS];
	} cli_servers[MAX_CLI_SERVERS];

	/* FIXME: getopt_long is a GNU extension */
	while (0 < (opt_c = getopt_long(argc, argv, ":s:p:w:n:c:r:u:hv", long_opts, &opt_i))) {


@@ 151,11 159,11 @@ parse_args(int argc, char **argv)
				if (*optarg == '-')
					arg_error("-s/--server requires an argument");

				if (++n_servers == IO_MAX_CONNECTIONS)
					arg_error("exceeded maximum number of servers (%d)", IO_MAX_CONNECTIONS);
				if (++n_servers == MAX_CLI_SERVERS)
					arg_error("exceeded maximum number of servers (%d)", MAX_CLI_SERVERS);

				cli_servers[n_servers - 1].host = optarg;
				cli_servers[n_servers - 1].port = "6667";
				cli_servers[n_servers - 1].port = "6697";
				cli_servers[n_servers - 1].pass = NULL;
				cli_servers[n_servers - 1].nicks = NULL;
				cli_servers[n_servers - 1].chans = NULL;


@@ 233,6 241,7 @@ parse_args(int argc, char **argv)
		default_realname = getpwuid_pw_name();

	state_init();
	draw_init();

	for (size_t i = 0; i < n_servers; i++) {



@@ 260,6 269,8 @@ parse_args(int argc, char **argv)
		channel_set_current(s->channel);
	}

	io_init();

	for (size_t i = 0; i < n_servers; i++)
		io_cx(cli_servers[i].s->connection);



@@ 273,8 284,7 @@ main(int argc, char **argv)
	int ret;

	if ((ret = parse_args(argc, argv)) == 0) {
		draw_init();
		io_init();
		io_start();
		draw_term();
		state_term();
	}

M src/rirc.h => src/rirc.h +1 -1
@@ 3,10 3,10 @@

/* Default config values obtained at runtime */

extern const char *ca_cert_path;
extern const char *default_nick_set;
extern const char *default_username;
extern const char *default_realname;

extern const char *runtime_name;

#endif

M src/state.c => src/state.c +40 -22
@@ 24,7 24,7 @@

static void _newline(struct channel*, enum buffer_line_t, const char*, const char*, va_list);
static void state_io_cxed(struct server*);
static void state_io_dxed(struct server*, va_list);
static void state_io_dxed(struct server*);
static void state_io_ping(struct server*, unsigned int);
static void state_io_signal(enum io_sig_t);



@@ 133,7 133,7 @@ state_term(void)
	do {
		s2 = s1;
		s1 = s2->next;
		io_free(s2->connection);
		connection_free(s2->connection);
		server_free(s2);
	} while (s1 != state_server_list()->head);
}


@@ 337,7 337,7 @@ action_close_server(char c)
			newlinef(s->channel, 0, "-!!-", "sendf fail: %s", io_err(ret));

		io_dx(s->connection);
		io_free(s->connection);
		connection_free(s->connection);
		server_list_del(state_server_list(), s);
		server_free(s);



@@ 375,7 375,7 @@ action(int (*a_handler)(char), const char *fmt, ...)
/* Action line should be:
 *
 *
 * Find: [current result]/[(server if not current server[socket if not 6667])] : <search input> */
 * Find: [current result]/[(server if not current server[socket if not 6697])] : <search input> */
static int
action_find_channel(char c)
{


@@ 429,7 429,7 @@ action_find_channel(char c)
			action(action_find_channel, "Find: %s -- %s",
					search_cptr->name, search_buf);
		} else {
			if (!strcmp(search_cptr->server->port, "6667"))
			if (!strcmp(search_cptr->server->port, "6697"))
				action(action_find_channel, "Find: %s/%s -- %s",
						search_cptr->server->host, search_cptr->name, search_buf);
			else


@@ 729,15 729,12 @@ state_io_cxed(struct server *s)
}

static void
state_io_dxed(struct server *s, va_list ap)
state_io_dxed(struct server *s)
{
	struct channel *c = s->channel;
	va_list ap_copy;

	do {
		va_copy(ap_copy, ap);
		_newline(c, 0, "-!!-", va_arg(ap_copy, const char *), ap_copy);
		va_end(ap_copy);
		newline(c, 0, "-!!-", " -- disconnected --");
		channel_reset(c);
		c = c->next;
	} while (c != s->channel);


@@ 779,10 776,9 @@ io_cb(enum io_cb_t type, const void *cb_obj, ...)
	switch (type) {
		case IO_CB_CXED:
			state_io_cxed(s);
			_newline(s->channel, 0, "--", va_arg(ap, const char *), ap);
			break;
		case IO_CB_DXED:
			state_io_dxed(s, ap);
			state_io_dxed(s);
			break;
		case IO_CB_PING_0:
		case IO_CB_PING_1:


@@ 820,11 816,11 @@ command(struct channel *c, char *buf)
	}

	if (!strcasecmp(cmnd, "quit")) {
		io_term();
		io_stop();
	}

	if (!strcasecmp(cmnd, "connect")) {

		// TODO: parse --args
		const char *host = strtok_r(NULL, " ", &saveptr);
		const char *port = strtok_r(NULL, " ", &saveptr);
		const char *pass = strtok_r(NULL, " ", &saveptr);


@@ 840,7 836,7 @@ command(struct channel *c, char *buf)
				newlinef(c, 0, "-!!-", "%s", io_err(err));
			}
		} else {
			port = (port ? port : "6667");
			port = (port ? port : "6697");
			user = (user ? user : default_username);
			real = (real ? real : default_realname);



@@ 1047,14 1043,36 @@ io_cb_read_inp(char *buf, size_t len)
void
io_cb_read_soc(char *buf, size_t len, const void *cb_obj)
{
	struct channel *c = ((struct server *)cb_obj)->channel;
	struct server *s = (struct server *)cb_obj;
	struct channel *c = s->channel;
	size_t ci = s->read.i;
	size_t n = len;

	struct irc_message m;
	for (size_t i = 0; i < n; i++) {

	if (!(irc_message_parse(&m, buf, len)))
		newlinef(c, 0, "-!!-", "failed to parse message");
	else
		irc_recv((struct server *)cb_obj, &m);
		char cc = buf[i];

	redraw();
		if (ci && cc == '\n' && ((i && buf[i - 1] == '\r') || (!i && s->read.cl == '\r'))) {

			s->read.buf[ci] = 0;

			debug(" recv: (%zu) %s", ci, s->read.buf);

			struct irc_message m;

			if (!(irc_message_parse(&m, s->read.buf, ci)))
				newlinef(c, 0, "-!!-", "failed to parse message");
			else
				irc_recv(s, &m);

			redraw();

			ci = 0;
		} else if (ci < IRC_MESSAGE_LEN && (isprint(cc) || cc == 0x01)) {
			s->read.buf[ci++] = cc;
		}
	}

	s->read.cl = buf[n - 1];
	s->read.i = ci;
}

M src/utils/utils.h => src/utils/utils.h +3 -0
@@ 13,6 13,9 @@
#define ELEMS(X) (sizeof((X)) / sizeof((X)[0]))
#define ARR_ELEM(A, E) ((E) >= 0 && (size_t)(E) < ELEMS((A)))

#define SEC_IN_MS(X) ((X) * 1000)
#define SEC_IN_US(X) ((X) * 1000 * 1000)

#define UNUSED(X) ((void)(X))

#define MESSAGE(TYPE, ...) \

M test/draw.c.mock => test/draw.c.mock +1 -0
@@ 1,4 1,5 @@
void draw(union draw d) { UNUSED(d); }
void draw_init(void) { ; }
void draw_bell(void) { ; }
void draw_term(void) { ; }
void

M test/io.c => test/io.c +2 -83
@@ 1,88 1,7 @@
#include "test/test.h"

/* Preclude definition for testing */
#define IO_MESG_LEN 10

#include "src/io.c"

/* Stubbed state callbacks */
void io_cb(enum io_cb_t t, const void *obj, ...) { UNUSED(t); UNUSED(obj); }
void io_cb_read_inp(char *buf, size_t n) { UNUSED(buf); UNUSED(n); }

static int cb_count;
static int cb_size;
static char soc_buf[IO_MESG_LEN + 1];

void io_cb_read_soc(char *buf, size_t n, const void *obj)
{
	UNUSED(obj);
	UNUSED(n);
	cb_count++;
	cb_size = (int)n;
	snprintf(soc_buf, sizeof(soc_buf), "%s", buf);
}

static void
test_io_recv(void)
{
	struct connection c;
	memset(&c, 0, sizeof(c));

#define IO_RECV(S) \
	io_recv(&c, (S), sizeof((S)) - 1);

	/* Test complete message received */
	soc_buf[0] = 0;

	IO_RECV("foo\r\nbar\r\n");
	assert_eq((signed) c.read.i, 0);
	assert_eq(cb_count, 2);
	assert_eq(cb_size, 3);
	assert_strcmp(soc_buf, "bar");

	/* Test empty messages */
	IO_RECV("\r\n\r\n");
	IO_RECV("\r");
	IO_RECV("\n");
	assert_eq(cb_count, 2);
	
	/* Test message received in multiple parts */
	IO_RECV("a");
	IO_RECV("b");
	IO_RECV("c\r");
	IO_RECV("\nx");
	assert_eq(cb_count, 3);
	assert_eq(cb_size, 3);
	assert_strcmp(soc_buf, "abc");
	IO_RECV("yz\r\n");
	assert_eq(cb_count, 4);
	assert_eq(cb_size, 3);
	assert_strcmp(soc_buf, "xyz");

	/* Test non-delimiter, non-CTCP control character are skiped */
	const char str1[] = {'a', 0x00, 0x01, 0x02, '\r', 'b', '\n', 'c', 0x01, '\r', '\n', 0};
	const char str2[] = {'a', 0x01, 'b', 'c', 0x01, 0};
	IO_RECV(str1);
	assert_eq(cb_count, 5);
	assert_eq(cb_size, 5);
	assert_strcmp(soc_buf, str2);

	/* Test buffer overrun */
	IO_RECV("abcdefghijklmnopqrstuvwxyz");
	IO_RECV("\r\n");
	assert_eq(cb_count, 6);
	assert_eq(cb_size, 10);
	assert_strcmp(soc_buf, "abcdefghij");

#undef IO_RECV
}
#include <stdlib.h>

int
main(void)
{
	struct testcase tests[] = {
		TESTCASE(test_io_recv),
	};

	return run_tests(tests);
	return EXIT_SUCCESS;
}

M test/io.c.mock => test/io.c.mock +4 -2
@@ 12,5 12,7 @@ int io_dx(struct connection *c) { UNUSED(c); return 0; }
int io_sendf(struct connection *c, const char *f, ...) { UNUSED(c); UNUSED(f); return 0; }
unsigned io_tty_cols(void) { return 0; }
unsigned io_tty_rows(void) { return 0; }
void io_free(struct connection *c) { UNUSED(c); }
void io_term(void) { ; }
void connection_free(struct connection *c) { UNUSED(c); }
void io_stop(void) { ; }
void io_start(void) { ; }
void io_init(void) { ; }

M test/state.c => test/state.c +0 -2
@@ 69,8 69,6 @@ test_state(void)
	assert_ptr_eq(server_list_add(state_server_list(), s1), NULL);
	assert_ptr_eq(server_list_add(state_server_list(), s2), NULL);
	assert_ptr_eq(server_list_add(state_server_list(), s3), NULL);

	state_term();
}

int