~rcr/rirc

c5e39cbb9b7a23869ab6deae8fce7e49413059d4 — Richard Robbins 1 year, 1 month ago 6f79e27 + 9df2102
Merge branch 'dev' into static_analysis
M .builds/alpine.yml => .builds/alpine.yml +1 -1
@@ 15,4 15,4 @@ tasks:
      git submodule update --recursive
  - build: |
      cd rirc
      make rirc debug test
      make rirc rirc.debug test

M .builds/debian.yml => .builds/debian.yml +2 -2
@@ 24,7 24,7 @@ tasks:
      git submodule update --recursive
  - build: |
      cd rirc
      make rirc debug test
      make rirc rirc.debug test
  - static-analysis: |
      cd rirc
      branch=$(git name-rev --name-only HEAD)


@@ 53,5 53,5 @@ tasks:
      echo >> sonar-project.properties "sonar.projectKey=rirc"
      echo >> sonar-project.properties "sonar.sources=src"
      echo >> sonar-project.properties "sonar.tests=test"
      ./build-wrapper-linux-x86/build-wrapper-linux-x86-64 --out-dir bw-output make clean debug test
      ./build-wrapper-linux-x86/build-wrapper-linux-x86-64 --out-dir bw-output make clean rirc.debug test
      ./sonar-scanner-$SONAR_VER-linux/bin/sonar-scanner

M .gitignore => .gitignore +2 -0
@@ 12,5 12,7 @@ config.h
coverage
rirc
rirc.debug
rirc.debug.address
rirc.debug.thread
rirc.out
tags

M CHANGELOG => CHANGELOG +5 -1
@@ 4,6 4,10 @@ Summary of notable changes and features
## Unreleased (dev)
### Features
 - add command :disconnect
 - add cli option `--ipv4`
 - add cli option `--ipv6`
 - add cli option `--tls-disable`
 - add cli option `--tls-verify`
 - add SSL support
    - add mbedtls git submodule
    - add CA_CERT_PATH define to config.h


@@ 12,7 16,7 @@ Summary of notable changes and features
    - add command /cap-ls
    - add command /cap-list
 - changed standard versions
    - c99 -> c11
    - c99          -> c11
    - POSIX.1-2001 -> POSIX.1-2008
### Fixes


M Makefile => Makefile +18 -25
@@ 1,5 1,3 @@
.POSIX:

VERSION := 0.1.2

# Release and debug build executable names


@@ 7,24 5,20 @@ BIN_R := rirc
BIN_D := rirc.debug

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

STDS := \
	-std=c11 \
	-D_BSD_VISIBLE \
	-D_DARWIN_C_SOURCE \
	-D_POSIX_C_SOURCE=200809L
TLS_CONF := $(PWD)/lib/mbedtls.h
TLS_INCL := -I $(PWD)/lib/mbedtls/include/ -DMBEDTLS_CONFIG_FILE='<$(TLS_CONF)>'
TLS_LIBS := ./lib/mbedtls/library/libmbedtls.a \
            ./lib/mbedtls/library/libmbedx509.a \
            ./lib/mbedtls/library/libmbedcrypto.a

TLS_CONF := ./lib/mbedtls.h
TLS_LIBS := \
	./lib/mbedtls/library/libmbedtls.a \
	./lib/mbedtls/library/libmbedx509.a \
	./lib/mbedtls/library/libmbedcrypto.a
STDS := -std=c11 -D_POSIX_C_SOURCE=200809L

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


@@ 71,6 65,10 @@ $(DIR_B)/%.db.o: $(DIR_S)/%.c config.h
	@$(PP) $(CFLAGS_D) -MM -MP -MT $@ -MF $(@:.o=.d) $<
	@$(CC) $(CFLAGS_D) -c -o $@ $<

# Default config file
config.h:
	cp config.def.h config.h

# Gperf generated source
%.gperf.out: %.gperf
	gperf --output-file=$@ $<


@@ 82,16 80,13 @@ $(DIR_B)/%.t: $(DIR_T)/%.c
	-@rm -f $(@:.t=.td) && $(TEST_EXT) ./$@ || mv $@ $(@:.t=.td)
	@[ ! -f $(@:.t=.td) ]

# TLS libraries
$(TLS_LIBS): $(TLS_CONF)
	@CFLAGS="-I$(PWD) -DMBEDTLS_CONFIG_FILE='<$(TLS_CONF)>'" $(MAKE) -C ./lib/mbedtls clean lib

# Build directories
$(DIR_B):
	@for dir in $(patsubst $(DIR_S)/%, %, $(SUBDIRS)); do mkdir -p $(DIR_B)/$$dir; done

config.h:
	cp config.def.h config.h
# TLS libraries
$(TLS_LIBS): $(TLS_CONF)
	@CFLAGS="$(TLS_INCL)" $(MAKE) -C ./lib/mbedtls clean lib

clean:
	rm -rf $(DIR_B) $(BIN_R) $(BIN_D)


@@ 110,12 105,10 @@ uninstall:
	rm -f $(BIN_DIR)/rirc
	rm -f $(MAN_DIR)/rirc.1

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

-include $(OBJS_R:.o=.d)
-include $(OBJS_D:.o=.d)
-include $(OBJS_T:.t=.d)

.PHONY: all clean default install uninstall test
.PHONY: clean install uninstall test

M README.md => README.md +11 -5
@@ 28,7 28,7 @@

A minimalistic irc client written in C.

rirc supports only TLS connections, the default port is 6697
Connections are TLS enabled over port 6697 by default.

### Configuring:



@@ 66,13 66,13 @@ make install
### Usage:

```
rirc [-hv] [-s server [-p port] [-w pass] [-u user] [-r real] [-n nicks] [-c chans]], ...]
rirc [-hv] [-s server [...]]

Help:
  -h, --help      Print this message and exit
Info:
  -h, --help      Print help message and exit
  -v, --version   Print rirc version and exit

Options:
Server options:
  -s, --server=SERVER       Connect to SERVER
  -p, --port=PORT           Connect to SERVER using PORT
  -w, --pass=PASS           Connect to SERVER using PASS


@@ 80,6 80,12 @@ Options:
  -r, --realname=REALNAME   Connect to SERVER using REALNAME
  -n, --nicks=NICKS         Comma separated list of nicks to use for SERVER
  -c, --chans=CHANNELS      Comma separated list of channels to join for SERVER

Server connection options:
   --ipv4                   Connect to server using only ipv4 addresses
   --ipv6                   Connect to server using only ipv6 addresses
   --tls-disable            Set server TLS disabled
   --tls-verify=<mode>      Set server TLS peer certificate verification mode
```

Commands:

M lib/mbedtls => lib/mbedtls +1 -1
@@ 1,1 1,1 @@
Subproject commit 0fce215851cc069c5b5def12fcc18725055fa6cf
Subproject commit 523f0554b6cdc7ace5d360885c3f5bbcc73ec0e8

M lib/mbedtls.h => lib/mbedtls.h +1 -1
@@ 111,6 111,6 @@
/* Error strings */
#define MBEDTLS_ERROR_C

#include "lib/mbedtls/include/mbedtls/check_config.h"
#include "mbedtls/check_config.h"

#endif

M rirc.1 => rirc.1 +44 -70
@@ 3,78 3,59 @@
.SH NAME
rirc \- a minimalistic irc client written in C
.SH SYNOPSIS
.B rirc
.RB [ -hv ]
.RB [ -s
.I server
.RB [ -p
.IR port ]
.RB [ -w
.IR pass ]
.RB [ -u
.IR username ]
.RB [ -r
.IR realname ]
.RB [ -n
.IR nicks ]
.RB [ -c
.IR chans "]], ...]"
\fBrirc\fR [ \fB-hv\fR ] [ \fB-s\fR \fIserver\fR [ \fB...\fR ]]
.SH DESCRIPTION
.P
.PP
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.
.PP
Connections are TLS enabled over port 6697 by default.
.SH OPTIONS
.TP 5
.B "-h, --help"
Print help message and exit
.TP
.B -h, --help
Print this message and exit
.TP
.B -v, --version
.B "-v, --version"
Print rirc version and exit
.TP
.SS Server options
.TP 5
.BI "-s, --server=" server
Connect to
.I server
Connect to \fIserver\fP
.TP
.BI "-p, --port=" port
Connect to
.I server
using
.I port
Connect to \fIserver\fP using \fIport\fP
.TP
.BI "-w, --pass=" pass
Connect to
.I server
using
.I pass
Connect to \fIserver\fP using \fIpass\fP
.TP
.BI "-u, --username=" username
Connect to
.I server
using
.I username
Connect to \fIserver\fP using \fIusername\fP
.TP
.BI "-r, --realname=" realname
Connect to
.I server
using
.I realname
Connect to \fIserver\fP using \fIrealname\fP
.TP
.BI "-n, --nicks=" nicks
Comma separated list of
.I nicks
to use for
.I server
Comma separated list of \fInicks\fP to use for \fIserver\fP
.TP
.BI "-c, --chans=" chans
Comma separated list of
.I chans
to join on
.I server
Comma separated list of \fIchannels\fP to join for \fIserver\fP
.SS Server connection options
.TP 5
.B --ipv4
Connect to \fIserver\fP using only ipv4 addresses
.TP
.B --ipv6
Connect to \fIserver\fP using only ipv6 addresses
.TP
.B --tls-disable
Set \fIserver\fP TLS disabled, default port to 6667
.TP
.BI --tls-verify= mode
Set \fIserver\fP TLS peer certificate verification \fImode\fP:
.EX
\(bu \fIdisabled\fP - cert is not verified
\(bu \fIoptional\fP - cert is verified, handshake continues on error
\(bu \fIrequired\fP - cert is verified, handshake is aborted on error (default)
.EE
.SH USAGE
.TS
l .


@@ 95,10 76,10 @@ Keys:
  ^X;close current channel
  ^U;scroll current buffer up
  ^D;scroll current buffer down
   >;input cursor back
   <;input cursor forward
   ^;input history back
   v;input history forward
  <left>;input cursor back
  <right>;input cursor forward
  <up>;input history back
  <down>;input history forward
.TE

.TS


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



@@ 136,18 117,11 @@ IRC commands:
  /quit;[quit message]
  /raw;<message>
.TE

.SH AUTHORS
.MT mail@rcr.io
Richard 'rcr' Robbins
.ME
.SH LICENSE
See the
.I LICENSE
file in the source directory for the terms of redistribution.
.ME .
.SH SEE ALSO
See
.UR
http://rcr.io/rirc/
.UE
for additional documentation.
.UR http://rcr.io/rirc/
Additional documentation
.UE .

M scripts/compile_commands.sh => scripts/compile_commands.sh +2 -2
@@ 1,4 1,4 @@
#!/bin/sh
#!/bin/bash

set -e



@@ 7,4 7,4 @@ rm -f compile_commands.json
export CC=clang
export CC_EXT="-Wno-empty-translation-unit"

bear make clean debug
bear make clean rirc.debug

M scripts/coverage.sh => scripts/coverage.sh +1 -1
@@ 1,4 1,4 @@
#!/bin/sh
#!/bin/bash

set -e


M scripts/coverity_run.sh => scripts/coverity_run.sh +3 -3
@@ 16,12 16,12 @@ if [[ -z "${COVERITY_TOKEN}" ]]; then
	fail "missing env COVERITY_TOKEN"
fi

COVERITY_OUT="coverity-out"
COVERITY_TAR="coverity-out.tgz"
COVERITY_OUT="cov-int"
COVERITY_TAR="cov-int.tgz"

VERSION=$(git rev-parse --short HEAD)

PATH=$(pwd)/$1/bin:$PATH cov-build --dir "$COVERITY_OUT" make clean rirc debug test
PATH=$(pwd)/$1/bin:$PATH cov-build --dir "$COVERITY_OUT" make clean rirc rirc.debug test

tar czf "$COVERITY_TAR" "$COVERITY_OUT"


M scripts/pre-commit.sh => scripts/pre-commit.sh +1 -1
@@ 1,4 1,4 @@
#!/bin/sh
#!/bin/bash
#
# pre-commit testing hook, setup:
#   ln -s ../../scripts/pre-commit.sh .git/hooks/pre-commit

A scripts/sanitizers_build.sh => scripts/sanitizers_build.sh +19 -0
@@ 0,0 1,19 @@
#!/bin/bash

set -e

export CC=clang

export CC_EXT="-fsanitize=address,undefined -fno-omit-frame-pointer"
export LD_EXT="-fsanitize=address,undefined"

make -e clean rirc.debug

mv rirc.debug rirc.debug.address

export CC_EXT="-fsanitize=address,undefined -fno-omit-frame-pointer"
export LD_EXT="-fsanitize=thread,undefined"

make -e clean rirc.debug

mv rirc.debug rirc.debug.thread

A scripts/sanitizers_test.sh => scripts/sanitizers_test.sh +13 -0
@@ 0,0 1,13 @@
#!/bin/bash

set -e

export CC=clang

export CC_EXT="-fsanitize=address,undefined -fno-omit-frame-pointer"
export LD_EXT="-fsanitize=address,undefined"

# for core dumps:
# export ASAN_OPTIONS="abort_on_error=1:disable_coredump=0"

make -e test

M scripts/stack_usage.sh => scripts/stack_usage.sh +2 -2
@@ 1,10 1,10 @@
#!/bin/sh
#!/bin/bash

set -e

export CC=gcc
export CC_EXT="-fstack-usage"

make -e clean debug
make -e clean rirc.debug

find . -name "*.su" -exec cat "{}" ";" | sort -n -k2 | column -t

M src/handlers/ircv3.h => src/handlers/ircv3.h +0 -13
@@ 1,16 1,3 @@
/* TODO
 *  - cap commands (after registration)
 *      - /cap-req [-]cap [...]
 *  - cap setting for auto on connect/NEW
 *      - :set, set/unset auto, and for printing state
 *  - LS caps:
 *      [name] [set/unset] [auto]
 *      [name] [unsupported]
 *  - LIST caps:
 *      [name] [auto]
 *  - CAP args, e.g. cap=foo,bar
 */

#ifndef IRCV3_H
#define IRCV3_H


M src/io.c => src/io.c +374 -216
@@ 1,5 1,6 @@
#include "src/io.h"

#include <arpa/inet.h>
#include <errno.h>
#include <netdb.h>
#include <pthread.h>


@@ 14,18 15,14 @@

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

/* lib/mbedtls.h is the compile time config
 * and must precede the other mbedtls headers */
#include "lib/mbedtls.h"
#include "lib/mbedtls/include/mbedtls/ctr_drbg.h"
#include "lib/mbedtls/include/mbedtls/entropy.h"
#include "lib/mbedtls/include/mbedtls/error.h"
#include "lib/mbedtls/include/mbedtls/net_sockets.h"
#include "lib/mbedtls/include/mbedtls/ssl.h"
#include "lib/mbedtls/include/mbedtls/x509_crt.h"
#include "mbedtls/ctr_drbg.h"
#include "mbedtls/entropy.h"
#include "mbedtls/error.h"
#include "mbedtls/net_sockets.h"
#include "mbedtls/ssl.h"
#include "mbedtls/x509_crt.h"

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


@@ 79,9 76,9 @@
#define PT_UL(X) PT_CF(pthread_mutex_unlock((X)))
#define PT_CB(...) \
	do {                    \
		PT_LK(&cb_mutex);   \
		PT_LK(&io_cb_mutex);   \
		io_cb(__VA_ARGS__); \
		PT_UL(&cb_mutex);   \
		PT_UL(&io_cb_mutex);   \
	} while (0)

/* state transition */


@@ 122,16 119,17 @@ struct connection
		IO_ST_PING, /* Socket connected, network state in question */
	} st_cur, /* current thread state */
	  st_new; /* new thread state */
	int soc;
	mbedtls_net_context tls_fd;
	mbedtls_ssl_config  tls_conf;
	mbedtls_ssl_context tls_ctx;
	pthread_mutex_t mtx;
	pthread_t tid;
	uint32_t flags;
	unsigned ping;
	unsigned rx_sleep;
};

static const char* io_tls_strerror(int, char*, size_t);
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_ping(struct connection*);


@@ 140,8 138,6 @@ static int io_cx_read(struct connection*, unsigned);
static void io_fatal(const char*, int);
static void io_sig_handle(int);
static void io_sig_init(void);
static void io_tls_init(void);
static void io_tls_term(void);
static void io_tty_init(void);
static void io_tty_term(void);
static void io_tty_winsize(void);


@@ 151,15 147,26 @@ static int io_running;
static mbedtls_ctr_drbg_context tls_ctr_drbg;
static mbedtls_entropy_context  tls_entropy;
static mbedtls_x509_crt         tls_x509_crt;
static pthread_mutex_t cb_mutex = PTHREAD_MUTEX_INITIALIZER;
static pthread_mutex_t io_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 const char* io_strerror(char*, size_t);
static int io_net_connect(struct connection*);
static void io_net_close(int);

/* TLS */
static const char* io_tls_err(int);
static int io_tls_establish(struct connection*);
static int io_tls_x509_vrfy(struct connection*);
static void io_tls_init(void);
static void io_tls_term(void);

struct connection*
connection(const void *obj, const char *host, const char *port)
connection(const void *obj, const char *host, const char *port, uint32_t flags)
{
	struct connection *cx;



@@ 167,6 174,7 @@ connection(const void *obj, const char *host, const char *port)
		fatal("malloc: %s", strerror(errno));

	cx->obj = obj;
	cx->flags = flags;
	cx->host = strdup(host);
	cx->port = strdup(port);
	cx->st_cur = IO_ST_DXED;


@@ 272,20 280,34 @@ io_sendf(struct connection *cx, const char *fmt, ...)
	ret = 0;
	written = 0;

	do {
		if ((ret = mbedtls_ssl_write(&(cx->tls_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;
	if (cx->flags & IO_TLS_ENABLED) {
		do {
			if ((ret = mbedtls_ssl_write(&(cx->tls_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);
		} while ((written += ret) < len);
	} else {
		do {
			if ((ret = send(cx->soc, sendbuf + ret, len - ret, 0)) == -1) {
				switch (errno) {
					case EINTR:
						ret = 0;
						continue;
					default:
						return IO_ERR_SSL_WRITE;
				}
			}
		} while ((written += ret) < len);
	}

	return IO_ERR_NONE;
}


@@ 309,9 331,9 @@ io_start(void)
		ssize_t ret = read(STDIN_FILENO, buf, sizeof(buf));

		if (ret > 0) {
			PT_LK(&cb_mutex);
			PT_LK(&io_cb_mutex);
			io_cb_read_inp(buf, ret);
			PT_UL(&cb_mutex);
			PT_UL(&io_cb_mutex);
		} else {
			if (errno == EINTR) {
				if (flag_sigwinch_cb) {


@@ 378,13 400,6 @@ io_err(int err)
	}
}

const char*
io_tls_strerror(int err, char *buf, size_t len)
{
	mbedtls_strerror(err, buf, len);
	return buf;
}

static enum io_state_t
io_state_rxng(struct connection *cx)
{


@@ 409,123 424,13 @@ io_state_rxng(struct connection *cx)
static enum io_state_t
io_state_cxng(struct connection *cx)
{
	char buf[MAX(INET6_ADDRSTRLEN, 512)];
	enum io_state_t st = IO_ST_RXNG;
	int ret;
	uint32_t cert_ret;

	mbedtls_net_init(&(cx->tls_fd));
	mbedtls_ssl_init(&(cx->tls_ctx));
	mbedtls_ssl_config_init(&(cx->tls_conf));

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

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

	if ((ret = io_net_ip_str(cx->tls_fd.fd, buf, sizeof(buf))) != 0) {
		if (ret == IO_NET_ERR_EINTR) {
			st = IO_ST_DXED;
			goto err;
		}
		io_cb_info(cx, " ... Connected (failed to optain IP address)");
	} else {
		io_cb_info(cx, " ... Connected to [%s]", buf);
	}

	io_cb_info(cx, " ... Establishing TLS connection");

	if (mbedtls_ssl_config_defaults(
			&(cx->tls_conf),
			MBEDTLS_SSL_IS_CLIENT,
			MBEDTLS_SSL_TRANSPORT_STREAM,
			MBEDTLS_SSL_PRESET_DEFAULT) != 0) {
		io_cb_err(cx, " ... mbedtls_ssl_config_defaults: %s", io_tls_strerror(ret, buf, sizeof(buf)));
		goto err;
	}

	mbedtls_ssl_conf_max_version(
			&(cx->tls_conf),
			MBEDTLS_SSL_MAJOR_VERSION_3,
			MBEDTLS_SSL_MINOR_VERSION_3);

	mbedtls_ssl_conf_min_version(
			&(cx->tls_conf),
			MBEDTLS_SSL_MAJOR_VERSION_3,
			MBEDTLS_SSL_MINOR_VERSION_3);

	mbedtls_ssl_conf_ca_chain(&(cx->tls_conf), &tls_x509_crt, NULL);
	mbedtls_ssl_conf_rng(&(cx->tls_conf), mbedtls_ctr_drbg_random, &tls_ctr_drbg);

	if ((ret = mbedtls_net_set_block(&(cx->tls_fd))) != 0) {
		io_cb_err(cx, " ... mbedtls_net_set_block: %s", io_tls_strerror(ret, buf, sizeof(buf)));
		goto err;
	}

	if ((ret = mbedtls_ssl_setup(&(cx->tls_ctx), &(cx->tls_conf))) != 0) {
		io_cb_err(cx, " ... mbedtls_ssl_setup: %s", io_tls_strerror(ret, buf, sizeof(buf)));
		goto err;
	}

	if ((ret = mbedtls_ssl_set_hostname(&(cx->tls_ctx), cx->host)) != 0) {
		io_cb_err(cx, " ... mbedtls_ssl_set_hostname: %s", io_tls_strerror(ret, buf, sizeof(buf)));
		goto err;
	}

	mbedtls_ssl_set_bio(
		&(cx->tls_ctx),
		&(cx->tls_fd),
		mbedtls_net_send,
		NULL,
		mbedtls_net_recv_timeout);

	while ((ret = mbedtls_ssl_handshake(&(cx->tls_ctx))) != 0) {
		if (ret != MBEDTLS_ERR_SSL_WANT_READ && ret != MBEDTLS_ERR_SSL_WANT_WRITE) {
			io_cb_err(cx, " ... mbedtls_ssl_handshake: %s", io_tls_strerror(ret, buf, sizeof(buf)));
			goto err;
		}
	}

	if ((cert_ret = mbedtls_ssl_get_verify_result(&(cx->tls_ctx))) != 0) {
		if (mbedtls_x509_crt_verify_info(buf, sizeof(buf), "", cert_ret) <= 0) {
			io_cb_err(cx, " ... failed to verify cert: unknown failure");
		} else {
			io_cb_err(cx, " ... failed to verify cert: %s", buf);
		}
		goto err;
	}
	if ((cx->soc = io_net_connect(cx)) < 0)
		return IO_ST_RXNG;

	io_cb_info(cx, " ... TLS connection established");
	io_cb_info(cx, " ...   - version:     %s", mbedtls_ssl_get_version(&(cx->tls_ctx)));
	io_cb_info(cx, " ...   - ciphersuite: %s", mbedtls_ssl_get_ciphersuite(&(cx->tls_ctx)));
	if ((cx->flags & IO_TLS_ENABLED) && io_tls_establish(cx) < 0)
		return IO_ST_RXNG;

	return IO_ST_CXED;

err:

	io_cb_err(cx, " ... TLS connection failure");

	mbedtls_ssl_config_free(&(cx->tls_conf));
	mbedtls_ssl_free(&(cx->tls_ctx));
	mbedtls_net_free(&(cx->tls_fd));

	return st;
}

static enum io_state_t


@@ 615,10 520,10 @@ io_thread(void *arg)
	cx->st_cur = IO_ST_CXNG;

	do {
		enum io_state_t st_cur;
		enum io_state_t st_new;
		enum io_state_t st_old;

		switch (cx->st_cur) {
		switch ((st_cur = cx->st_cur)) {
			case IO_ST_CXED: st_new = io_state_cxed(cx); break;
			case IO_ST_CXNG: st_new = io_state_cxng(cx); break;
			case IO_ST_PING: st_new = io_state_ping(cx); break;


@@ 627,30 532,28 @@ io_thread(void *arg)
				fatal("invalid state: %d", cx->st_cur);
		}

		st_old = cx->st_cur;

		PT_LK(&(cx->mtx));

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

		cx->st_cur = st_new;
		cx->st_new = IO_ST_INVALID;

		PT_UL(&(cx->mtx));

		/* State transitions */
		switch (ST_X(st_old, st_new)) {
		switch (ST_X(st_cur, st_new)) {
			case ST_X(IO_ST_DXED, IO_ST_CXNG): /* A1 */
			case ST_X(IO_ST_RXNG, IO_ST_CXNG): /* A2,C */
				io_cb_info(cx, "Connecting to %s:%s", cx->host, cx->port);
				break;
			case ST_X(IO_ST_CXED, IO_ST_CXNG): /* F1 */
				io_cb_dxed(cx);
				break;
			case ST_X(IO_ST_PING, IO_ST_CXNG): /* F2 */
				io_cb_err(cx, "connection timeout (%u)", cx->ping);
				io_cb_err(cx, "Connection timeout (%u)", cx->ping);
				io_cb_dxed(cx);
				break;
			case ST_X(IO_ST_RXNG, IO_ST_DXED): /* B1 */


@@ 663,12 566,12 @@ io_thread(void *arg)
				io_cb_dxed(cx);
				break;
			case ST_X(IO_ST_CXNG, IO_ST_CXED): /* D */
				io_cb_info(cx, " ... Connection successful");
				io_cb_info(cx, " .. Connection successful");
				io_cb_cxed(cx);
				cx->rx_sleep = 0;
				break;
			case ST_X(IO_ST_CXNG, IO_ST_RXNG): /* E */
				io_cb_err(cx, " ... Connection failed -- retrying");
				io_cb_err(cx, " .. Connection failed -- retrying");
				break;
			case ST_X(IO_ST_CXED, IO_ST_PING): /* G */
				cx->ping = IO_PING_MIN;


@@ 683,7 586,7 @@ io_thread(void *arg)
				io_cb_ping_0(cx, cx->ping);
				break;
			default:
				fatal("BAD ST_X from: %d to: %d", st_old, st_new);
				fatal("BAD ST_X from: %d to: %d", st_cur, st_new);
		}

	} while (cx->st_cur != IO_ST_DXED);


@@ 695,14 598,21 @@ static int
io_cx_read(struct connection *cx, unsigned timeout)
{
	int ret;
	unsigned char ssl_readbuf[1024];

	mbedtls_ssl_conf_read_timeout(&(cx->tls_conf), SEC_IN_MS(timeout));

	if ((ret = mbedtls_ssl_read(&(cx->tls_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);
	unsigned char buf[1024];

	if (cx->flags & IO_TLS_ENABLED) {
		mbedtls_ssl_conf_read_timeout(&(cx->tls_conf), SEC_IN_MS(timeout));
		if ((ret = mbedtls_ssl_read(&(cx->tls_ctx), buf, sizeof(buf))) > 0) {
			PT_LK(&io_cb_mutex);
			io_cb_read_soc((char *)buf, (size_t)ret,  cx->obj);
			PT_UL(&io_cb_mutex);
		}
	} else {
		while ((ret = recv(cx->soc, buf, sizeof(buf), 0)) > 0) {
			PT_LK(&io_cb_mutex);
			io_cb_read_soc((char *)buf, (size_t)ret,  cx->obj);
			PT_UL(&io_cb_mutex);
		}
	}

	return ret;


@@ 746,17 656,297 @@ io_sig_init(void)
}

static void
io_tty_init(void)
{
	struct termios nterm;

	if (isatty(STDIN_FILENO) == 0)
		fatal("isatty: %s", strerror(errno));

	if (tcgetattr(STDIN_FILENO, &term) < 0)
		fatal("tcgetattr: %s", strerror(errno));

	nterm = term;
	nterm.c_lflag &= ~(ECHO | ICANON | ISIG);
	nterm.c_cc[VMIN]  = 1;
	nterm.c_cc[VTIME] = 0;

	if (tcsetattr(STDIN_FILENO, TCSANOW, &nterm) < 0)
		fatal("tcsetattr: %s", strerror(errno));

	if (atexit(io_tty_term))
		fatal("atexit");
}

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

	if (tcsetattr(STDIN_FILENO, TCSADRAIN, &term) < 0)
		fatal_noexit("tcsetattr: %s", strerror(errno));
}

static int
io_net_connect(struct connection *cx)
{
	char buf[MAX(INET6_ADDRSTRLEN, 512)];
	const void *addr;
	int ret;
	int soc;
	struct addrinfo *p, *res;
	struct addrinfo hints = {
		.ai_family   = AF_UNSPEC,
		.ai_flags    = AI_PASSIVE,
		.ai_protocol = IPPROTO_TCP,
		.ai_socktype = SOCK_STREAM,
	};

	if (cx->flags & IO_IPV_4)
		hints.ai_family = AF_INET;

	if (cx->flags & IO_IPV_6)
		hints.ai_family = AF_INET6;

	errno = 0;

	if ((ret = getaddrinfo(cx->host, cx->port, &hints, &res))) {

		if (ret == EAI_SYSTEM && errno == EINTR)
			return -1;

		if (ret == EAI_SYSTEM) {
			io_cb_err(cx, " .. Failed to resolve host: %s",
				io_strerror(buf, sizeof(buf)));
		} else {
			io_cb_err(cx, " .. Failed to resolve host: %s",
				gai_strerror(ret));
		}

		return -1;
	}

	ret = -1;

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

		if ((soc = socket(p->ai_family, p->ai_socktype, p->ai_protocol)) == -1)
			continue;

		if (connect(soc, p->ai_addr, p->ai_addrlen) == 0)
			break;

		io_net_close(soc);

		if (errno == EINTR)
			goto err;
	}

	if (!p && soc == -1) {
		io_cb_err(cx, " .. Failed to obtain socket: %s", io_strerror(buf, sizeof(buf)));
		goto err;
	}

	if (!p && soc >= 0) {
		io_cb_err(cx, " .. Failed to connect: %s", io_strerror(buf, sizeof(buf)));
		goto err;
	}

	if (p->ai_family == AF_INET)
		addr = &(((struct sockaddr_in*)p->ai_addr)->sin_addr);
	else
		addr = &(((struct sockaddr_in6*)p->ai_addr)->sin6_addr);

	if (inet_ntop(p->ai_family, addr, buf, sizeof(buf)))
		io_cb_info(cx, " .. Connected [%s]", buf);

	ret = soc;

err:
	freeaddrinfo(res);

	return ret;
}

static void
io_net_close(int soc)
{
	int errno_save = errno;

	while (close(soc) && errno == EINTR)
		errno_save = EINTR;

	errno = errno_save;
}

static const char*
io_strerror(char *buf, size_t buflen)
{
	if (strerror_r(errno, buf, buflen))
		snprintf(buf, buflen, "(failed to get error message)");

	return buf;
}

static int
io_tls_establish(struct connection *cx)
{
	int ret;

	io_cb_info(cx, " .. Establishing TLS connection");

	mbedtls_net_init(&(cx->tls_fd));
	mbedtls_ssl_init(&(cx->tls_ctx));
	mbedtls_ssl_config_init(&(cx->tls_conf));

	cx->tls_fd.fd = cx->soc;

	if ((ret = mbedtls_ssl_config_defaults(
			&(cx->tls_conf),
			MBEDTLS_SSL_IS_CLIENT,
			MBEDTLS_SSL_TRANSPORT_STREAM,
			MBEDTLS_SSL_PRESET_DEFAULT))) {
		io_cb_err(cx, " .. %s ", io_tls_err(ret));
		goto err;
	}

	mbedtls_ssl_conf_max_version(
			&(cx->tls_conf),
			MBEDTLS_SSL_MAJOR_VERSION_3,
			MBEDTLS_SSL_MINOR_VERSION_3);

	mbedtls_ssl_conf_min_version(
			&(cx->tls_conf),
			MBEDTLS_SSL_MAJOR_VERSION_3,
			MBEDTLS_SSL_MINOR_VERSION_3);

	mbedtls_ssl_conf_rng(&(cx->tls_conf), mbedtls_ctr_drbg_random, &tls_ctr_drbg);

	if (cx->flags & IO_TLS_VRFY_DISABLED) {
		mbedtls_ssl_conf_authmode(&(cx->tls_conf), MBEDTLS_SSL_VERIFY_NONE);
	} else {
		mbedtls_ssl_conf_ca_chain(&(cx->tls_conf), &tls_x509_crt, NULL);

		if (cx->flags & IO_TLS_VRFY_OPTIONAL)
			mbedtls_ssl_conf_authmode(&(cx->tls_conf), MBEDTLS_SSL_VERIFY_OPTIONAL);

		if (cx->flags & IO_TLS_VRFY_REQUIRED)
			mbedtls_ssl_conf_authmode(&(cx->tls_conf), MBEDTLS_SSL_VERIFY_REQUIRED);
	}

	if ((ret = mbedtls_net_set_block(&(cx->tls_fd)))) {
		io_cb_err(cx, " .. %s ", io_tls_err(ret));
		goto err;
	}

	if ((ret = mbedtls_ssl_setup(&(cx->tls_ctx), &(cx->tls_conf)))) {
		io_cb_err(cx, " .. %s ", io_tls_err(ret));
		goto err;
	}

	if ((ret = mbedtls_ssl_set_hostname(&(cx->tls_ctx), cx->host))) {
		io_cb_err(cx, " .. %s ", io_tls_err(ret));
		goto err;
	}

	mbedtls_ssl_set_bio(
		&(cx->tls_ctx),
		&(cx->tls_fd),
		mbedtls_net_send,
		NULL,
		mbedtls_net_recv_timeout);

	while ((ret = mbedtls_ssl_handshake(&(cx->tls_ctx)))) {
		if (ret != MBEDTLS_ERR_SSL_WANT_READ
		 && ret != MBEDTLS_ERR_SSL_WANT_WRITE)
			break;
	}

	if (ret && cx->flags & IO_TLS_VRFY_DISABLED) {
		io_cb_err(cx, " .. %s ", io_tls_err(ret));
		goto err;
	}

	if (io_tls_x509_vrfy(cx) < 0)
		io_cb_err(cx, " .... Unknown x509 error");

	if (ret) {
		io_cb_err(cx, " .. %s ", io_tls_err(ret));
		goto err;
	}

	io_cb_info(cx, " .. TLS connection established");
	io_cb_info(cx, " ..   - version:     %s", mbedtls_ssl_get_version(&(cx->tls_ctx)));
	io_cb_info(cx, " ..   - ciphersuite: %s", mbedtls_ssl_get_ciphersuite(&(cx->tls_ctx)));

	return 0;

err:

	io_cb_err(cx, " .. TLS connection failure");

	mbedtls_ssl_config_free(&(cx->tls_conf));
	mbedtls_ssl_free(&(cx->tls_ctx));
	mbedtls_net_free(&(cx->tls_fd));

	return -1;
}

static int
io_tls_x509_vrfy(struct connection *cx)
{
	char *s, *p;
	char buf[1024];
	uint32_t ret;

	if (!(ret = mbedtls_ssl_get_verify_result(&(cx->tls_ctx))))
		return 0;

	if (ret == (uint32_t)(-1))
		return -1;

	if (mbedtls_x509_crt_verify_info(buf, sizeof(buf), "", ret) < 0)
		return -1;

	s = buf;

	do {
		if ((p = strchr(buf, '\n')))
			*p++ = 0;

		io_cb_err(cx, " .... %s", s);

	} while ((s = p));

	return 0;
}

static const char*
io_tls_err(int err)
{
	const char *str;

	if ((str = mbedtls_high_level_strerr(err)))
		return str;

	if ((str = mbedtls_low_level_strerr(err)))
		return str;

	return "Unknown error";
}

static void
io_tls_init(void)
{
	char buf[512];
	int err;
	int ret;
	struct timespec ts;

	mbedtls_ctr_drbg_init(&tls_ctr_drbg);
	mbedtls_entropy_init(&tls_entropy);
	mbedtls_x509_crt_init(&tls_x509_crt);

	if (atexit(io_tls_term) != 0)
	if (atexit(io_tls_term))
		fatal("atexit");

	if (timespec_get(&ts, TIME_UTC) != TIME_UTC)


@@ 765,17 955,17 @@ io_tls_init(void)
	if (snprintf(buf, sizeof(buf), "rirc-%lu-%lu", ts.tv_sec, ts.tv_nsec) < 0)
		fatal("snprintf");

	if ((err = mbedtls_ctr_drbg_seed(
	if ((ret = mbedtls_ctr_drbg_seed(
			&tls_ctr_drbg,
			mbedtls_entropy_func,
			&tls_entropy,
			(const unsigned char *)buf,
			strlen(buf))) != 0) {
		fatal("mbedtls_ctr_drbg_seed: %s", io_tls_strerror(err, buf, sizeof(buf)));
			strlen(buf)))) {
		fatal("mbedtls_ctr_drbg_seed: %s", io_tls_err(ret));
	}

	if ((err = mbedtls_x509_crt_parse_path(&tls_x509_crt, ca_cert_path)) < 0)
		fatal("mbedtls_x509_crt_parse_path: %s", io_tls_strerror(err, buf, sizeof(buf)));
	if ((ret = mbedtls_x509_crt_parse_path(&tls_x509_crt, ca_cert_path)) < 0)
		fatal("mbedtls_x509_crt_parse_path: %s", io_tls_err(ret));
}

static void


@@ 787,35 977,3 @@ io_tls_term(void)
	mbedtls_entropy_free(&tls_entropy);
	mbedtls_x509_crt_free(&tls_x509_crt);
}

static void
io_tty_init(void)
{
	struct termios nterm;

	if (isatty(STDIN_FILENO) == 0)
		fatal("isatty: %s", strerror(errno));

	if (tcgetattr(STDIN_FILENO, &term) < 0)
		fatal("tcgetattr: %s", strerror(errno));

	nterm = term;
	nterm.c_lflag &= ~(ECHO | ICANON | ISIG);
	nterm.c_cc[VMIN]  = 1;
	nterm.c_cc[VTIME] = 0;

	if (tcsetattr(STDIN_FILENO, TCSANOW, &nterm) < 0)
		fatal("tcsetattr: %s", strerror(errno));

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

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

	if (tcsetattr(STDIN_FILENO, TCSADRAIN, &term) < 0)
		fatal_noexit("tcsetattr: %s", strerror(errno));
}

M src/io.h => src/io.h +33 -16
@@ 12,12 12,6 @@
 *  - cxed: connected    ~ Socket connected
 *  - ping: timing out   ~ Socket connected, network state in question
 *
 *
 *    TODO: how to we label the difference between A, and (A,C) ?
 *          just for the sake of consistency should it be A1, (A2, C)
 *
 *          is the H transition necessary?
 *
 *                             +--------+
 *                 +----(B1)-- |  rxng  |
 *                 |           +--------+


@@ 79,17 73,12 @@
 * a call to io_stop
 */

#include <stdarg.h>
#include <stddef.h>
#include <stdint.h>

struct connection;

enum io_sig_t
{
	IO_SIG_INVALID,
	IO_SIGWINCH,
	IO_SIG_SIZE
};

enum io_cb_t
{
	IO_CB_INVALID,


@@ 104,11 93,36 @@ enum io_cb_t
	IO_CB_SIZE
};

enum io_log_level
{
	IO_LOG_ERROR,
	IO_LOG_WARN,
	IO_LOG_INFO,
	IO_LOG_DEBUG,
};

enum io_sig_t
{
	IO_SIG_INVALID,
	IO_SIGWINCH,
	IO_SIG_SIZE
};

#define IO_IPV_UNSPEC        (1 << 1)
#define IO_IPV_4             (1 << 2)
#define IO_IPV_6             (1 << 3)
#define IO_TLS_ENABLED       (1 << 4)
#define IO_TLS_DISABLED      (1 << 5)
#define IO_TLS_VRFY_DISABLED (1 << 6)
#define IO_TLS_VRFY_OPTIONAL (1 << 7)
#define IO_TLS_VRFY_REQUIRED (1 << 8)

/* Returns a connection, or NULL if limit is reached */
struct connection* connection(
	const void*,  /* callback object */
	const char*,  /* host */
	const char*); /* port */
	const void*, /* callback object */
	const char*, /* host */
	const char*, /* port */
	uint32_t);   /* flags */

void connection_free(struct connection*);



@@ 138,4 152,7 @@ void io_cb(enum io_cb_t, const void*, ...);
void io_cb_read_inp(char*, size_t);
void io_cb_read_soc(char*, size_t, const void*);

/* Log message callback */
void io_cb_log(const void*, enum io_log_level, const char*, ...);

#endif

D src/io_net.c => src/io_net.c +0 -79
@@ 1,79 0,0 @@
#include <errno.h>
#include <netdb.h>
#include <unistd.h>

#include "io_net.h"
#include "utils/utils.h"

enum io_net_err
io_net_connect(int *soc_set, const char *host, const char *port)
{
	int soc = -1;
	int ret = IO_NET_ERR_UNKNOWN;
	struct addrinfo *p, *results;

	struct addrinfo hints = {
		.ai_family   = AF_UNSPEC,
		.ai_flags    = AI_PASSIVE,
		.ai_protocol = IPPROTO_TCP,
		.ai_socktype = SOCK_STREAM,
	};

	errno = 0;

	if ((ret = getaddrinfo(host, port, &hints, &results)) != 0) {

		if (ret == EAI_SYSTEM && errno == EINTR)
			return IO_NET_ERR_EINTR;

		return IO_NET_ERR_UNKNOWN_HOST;
	}

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

		if ((soc = socket(p->ai_family, p->ai_socktype, p->ai_protocol)) < 0) {
			ret = IO_NET_ERR_SOCKET_FAILED;
			continue;
		}

		if (connect(soc, p->ai_addr, p->ai_addrlen) == 0) {
			ret = IO_NET_ERR_NONE;
			break;
		}

		if (errno == EINTR) {
			ret = IO_NET_ERR_EINTR;
			break;
		}

		close(soc);
		ret = IO_NET_ERR_CONNECT_FAILED;
	}

	freeaddrinfo(results);

	if (ret == IO_NET_ERR_NONE)
		*soc_set = soc;

	return ret;
}

enum io_net_err
io_net_ip_str(int soc, char *buf, size_t len)
{
	int ret;
	socklen_t addr_len;
	struct sockaddr addr;

	addr_len = sizeof(addr);

	if ((ret = getpeername(soc, &addr, &addr_len)) == -1)
		return IO_NET_ERR_IP;

	if ((ret = getnameinfo(&addr, addr_len, buf, len, NULL, 0, NI_NUMERICHOST)) < 0) {
		if (ret == EAI_SYSTEM && errno == EINTR)
			return IO_NET_ERR_EINTR;
	}

	return (ret ? IO_NET_ERR_IP : IO_NET_ERR_NONE);
}

D src/io_net.h => src/io_net.h +0 -20
@@ 1,20 0,0 @@
#ifndef IO_NET_H
#define IO_NET_H

#include <stddef.h>

enum io_net_err
{
	IO_NET_ERR_NONE = 0,
	IO_NET_ERR_SOCKET_FAILED,
	IO_NET_ERR_UNKNOWN_HOST,
	IO_NET_ERR_CONNECT_FAILED,
	IO_NET_ERR_EINTR,
	IO_NET_ERR_IP,
	IO_NET_ERR_UNKNOWN
};

enum io_net_err io_net_connect(int*, const char*, const char*);
enum io_net_err io_net_ip_str(int, char*, size_t);

#endif

M src/rirc.c => src/rirc.c +95 -37
@@ 14,10 14,10 @@
#define MAX_CLI_SERVERS 16

#define arg_error(...) \
	do { fprintf(stderr, "%s: ", runtime_name); \
	do { fprintf(stderr, "%s ", runtime_name); \
	     fprintf(stderr, __VA_ARGS__); \
	     fprintf(stderr, "\n%s --help for usage\n", runtime_name); \
	     return 1; \
	     return -1; \
	} while (0)

static const char* opt_arg_str(char);


@@ 54,17 54,17 @@ const char *runtime_name = "rirc.debug";
const char *runtime_name = "rirc";
#endif

static const char *const rirc_usage =
static const char *const rirc_help =
"\nrirc v"VERSION" ~ Richard C. Robbins <mail@rcr.io>"
"\n"
"\nUsage:"
"\n  rirc [-hv] [-s server [-p port] [-w pass] [-n nicks] [-c chans] [-u user] [-r real]], ...]"
"\n  rirc [-hv] [-s server [...]]"
"\n"
"\nHelp:"
"\n  -h, --help      Print this message and exit"
"\nInfo:"
"\n  -h, --help      Print help message and exit"
"\n  -v, --version   Print rirc version and exit"
"\n"
"\nOptions:"
"\nServer options:"
"\n  -s, --server=SERVER       Connect to SERVER"
"\n  -p, --port=PORT           Connect to SERVER using PORT"
"\n  -w, --pass=PASS           Connect to SERVER using PASS"


@@ 72,6 72,12 @@ static const char *const rirc_usage =
"\n  -r, --realname=REALNAME   Connect to SERVER using REALNAME"
"\n  -n, --nicks=NICKS         Comma separated list of nicks to use for SERVER"
"\n  -c, --chans=CHANNELS      Comma separated list of channels to join for SERVER"
"\n"
"\nServer connection options:"
"\n   --ipv4                   Connect to server using only ipv4 addresses"
"\n   --ipv6                   Connect to server using only ipv6 addresses"
"\n   --tls-disable            Set server TLS disabled"
"\n   --tls-verify=<mode>      Set server TLS peer certificate verification mode"
"\n";

static const char *const rirc_version =


@@ 92,6 98,10 @@ opt_arg_str(char c)
		case 'c': return "-c/--chans";
		case 'u': return "-u/--username";
		case 'r': return "-r/--realname";
		case '4': return "--ipv4";
		case '6': return "--ipv6";
		case 'x': return "--tls-disable";
		case 'y': return "--tls-verify";
		default:
			fatal("unknown option flag '%c'", c);
	}


@@ 119,23 129,22 @@ parse_args(int argc, char **argv)

	size_t n_servers = 0;

	if (argc > 0)
		runtime_name = argv[0];

	srand(time(NULL));

	opterr = 0;

	struct option long_opts[] = {
		{"server",   required_argument, 0, 's'},
		{"port",     required_argument, 0, 'p'},
		{"pass",     required_argument, 0, 'w'},
		{"nicks",    required_argument, 0, 'n'},
		{"chans",    required_argument, 0, 'c'},
		{"username", required_argument, 0, 'u'},
		{"realname", required_argument, 0, 'r'},
		{"help",     no_argument,       0, 'h'},
		{"version",  no_argument,       0, 'v'},
		{"server",      required_argument, 0, 's'},
		{"port",        required_argument, 0, 'p'},
		{"pass",        required_argument, 0, 'w'},
		{"nicks",       required_argument, 0, 'n'},
		{"chans",       required_argument, 0, 'c'},
		{"username",    required_argument, 0, 'u'},
		{"realname",    required_argument, 0, 'r'},
		{"help",        no_argument,       0, 'h'},
		{"version",     no_argument,       0, 'v'},
		{"ipv4",        no_argument,       0, '4'},
		{"ipv6",        no_argument,       0, '6'},
		{"tls-disable", no_argument,       0, 'x'},
		{"tls-verify",  required_argument, 0, 'y'},
		{0, 0, 0, 0}
	};



@@ 147,10 156,12 @@ parse_args(int argc, char **argv)
		const char *chans;
		const char *username;
		const char *realname;
		int ipv;
		int tls;
		int tls_vrfy;
		struct server *s;
	} 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))) {

		switch (opt_c) {


@@ 163,55 174,89 @@ parse_args(int argc, char **argv)
				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 = "6697";
				cli_servers[n_servers - 1].pass = NULL;
				cli_servers[n_servers - 1].nicks = NULL;
				cli_servers[n_servers - 1].chans = NULL;
				cli_servers[n_servers - 1].host     = optarg;
				cli_servers[n_servers - 1].port     = NULL;
				cli_servers[n_servers - 1].pass     = NULL;
				cli_servers[n_servers - 1].nicks    = NULL;
				cli_servers[n_servers - 1].chans    = NULL;
				cli_servers[n_servers - 1].username = NULL;
				cli_servers[n_servers - 1].realname = NULL;
				cli_servers[n_servers - 1].ipv      = IO_IPV_UNSPEC;
				cli_servers[n_servers - 1].tls      = IO_TLS_ENABLED;
				cli_servers[n_servers - 1].tls_vrfy = IO_TLS_VRFY_REQUIRED;
				break;

			#define CHECK_SERVER_OPTARG(OPT_C) \
				if (*optarg == '-') \
			#define CHECK_SERVER_OPTARG(OPT_C, REQ) \
				if ((REQ) && *optarg == '-') \
					arg_error("option '%s' requires an argument", opt_arg_str((OPT_C))); \
				if (n_servers == 0) \
					arg_error("option '%s' requires a server argument first", opt_arg_str((OPT_C)));

			case 'p': /* Connect using port */
				CHECK_SERVER_OPTARG(opt_c);
				CHECK_SERVER_OPTARG(opt_c, 1);
				cli_servers[n_servers - 1].port = optarg;
				break;

			case 'w': /* Connect using port */
				CHECK_SERVER_OPTARG(opt_c);
				CHECK_SERVER_OPTARG(opt_c, 1);
				cli_servers[n_servers - 1].pass = optarg;
				break;

			case 'n': /* Comma separated list of nicks to use */
				CHECK_SERVER_OPTARG(opt_c);
				CHECK_SERVER_OPTARG(opt_c, 1);
				cli_servers[n_servers - 1].nicks = optarg;
				break;

			case 'c': /* Comma separated list of channels to join */
				CHECK_SERVER_OPTARG(opt_c);
				CHECK_SERVER_OPTARG(opt_c, 1);
				cli_servers[n_servers - 1].chans = optarg;
				break;

			case 'u': /* Connect using username */
				CHECK_SERVER_OPTARG(opt_c);
				CHECK_SERVER_OPTARG(opt_c, 1);
				cli_servers[n_servers - 1].username = optarg;
				break;

			case 'r': /* Connect using realname */
				CHECK_SERVER_OPTARG(opt_c);
				CHECK_SERVER_OPTARG(opt_c, 1);
				cli_servers[n_servers - 1].realname = optarg;
				break;

			case '4': /* Connect using ipv4 only */
				CHECK_SERVER_OPTARG(opt_c, 0);
				cli_servers[n_servers -1].ipv = IO_IPV_4;
				break;

			case '6': /* Connect using ipv6 only */
				CHECK_SERVER_OPTARG(opt_c, 0);
				cli_servers[n_servers -1].ipv = IO_IPV_6;
				break;

			case 'x': /* Set server TLS disabled */
				CHECK_SERVER_OPTARG(opt_c, 0);
				cli_servers[n_servers -1].tls = IO_TLS_DISABLED;
				break;

			case 'y': /* Set server TLS peer certificate verification mode */
				CHECK_SERVER_OPTARG(opt_c, 1);
				if (!strcmp(optarg, "0") || !strcmp(optarg, "disabled")) {
					cli_servers[n_servers -1].tls_vrfy = IO_TLS_VRFY_DISABLED;
					break;
				}
				if (!strcmp(optarg, "1") || !strcmp(optarg, "optional")) {
					cli_servers[n_servers -1].tls_vrfy = IO_TLS_VRFY_OPTIONAL;
					break;
				}
				if (!strcmp(optarg, "2") || !strcmp(optarg, "required")) {
					cli_servers[n_servers -1].tls_vrfy = IO_TLS_VRFY_REQUIRED;
					break;
				}
				arg_error("option '--tls-verify' mode must be 'disabled', 'optional', or 'required'");

			#undef CHECK_SERVER_OPTARG

			case 'h':
				puts(rirc_usage);
				puts(rirc_help);
				exit(EXIT_SUCCESS);

			case 'v':


@@ 246,6 291,14 @@ parse_args(int argc, char **argv)

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

		uint32_t flags =
			cli_servers[i].ipv |
			cli_servers[i].tls |
			cli_servers[i].tls_vrfy;

		if (cli_servers[i].port == NULL)
			cli_servers[i].port = (cli_servers[i].tls == IO_TLS_ENABLED) ? "6697" : "6667";

		struct server *s = server(
			cli_servers[i].host,
			cli_servers[i].port,


@@ 254,7 307,7 @@ parse_args(int argc, char **argv)
			(cli_servers[i].realname ? cli_servers[i].realname : default_realname)
		);

		s->connection = connection(s, cli_servers[i].host, cli_servers[i].port);
		s->connection = connection(s, cli_servers[i].host, cli_servers[i].port, flags);

		if (server_list_add(state_server_list(), s))
			arg_error("duplicate server: %s:%s", cli_servers[i].host, cli_servers[i].port);


@@ 284,6 337,11 @@ main(int argc, char **argv)
{
	int ret;

	if (argc > 0)
		runtime_name = argv[0];

	srand(time(NULL));

	if ((ret = parse_args(argc, argv)) == 0) {
		io_start();
		draw_term();

M src/state.c => src/state.c +25 -1
@@ 828,7 828,7 @@ command(struct channel *c, char *buf)
				newlinef(s->channel, 0, "-!!-", "already connected to %s:%s", host, port);
			} else {
				s = server(host, port, pass, user, real);
				s->connection = connection(s, host, port);
				s->connection = connection(s, host, port, 0);
				server_list_add(state_server_list(), s);
				channel_set_current(s->channel);
				io_cx(s->connection);


@@ 1059,3 1059,27 @@ io_cb_read_soc(char *buf, size_t len, const void *cb_obj)

	draw(DRAW_FLUSH);
}

void
io_cb_log(const void *cb_obj, enum io_log_level lvl, const char *fmt, ...)
{
	struct server *s = (struct server *)cb_obj;

	va_list ap;
	va_start(ap, fmt);

	switch (lvl) {
		case IO_LOG_ERROR:
			_newline(s->channel, 0, "-!!-", fmt, ap);
			break;
		case IO_LOG_WARN:
		case IO_LOG_INFO:
		case IO_LOG_DEBUG:
			_newline(s->channel, 0, "--", fmt, ap);
			break;
		default:
			fatal("invalid log level");
	}

	va_end(ap);
}

M test/io.mock.c => test/io.mock.c +2 -1
@@ 35,11 35,12 @@ io_sendf(struct connection *c, const char *fmt, ...)
}

struct connection*
connection(const void *o, const char *h, const char *p)
connection(const void *o, const char *h, const char *p, uint32_t f)
{
	UNUSED(o);
	UNUSED(h);
	UNUSED(p);
	UNUSED(f);
	return NULL;
}


D test/io_net.c => test/io_net.c +0 -7
@@ 1,7 0,0 @@
#include <stdlib.h>

int
main(void)
{
	return EXIT_SUCCESS;
}