~rcr/rirc

78453c9fc7d68a41d2f93824c29cd95e2956228a — Richard Robbins 1 year, 1 month ago cfe33d9 + 8c2f704 v0.1.3
Merge branch 'static_analysis' into master
74 files changed, 4527 insertions(+), 2542 deletions(-)

A .builds/alpine.yml
A .builds/debian.yml
M .gitignore
A .gitmodules
D .travis.yml
M CHANGELOG
M LICENSE
M Makefile
M README.md
R config.h => config.def.h
A lib/mbedtls
A lib/mbedtls.h
M rirc.1
A scripts/compile_commands.sh
M scripts/coverage.sh
A scripts/coverity_get.sh
A scripts/coverity_run.sh
A scripts/pre-commit.sh
A scripts/sanitizers_build.sh
A scripts/sanitizers_test.sh
M scripts/stack_usage.sh
M src/components/README.md
M src/components/buffer.c
M src/components/buffer.h
A src/components/ircv3.c
A src/components/ircv3.h
M src/components/mode.c
M src/components/server.c
M src/components/server.h
M src/components/user.c
M src/components/user.h
M src/draw.c
M src/draw.h
M src/handlers/irc_ctcp.c
M src/handlers/irc_recv.c
M src/handlers/irc_recv.gperf
M src/handlers/irc_recv.h
M src/handlers/irc_send.c
M src/handlers/irc_send.gperf
M src/handlers/irc_send.h
A src/handlers/ircv3.c
A src/handlers/ircv3.h
M src/io.c
M src/io.h
M src/rirc.c
M src/rirc.h
M src/state.c
M src/state.h
M src/utils/tree.h
M src/utils/utils.c
M src/utils/utils.h
M test/components/buffer.c
A test/components/ircv3.c
M test/components/server.c
M test/components/user.c
M test/draw.c
D test/draw.c.mock
A test/draw.mock.c
M test/handlers/irc_ctcp.c
M test/handlers/irc_recv.c
R test/handlers/{irc_recv.c.mock => irc_recv.mock.c}
M test/handlers/irc_send.c
R test/handlers/{irc_send.c.mock => irc_send.mock.c}
A test/handlers/ircv3.c
M test/io.c
D test/io.c.mock
A test/io.mock.c
M test/rirc.c
R test/{rirc.c.mock => rirc.mock.c}
M test/state.c
A test/state.mock.c
M test/test.h
M test/utils/tree.c
M test/utils/utils.c
A .builds/alpine.yml => .builds/alpine.yml +18 -0
@@ 0,0 1,18 @@
image: alpine/edge

packages:
  - cmake
  - gperf
  - perl

sources:
  - https://git.sr.ht/~rcr/rirc

tasks:
  - setup: |
      cd rirc
      git submodule init
      git submodule update --recursive
  - build: |
      cd rirc
      make rirc rirc.debug test

A .builds/debian.yml => .builds/debian.yml +57 -0
@@ 0,0 1,57 @@
image: debian/stable

packages:
  - cmake
  - curl
  - gperf
  - perl
  - unzip

sources:
  - https://git.sr.ht/~rcr/rirc

environment:
  SONAR_VER: 4.4.0.2170

secrets:
  - 8c2439c9-5f91-4b19-b3c3-33d82f1b861f
  - a58d0951-f57f-44ef-8ef2-25f2f84f0e89

tasks:
  - setup: |
      cd rirc
      git submodule init
      git submodule update --recursive
  - build: |
      cd rirc
      make rirc rirc.debug test
  - static-analysis: |
      cd rirc
      branch=$(git name-rev --name-only HEAD)
      [ $branch = "remotes/origin/static_analysis" ] || complete-build
      set +x
      source ~/export_coverity
      source ~/export_sonarscan
      set -x
      # Coverity
      ./scripts/coverity_get.sh coverity
      ./scripts/coverity_run.sh coverity
      # Sonarcloud
      curl -o build-wrapper.zip https://sonarcloud.io/static/cpp/build-wrapper-linux-x86.zip
      curl -o sonar-scanner.zip https://binaries.sonarsource.com/Distribution/sonar-scanner-cli/sonar-scanner-cli-$SONAR_VER-linux.zip
      unzip build-wrapper.zip
      unzip sonar-scanner.zip
      ./scripts/coverage.sh
      echo >> sonar-project.properties "sonar.branch.name=$branch"
      echo >> sonar-project.properties "sonar.cfamily.build-wrapper-output=bw-output"
      echo >> sonar-project.properties "sonar.cfamily.cache.enabled=false"
      echo >> sonar-project.properties "sonar.cfamily.gcov.reportsPath=."
      echo >> sonar-project.properties "sonar.cfamily.threads=1"
      echo >> sonar-project.properties "sonar.coverage.exclusions=test"
      echo >> sonar-project.properties "sonar.host.url=https://sonarcloud.io"
      echo >> sonar-project.properties "sonar.organization=rirc"
      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 rirc.debug test
      ./sonar-scanner-$SONAR_VER-linux/bin/sonar-scanner

M .gitignore => .gitignore +5 -0
@@ 5,9 5,14 @@
*.o
*.t
*.td
.clangd
bld
compile_commands.json
config.h
coverage
rirc
rirc.debug
rirc.debug.address
rirc.debug.thread
rirc.out
tags

A .gitmodules => .gitmodules +4 -0
@@ 0,0 1,4 @@
[submodule "mbedtls"]
	path = lib/mbedtls
	url = https://github.com/ARMmbed/mbedtls.git
	ignore = dirty

D .travis.yml => .travis.yml +0 -53
@@ 1,53 0,0 @@
sudo: false
language: c

env:
  global:
    - secure: "NXwB/s0ZpQr0kJTESNxuVCrIVgX2ZsivmiODwK2LDFT1Nchdijd/Ope0cHb92FxURLcDetnl69EWlmLD5ROZQ5Qc+XakAdAEfy2ZrrCq14S9fSZzUtLB0wMW/GZ5hVneSnFmbprR3zQbTbcVvpTyvzDEr26Dd+HQNP0QQsOJ7B5kanLwKeiuNy437pjIOm7x4sjaylnA/b6FlV3a26C7eGN2ZiFCGT++9y3YlJAlxNlySSI+phRe0bihna/RygoYAs5efXefAuibEjtG5kL29cKMKzwEdXEFfDZcl0QYjkrAJqXUUC54YqIWnY56mhDzn0nfWIScQYth0ZS/vhteA8x+MYKFgGnqD9LBw+TsvRO3ZQ1iVrI5NBM6P2Ka6mAVv8OODN2EwWpwoqm2PJ36ZZ+2vJHBwHw10i3pDR48Fzw0x9Av6rkI5lS0hFLMogXtTaM2IlWu4VIevVRBICzrEz47LeOvJs17MZPYeOUhCFrG0ZP/8DR9nqJpmHVFA9FYcaalD/SXfDbBJTI+yV37mgfSM1zJ8tApPy4KOZ1wM2F0+COqEdxHqTJp2u9c1VQo1NegsYxxdB3mOYMLrTnAzQ5WOUdDezcFkAT+QE5WVhGUzpT4lzM1kVirJa2y3beh2i7uHfP2X+EtzJvWRbefJYQJNo8nYXXEwpdJ5v/0LyI="

addons:
  coverity_scan:
    project:
      name: "rcr/rirc"
      description: "Coverity scan of the static_analysis branch"
    notification_email: mail@rcr.io
    build_command_prepend: "make clean"
    build_command: "make test debug rirc -j2"
    branch_pattern: static_analysis

  sonarcloud:
    organization: "rcr-github"

  apt:
    packages: gperf
  homebrew:
    packages: gperf

matrix:
  include:
    - os: linux
      env: CC=gcc

    - os: linux
      env: CC=clang

    - os: osx
      env: CC=clang

before_install:
  - test "${TRAVIS_BRANCH}" != "static_analysis" -o "${TRAVIS_JOB_NUMBER##*.}" = "1" || exit 0

script:
  - set -e
  - 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
  - ./scripts/coverage.sh
  - echo "" > sonar-project.properties
  - echo "sonar.cfamily.build-wrapper-output=bw-output" >> sonar-project.properties
  - echo "sonar.cfamily.gcov.reportsPath=." >> sonar-project.properties
  - echo "sonar.coverage.exclusions=test" >> sonar-project.properties
  - echo "sonar.projectKey=rcr_rirc" >> sonar-project.properties
  - echo "sonar.sources=src" >> sonar-project.properties
  - echo "sonar.tests=test" >> sonar-project.properties
  - sonar-scanner

M CHANGELOG => CHANGELOG +21 -0
@@ 2,8 2,29 @@
Summary of notable changes and features

## Unreleased (dev)

## [0.1.3]
### Features
 - add command :disconnect
 - add cli option `--ipv4`
 - add cli option `--ipv6`
 - add cli option `--tls-disable`
 - add cli option `--tls-verify`
 - add TLS support
    - add mbedtls git submodule
    - add CA_CERT_PATH define to config.h
    - changed default connection type to TLS
    - changed default port to 6697
 - add IRCv3 CAP support
    - add command /cap-ls
    - add command /cap-list
    - add IRCv3 CAP multi-prefix
 - changed standard versions
    - c99          -> c11
    - POSIX.1-2001 -> POSIX.1-2008
### Fixes
 - fix segfault in CTCP extended format handling
 - fix default username/realname not being set

## [0.1.2]
### Features

M LICENSE => LICENSE +1 -1
@@ 1,4 1,4 @@
Copyright (C) 2014-2019 Richard Robbins <mail@rcr.io>
Copyright (C) 2014-2020 Richard Robbins <mail@rcr.io>

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

M Makefile => Makefile +44 -35
@@ 1,26 1,27 @@
.POSIX:

VERSION := 0.1.2
VERSION := 0.1.3

# 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
MAN_DIR = /usr/local/share/man/man1
BIN_DIR := /usr/local/bin
MAN_DIR := /usr/local/share/man/man1

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

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

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) $(STDS) $(TLS_INCL) -I. -DVERSION=\"$(VERSION)\" -Wall -Wextra -pedantic
CFLAGS_R := $(CFLAGS) -O2 -flto -DNDEBUG
CFLAGS_D := $(CFLAGS) -O0 -g
LDFLAGS  := $(LD_EXT) -lpthread

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


@@ 37,32 38,37 @@ SUBDIRS += $(shell find $(DIR_S) -name '*.gperf' -exec dirname {} \; | sort -u)
OBJS_D := $(patsubst $(DIR_S)/%.c, $(DIR_B)/%.db.o, $(SRC))
OBJS_R := $(patsubst $(DIR_S)/%.c, $(DIR_B)/%.o,    $(SRC))
OBJS_T := $(patsubst $(DIR_S)/%.c, $(DIR_B)/%.t,    $(SRC))
OBJS_T += $(DIR_B)/utils/tree.t # Header only file

# Gperf generated source files
OBJS_G := $(patsubst %.gperf, %.gperf.out, $(SRC_G))

# Release build executable
$(EXE_R): $(DIR_B) $(OBJS_G) $(OBJS_R)
$(BIN_R): $(TLS_LIBS) $(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): $(TLS_LIBS) $(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
$(DIR_B)/%.o: $(DIR_S)/%.c config.h
	@echo "cc $<..."
	@$(PP) $(CFLAGS) -MM -MP -MT $@ -MF $(@:.o=.d) $<
	@$(CC) $(CFLAGS) -c -o $@ $<
	@$(PP) $(CFLAGS_R) -MM -MP -MT $@ -MF $(@:.o=.d) $<
	@$(CC) $(CFLAGS_R) -c -o $@ $<

# Debug build objects
$(DIR_B)/%.db.o: $(DIR_S)/%.c
$(DIR_B)/%.db.o: $(DIR_S)/%.c config.h
	@echo "cc $<..."
	@$(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=$@ $<


@@ 71,35 77,38 @@ $(DIR_B)/%.db.o: $(DIR_S)/%.c
$(DIR_B)/%.t: $(DIR_T)/%.c
	@$(PP) $(CFLAGS_D) -MM -MP -MT $@ -MF $(@:.t=.d) $<
	@$(CC) $(CFLAGS_D) $(LDFLAGS) -o $@ $<
	-@./$@ || mv $@ $(@:.t=.td)
	-@rm -f $(@:.t=.td) && $(TEST_EXT) ./$@ || mv $@ $(@:.t=.td)
	@[ ! -f $(@:.t=.td) ]

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

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

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)
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 +79 -53
@@ 5,90 5,116 @@
---

<p align="center">
  <a href="https://sonarcloud.io/dashboard?id=rcr_rirc">
    <img alt="sonarcloud" src="https://sonarcloud.io/api/project_badges/measure?project=rcr_rirc&metric=ncloc"/>
  <a href="https://sonarcloud.io/dashboard?id=rirc">
    <img alt="sonarcloud" src="https://sonarcloud.io/api/project_badges/measure?project=rirc&metric=ncloc"/>
  </a>
  <a href="https://scan.coverity.com/projects/4940">
    <img alt="coverity" src="https://scan.coverity.com/projects/4940/badge.svg"/>
  </a>
  <a href="https://sonarcloud.io/dashboard?id=rcr_rirc">
    <img alt="sonarcloud" src="https://sonarcloud.io/api/project_badges/measure?project=rcr_rirc&metric=sqale_rating"/>
  <a href="https://sonarcloud.io/dashboard?id=rirc">
    <img alt="sonarcloud" src="https://sonarcloud.io/api/project_badges/measure?project=rirc&metric=sqale_rating"/>
  </a>
  <a href="https://sonarcloud.io/dashboard?id=rcr_rirc">
    <img alt="sonarcloud" src="https://sonarcloud.io/api/project_badges/measure?project=rcr_rirc&metric=reliability_rating"/>
  <a href="https://sonarcloud.io/dashboard?id=rirc">
    <img alt="sonarcloud" src="https://sonarcloud.io/api/project_badges/measure?project=rirc&metric=reliability_rating"/>
  </a>
  <a href="https://sonarcloud.io/dashboard?id=rcr_rirc">
    <img alt="sonarcloud" src="https://sonarcloud.io/api/project_badges/measure?project=rcr_rirc&metric=security_rating"/>
  <a href="https://sonarcloud.io/dashboard?id=rirc">
    <img alt="sonarcloud" src="https://sonarcloud.io/api/project_badges/measure?project=rirc&metric=security_rating"/>
  </a>
</p>

---

# 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.
Connections are TLS enabled over port 6697 by default.

### Configuring:

Configure rirc by editing `config.h`. Defaults are in `config.def.h`

### Building:

## Building:
rirc requires the latest version of GNU gperf to compile.

See: https://www.gnu.org/software/gperf/

    make
Build rirc:

Or
```
git submodule init
git submodule update --recursive
make
```

    make debug
### Installing:

## Installing:
Default install path:

    EXE_DIR = /usr/local/bin
    MAN_DIR = /usr/local/share/man/man1
```
BIN_DIR = /usr/local/bin
MAN_DIR = /usr/local/share/man/man1
```

Edit `Makefile` to alter install path if needed, then:

    make install

## Usage:

    rirc [-hv] [-s server [-p port] [-w pass] [-u user] [-r real] [-n nicks] [-c chans]], ...]
    
    Help:
      -h, --help      Print this message and exit
      -v, --version   Print rirc version and exit
    
    Options:
      -s, --server=SERVER       Connect to SERVER
      -p, --port=PORT           Connect to SERVER using PORT
      -w, --pass=PASS           Connect to SERVER using PASS
      -u, --username=USERNAME   Connect to SERVER using USERNAME
      -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
```
make install
```

### Usage:

```
rirc [-hv] [-s server [...]]

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

Server options:
  -s, --server=SERVER       Connect to SERVER
  -p, --port=PORT           Connect to SERVER using PORT
  -w, --pass=PASS           Connect to SERVER using PASS
  -u, --username=USERNAME   Connect to SERVER using USERNAME
  -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:

      :quit
      :clear
      :close
      :connect [host [port] [pass] [user] [real]]
```
  :clear
  :close
  :connect [host [port] [pass] [user] [real]]
  :disconnect
  :quit
```

Keys:

      ^N : go to next channel
      ^P : go to previous channel
      ^L : clear channel
      ^X : close channel
      ^F : find channel
      ^C : cancel input/action
      ^U : scroll buffer up
      ^D : scroll buffer down
       ← : input cursor back
       → : input cursor forward
       ↑ : input history back
       ↓ : input history forward

## More info:
```
  ^N : go to next channel
  ^P : go to previous channel
  ^L : clear channel
  ^X : close channel
  ^F : find channel
  ^C : cancel input/action
  ^U : scroll buffer up
  ^D : scroll buffer down
   ← : input cursor back
   → : input cursor forward
   ↑ : input history back
   ↓ : input history forward
```

### More info:

[rcr.io/rirc/](http://rcr.io/rirc/)

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

/* [NETWORK] */

#define CA_CERT_PATH "/etc/ssl/certs/"

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

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

A lib/mbedtls.h => lib/mbedtls.h +116 -0
@@ 0,0 1,116 @@
#ifndef MBEDTLS_CONFIG_H
#define MBEDTLS_CONFIG_H

/* Enabled ciphersuites, in order of preference.
 *   - Only ECHDE key exchanges, AEAD ciphers
 *   - Ordered by cipher:
 *     - ChaCha
 *     - AES-256-GCM
 *     - AES-256-CCM
 *     - AES-256-CCM_8
 *     - AES-128-GCM
 *     - AES-128-CCM
 *     - AES-128-CCM_8
 *   - Sub-ordered by authentication method:
 *     - ECDSA
 *     - RSA
 * Note: Only stream ciphers were chosen here, which may
 *       reveal the length of exchanged messages.
 */
#define MBEDTLS_SSL_CIPHERSUITES                           \
	/* ChaCha */                                           \
	MBEDTLS_TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256, \
	MBEDTLS_TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256,   \
	/* AES-256 */                                          \
	MBEDTLS_TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,       \
	MBEDTLS_TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,         \
	MBEDTLS_TLS_ECDHE_ECDSA_WITH_AES_256_CCM,              \
	MBEDTLS_TLS_ECDHE_ECDSA_WITH_AES_256_CCM_8,            \
	/* AES-128 */                                          \
	MBEDTLS_TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,       \
	MBEDTLS_TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,         \
	MBEDTLS_TLS_ECDHE_ECDSA_WITH_AES_128_CCM,              \
	MBEDTLS_TLS_ECDHE_ECDSA_WITH_AES_128_CCM_8

/* Supported ECC curves */
#define MBEDTLS_ECP_DP_BP256R1_ENABLED
#define MBEDTLS_ECP_DP_BP384R1_ENABLED
#define MBEDTLS_ECP_DP_BP512R1_ENABLED
#define MBEDTLS_ECP_DP_SECP256K1_ENABLED
#define MBEDTLS_ECP_DP_SECP256R1_ENABLED
#define MBEDTLS_ECP_DP_SECP384R1_ENABLED
#define MBEDTLS_ECP_DP_SECP521R1_ENABLED
#define MBEDTLS_ECP_DP_CURVE25519_ENABLED
#define MBEDTLS_ECP_DP_CURVE448_ENABLED

/* System support */
#define MBEDTLS_DEPRECATED_REMOVED
#define MBEDTLS_HAVE_ASM
#define MBEDTLS_HAVE_TIME
#define MBEDTLS_HAVE_TIME_DATE
#define MBEDTLS_REMOVE_3DES_CIPHERSUITES
#define MBEDTLS_REMOVE_ARC4_CIPHERSUITES
#define MBEDTLS_THREADING_C
#define MBEDTLS_THREADING_PTHREAD

/* Ciphersuite elements */
#define MBEDTLS_CCM_C
#define MBEDTLS_CHACHA20_C
#define MBEDTLS_CHACHAPOLY_C
#define MBEDTLS_GCM_C
#define MBEDTLS_KEY_EXCHANGE_ECDHE_ECDSA_ENABLED
#define MBEDTLS_KEY_EXCHANGE_ECDHE_RSA_ENABLED
#define MBEDTLS_POLY1305_C

/* TLS 1.2 client */
#define MBEDTLS_SSL_CLI_C
#define MBEDTLS_SSL_PROTO_TLS1_2

/* TLS modules */
#define MBEDTLS_AESNI_C
#define MBEDTLS_AES_C
#define MBEDTLS_ASN1_PARSE_C
#define MBEDTLS_ASN1_WRITE_C
#define MBEDTLS_BASE64_C
#define MBEDTLS_BIGNUM_C
#define MBEDTLS_CERTS_C
#define MBEDTLS_CIPHER_C
#define MBEDTLS_CTR_DRBG_C
#define MBEDTLS_ECDH_C
#define MBEDTLS_ECDSA_C
#define MBEDTLS_ECP_C
#define MBEDTLS_ENTROPY_C
#define MBEDTLS_FS_IO
#define MBEDTLS_GENPRIME
#define MBEDTLS_HMAC_DRBG_C
#define MBEDTLS_MD_C
#define MBEDTLS_NET_C
#define MBEDTLS_OID_C
#define MBEDTLS_PEM_PARSE_C
#define MBEDTLS_PKCS1_V15
#define MBEDTLS_PK_C
#define MBEDTLS_PK_PARSE_C
#define MBEDTLS_RSA_C
#define MBEDTLS_SHA1_C
#define MBEDTLS_SHA256_C
#define MBEDTLS_SHA512_C
#define MBEDTLS_SSL_TLS_C
#define MBEDTLS_X509_CRT_PARSE_C
#define MBEDTLS_X509_USE_C

/* TLS extensions */
#define MBEDTLS_SSL_EXTENDED_MASTER_SECRET /* RFC 7627 */
#define MBEDTLS_SSL_SERVER_NAME_INDICATION /* RFC 6066 */

/* Crypto features */
#define MBEDTLS_ECDSA_DETERMINISTIC
#define MBEDTLS_ECP_NIST_OPTIM
#define MBEDTLS_X509_CHECK_EXTENDED_KEY_USAGE
#define MBEDTLS_X509_CHECK_KEY_USAGE

/* Error strings */
#define MBEDTLS_ERROR_C

#include "mbedtls/check_config.h"

#endif

M rirc.1 => rirc.1 +43 -69
@@ 3,76 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
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 .


@@ 93,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


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



@@ 133,21 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.
.SH BUGS
See the
.I TODO
file in the source directory for current known issues.
Additional documentation
.UE .

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

set -e

rm -f compile_commands.json

export CC=clang
export CC_EXT="-Wno-empty-translation-unit"

bear make clean rirc.debug

M scripts/coverage.sh => scripts/coverage.sh +23 -10
@@ 1,15 1,14 @@
#!/bin/sh
#!/bin/bash

set -e

CDIR="coverage"

ENV=""
ENV="$ENV CC=\"gcc\""
ENV="$ENV CC_EXT=\"-fprofile-arcs -ftest-coverage\""
ENV="$ENV LD_EXT=\"-fprofile-arcs\""
export CC=gcc
export CC_EXT="-fprofile-arcs -ftest-coverage"
export LD_EXT="-fprofile-arcs"

eval $ENV make -e clean test
make -e clean test

rm -rf $CDIR
mkdir -p $CDIR


@@ 19,22 18,36 @@ find . -name "*.gcda" -print0 | xargs -0 -I % mv % $CDIR

FILTER=$(cat <<'EOF'
{
	if ($p) {
	if (eof()) {
		$cov = ($lc / $lt) * 100.0;
		printf("~\n");
		printf("~ total %21d/%d %7.2f%%\n", $lc, $lt, $cov);
	} elsif ($p) {
		chomp $file;
		chomp $_;
		$file =~ s/'//g;
		my @s1 = split / /, $file;
		my @s2 = split /:/, $_;
		my @s3 = split / /, $s2[1];
		printf("%-28s%5s: %7s\n", $s1[1], $s3[2], $s3[0]);
		chop($s3[0]);
		printf("%-30s%4s %7s%%\n", $s1[1], $s3[2], $s3[0]);
		$lt = $lt + $s3[2];
		$lc = $lc + $s3[2] * ($s3[0] / 100.0);
		$p = 0;
	}
	$p++ if /^File.*src/;
	$p++ if /^File.*src.*c'/;
	$file = $_;
}
EOF
)

gcov -pr $CDIR/*.gcno | perl -ne "$FILTER" | grep -v 'gperf' | sort
echo "~ Coverage:"

gcov -pr $CDIR/*.gcno | perl -ne "$FILTER" | sort

find . -name "*gperf*.gcov" -print0 | xargs -0 -I % rm %
find . -name "*test#*.gcov" -print0 | xargs -0 -I % rm %

if [ -x "$(command -v gcovr)" ]; then
	gcovr -r . --html --html-details --filter "src.*c$" -o $CDIR/index.html
fi

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

set -e

fail() { >&2 printf "%s\n" "$*"; exit 1; }

if [[ -z $1 ]]; then
	fail "Usage: '$0 dir'"
fi

if [[ -z "${COVERITY_TOKEN}" ]]; then
	fail "missing env COVERITY_TOKEN"
fi

COVERITY_MD5="$1/coverity_tool.md5"
COVERITY_TGZ="$1/coverity_tool.tgz"

mkdir "$1"

echo "curl https://scan.coverity.com/download/linux64 ..."

curl -fs --show-error https://scan.coverity.com/download/linux64 -o "$COVERITY_TGZ" --data "token=$COVERITY_TOKEN&project=rcr%2Frirc"
curl -fs --show-error https://scan.coverity.com/download/linux64 -o "$COVERITY_MD5" --data "token=$COVERITY_TOKEN&project=rcr%2Frirc&md5=1"

printf "%s\t$COVERITY_TGZ" "$(cat "$COVERITY_MD5")" | md5sum -c -

tar xzf "$COVERITY_TGZ" -C "$1" --strip-components 1

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

set -e

fail() { >&2 printf "%s\n" "$*"; exit 1; }

if [[ -z $1 ]]; then
	fail "Usage: '$0 dir'"
fi

if [[ -z "${COVERITY_EMAIL}" ]]; then
	fail "missing env COVERITY_EMAIL"
fi

if [[ -z "${COVERITY_TOKEN}" ]]; then
	fail "missing env COVERITY_TOKEN"
fi

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 rirc.debug test

tar czf "$COVERITY_TAR" "$COVERITY_OUT"

curl \
	--form file=@"$COVERITY_TAR" \
	--form email="$COVERITY_EMAIL" \
	--form token="$COVERITY_TOKEN" \
	--form version="$VERSION" \
	https://scan.coverity.com/builds?project=rcr%2Frirc

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

echo "Running pre-commit hook..."

RESULTS=$(make test)

if [[ "$RESULTS" == *"failure"* ]];
then
	echo -e "$RESULTS"
	exit 1
fi

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 +5 -6
@@ 1,11 1,10 @@
#!/bin/sh
#!/bin/bash

set -e

ENV=""
ENV="$ENV CC=\"gcc\""
ENV="$ENV CC_EXT=\"-fstack-usage\""
export CC=gcc
export CC_EXT="-fstack-usage"

eval $ENV make -e clean debug
make -e clean rirc.debug

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

M src/components/README.md => src/components/README.md +4 -0
@@ 34,6 34,10 @@ Stateful component hierarchy
       |
       |__connection
       |
       |__ircv3_caps
       |   |
       |   |__*ircv3_cap
       |
       |__mode
       |__mode_str
       |__mode_cfg

M src/components/buffer.c => src/components/buffer.c +28 -0
@@ 187,3 187,31 @@ buffer(struct buffer *b)

	memset(b, 0, sizeof(*b));
}

void
buffer_line_split(
	struct buffer_line *line,
	unsigned *head_w,
	unsigned *text_w,
	unsigned cols,
	unsigned pad)
{
	unsigned _head_w = sizeof(" HH:MM   "VERTICAL_SEPARATOR" ");

	if (BUFFER_PADDING)
		_head_w += pad;
	else
		_head_w += line->from_len;

	/* If header won't fit, split in half */
	if (_head_w >= cols)
		_head_w = cols / 2;

	_head_w -= 1;

	if (head_w)
		*head_w = _head_w;

	if (text_w)
		*text_w = cols - _head_w + 1;
}

M src/components/buffer.h => src/components/buffer.h +8 -0
@@ 76,4 76,12 @@ void buffer_newline(
	size_t,
	char);

void
buffer_line_split(
	struct buffer_line *line,
	unsigned *head_w,
	unsigned *text_w,
	unsigned cols,
	unsigned pad);

#endif

A src/components/ircv3.c => src/components/ircv3.c +40 -0
@@ 0,0 1,40 @@
#include "src/components/ircv3.h"

#include <string.h>

struct ircv3_cap*
ircv3_cap_get(struct ircv3_caps *caps, const char *cap_str)
{
	#define X(CAP, VAR, ATTRS) \
	if (!strcmp(cap_str, CAP)) \
		return &(caps->VAR);
	IRCV3_CAPS
	#undef X

	return NULL;
}

void
ircv3_caps(struct ircv3_caps *caps)
{
	#define X(CAP, VAR, ATTRS) \
	caps->VAR.req = 0;                                    \
	caps->VAR.set = 0;                                    \
	caps->VAR.supported = 0;                              \
	caps->VAR.supports_del = !(ATTRS & IRCV3_CAP_NO_DEL); \
	caps->VAR.supports_req = !(ATTRS & IRCV3_CAP_NO_REQ); \
	caps->VAR.req_auto = (ATTRS & IRCV3_CAP_AUTO);
	IRCV3_CAPS
	#undef X
}

void
ircv3_caps_reset(struct ircv3_caps *caps)
{
	#define X(CAP, VAR, ATTRS) \
	caps->VAR.req = 0; \
	caps->VAR.set = 0; \
	caps->VAR.supported = 0;
	IRCV3_CAPS
	#undef X
}

A src/components/ircv3.h => src/components/ircv3.h +44 -0
@@ 0,0 1,44 @@
#ifndef IRCV3_CAP_H
#define IRCV3_CAP_H

#define IRCV3_CAP_AUTO   (1 << 0)
#define IRCV3_CAP_NO_DEL (1 << 1)
#define IRCV3_CAP_NO_REQ (1 << 2)
#define IRCV3_CAP_VERSION "302"

#define IRCV3_CAPS_DEF \
	X("multi-prefix", multi_prefix, IRCV3_CAP_AUTO)

/* Extended by testcases */
#ifndef IRCV3_CAPS_TEST
#define IRCV3_CAPS_TEST
#endif

#define IRCV3_CAPS \
	IRCV3_CAPS_DEF \
	IRCV3_CAPS_TEST

struct ircv3_cap
{
	unsigned req          : 1; /* cap REQ sent */
	unsigned req_auto     : 1; /* cap REQ sent during registration */
	unsigned set          : 1; /* cap is unset/set */
	unsigned supported    : 1; /* cap is supported by server */
	unsigned supports_del : 1; /* cap supports CAP DEL */
	unsigned supports_req : 1; /* cap supports CAP REQ */
};

struct ircv3_caps
{
	#define X(CAP, VAR, ATTRS) \
	struct ircv3_cap VAR;
	IRCV3_CAPS
	#undef X
};

struct ircv3_cap* ircv3_cap_get(struct ircv3_caps*, const char*);

void ircv3_caps(struct ircv3_caps*);
void ircv3_caps_reset(struct ircv3_caps*);

#endif

M src/components/mode.c => src/components/mode.c +12 -0
@@ 396,6 396,18 @@ mode_prfxmode_prefix(struct mode *m, const struct mode_cfg *cfg, int flag)
	else
		MODE_SET(m->upper, bit, MODE_SET_ON);

	f = cfg->PREFIX.F,
	t = cfg->PREFIX.T;

	while (*f) {

		if (mode_isset(m, *f))
			break;

		f++;
		t++;
	}

	m->prefix = *t;

	return MODE_ERR_NONE;

M src/components/server.c => src/components/server.c +55 -48
@@ 19,7 19,7 @@ struct opt
	char *val;
};

static int parse_opt(struct opt*, char**);
static int parse_005(struct opt*, char**);
static int server_cmp(const struct server*, const char*, const char*);

#define X(cmd) static int server_set_##cmd(struct server*, char*);


@@ 42,6 42,7 @@ server(const char *host, const char *port, const char *pass, const char *user, c
	s->channel = channel(host, CHANNEL_T_SERVER);
	s->casemapping = CASEMAPPING_RFC1459;
	s->mode_str.type = MODE_STR_USERMODE;
	ircv3_caps(&(s->ircv3_caps));
	mode_cfg(&(s->mode_cfg), NULL, MODE_CFG_DEFAULTS);
	/* FIXME: remove server pointer from channel, remove
	 * server's channel from clist */


@@ 130,9 131,11 @@ server_list_del(struct server_list *sl, struct server *s)
void
server_reset(struct server *s)
{
	ircv3_caps_reset(&(s->ircv3_caps));
	mode_reset(&(s->usermodes), &(s->mode_str));
	s->ping = 0;
	s->quitting = 0;
	s->registered = 0;
	s->nicks.next = 0;
}



@@ 163,40 166,35 @@ server_set_004(struct server *s, char *str)
{
	/* <server_name> <version> <user_modes> <chan_modes> */

	struct channel *c = s->channel;
	const char *server_name; /* Not used */
	const char *version;     /* Not used */
	const char *user_modes;  /* Configure server usermodes */
	const char *chan_modes;  /* Configure server chanmodes */

	char *saveptr;
	char *server_name, /* Not used */
	     *version,     /* Not used */
	     *user_modes,  /* Configure server usermodes */
	     *chan_modes;  /* Configure server chanmodes */
	if (!(server_name = strsep(&str)))
		server_error(s, "invalid numeric 004: server_name is null");

	if (!(server_name = strtok_r(str, " ", &saveptr)))
		newline(c, 0, "-!!-", "invalid numeric 004: server_name is null");
	if (!(version = strsep(&str)))
		server_error(s, "invalid numeric 004: version is null");

	if (!(version = strtok_r(NULL, " ", &saveptr)))
		newline(c, 0, "-!!-", "invalid numeric 004: version is null");
	if (!(user_modes = strsep(&str)))
		server_error(s, "invalid numeric 004: user_modes is null");

	if (!(user_modes = strtok_r(NULL, " ", &saveptr)))
		newline(c, 0, "-!!-", "invalid numeric 004: user_modes is null");

	if (!(chan_modes = strtok_r(NULL, " ", &saveptr)))
		newline(c, 0, "-!!-", "invalid numeric 004: chan_modes is null");
	if (!(chan_modes = strsep(&str)))
		server_error(s, "invalid numeric 004: chan_modes is null");

	if (user_modes) {

		debug("Setting numeric 004 user_modes: %s", user_modes);

		if (mode_cfg(&(s->mode_cfg), user_modes, MODE_CFG_USERMODES) != MODE_ERR_NONE)
			newlinef(c, 0, "-!!-", "invalid numeric 004 user_modes: %s", user_modes);
		if (mode_cfg(&(s->mode_cfg), user_modes, MODE_CFG_USERMODES) == MODE_ERR_NONE)
			debug("Setting numeric 004 user_modes: %s", user_modes);
		else
			server_error(s, "invalid numeric 004 user_modes: %s", user_modes);
	}

	if (chan_modes) {

		debug("Setting numeric 004 chan_modes: %s", chan_modes);

		if (mode_cfg(&(s->mode_cfg), chan_modes, MODE_CFG_CHANMODES) != MODE_ERR_NONE)
			newlinef(c, 0, "-!!-", "invalid numeric 004 chan_modes: %s", chan_modes);
		if (mode_cfg(&(s->mode_cfg), chan_modes, MODE_CFG_CHANMODES) == MODE_ERR_NONE)
			debug("Setting numeric 004 chan_modes: %s", chan_modes);
		else
			server_error(s, "invalid numeric 004 chan_modes: %s", chan_modes);
	}
}



@@ 207,12 205,25 @@ server_set_005(struct server *s, char *str)

	struct opt opt;

	while (parse_opt(&opt, &str)) {
	while (parse_005(&opt, &str)) {

		int (*server_set)(struct server*, char*) = NULL;

		#define X(cmd) \
		if (!strcmp(opt.arg, #cmd) && server_set_##cmd(s, opt.val)) \
			newlinef(s->channel, 0, "-!!-", "invalid %s: %s", #cmd, opt.val);
		if (!strcmp(opt.arg, #cmd)) \
			server_set = server_set_##cmd;
		HANDLED_005
		#undef X

		if (server_set) {
			if (opt.val == NULL) {
				server_error(s, "invalid numeric 005 %s: value is NULL", opt.arg);
			} else if ((*server_set)(s, opt.val)) {
				server_error(s, "invalid numeric 005 %s: %s", opt.arg, opt.val);
			} else {
				debug("Setting numeric 005 %s: %s", opt.arg, opt.val);
			}
		}
	}
}



@@ 272,7 283,7 @@ server_cmp(const struct server *s, const char *host, const char *port)
}

static int
parse_opt(struct opt *opt, char **str)
parse_005(struct opt *opt, char **str)
{
	/* Parse a single argument from numeric 005 (ISUPPORT)
	 *


@@ 316,7 327,7 @@ parse_opt(struct opt *opt, char **str)
	opt->arg = NULL;
	opt->val = NULL;

	if (!str_trim(&p))
	if (!strtrim(&p))
		return 0;

	if (!isalnum(*p))


@@ 344,18 355,20 @@ parse_opt(struct opt *opt, char **str)
static int
server_set_CASEMAPPING(struct server *s, char *val)
{
	if (val == NULL)
		return 0;
	else if (!strcmp(val, "ascii"))
	if (!strcmp(val, "ascii")) {
		s->casemapping = CASEMAPPING_ASCII;
	else if (!strcmp(val, "rfc1459"))
		return 0;
	}

	if (!strcmp(val, "rfc1459")) {
		s->casemapping = CASEMAPPING_RFC1459;
	else if (!strcmp(val, "strict-rfc1459"))
		s->casemapping = CASEMAPPING_STRICT_RFC1459;
	else
		return 0;
	}

	debug("Setting numeric 005 CASEMAPPING: %s", val);
	if (!strcmp(val, "strict-rfc1459")) {
		s->casemapping = CASEMAPPING_STRICT_RFC1459;
		return 0;
	}

	return 1;
}


@@ 363,25 376,19 @@ server_set_CASEMAPPING(struct server *s, char *val)
static int
server_set_CHANMODES(struct server *s, char *val)
{
	debug("Setting numeric 005 CHANMODES: %s", val);

	return (mode_cfg(&(s->mode_cfg), val, MODE_CFG_SUBTYPES) != MODE_ERR_NONE);
	return mode_cfg(&(s->mode_cfg), val, MODE_CFG_SUBTYPES) != MODE_ERR_NONE;
}

static int
server_set_MODES(struct server *s, char *val)
{
	debug("Setting numeric 005 MODES: %s", val);

	return (mode_cfg(&(s->mode_cfg), val, MODE_CFG_MODES) != MODE_ERR_NONE);
	return mode_cfg(&(s->mode_cfg), val, MODE_CFG_MODES) != MODE_ERR_NONE;
}

static int
server_set_PREFIX(struct server *s, char *val)
{
	debug("Setting numeric 005 PREFIX: %s", val);

	return (mode_cfg(&(s->mode_cfg), val, MODE_CFG_PREFIX) != MODE_ERR_NONE);
	return mode_cfg(&(s->mode_cfg), val, MODE_CFG_PREFIX) != MODE_ERR_NONE;
}

void

M src/components/server.h => src/components/server.h +12 -1
@@ 3,8 3,11 @@

#include "src/components/buffer.h"
#include "src/components/channel.h"
#include "src/components/ircv3.h"
#include "src/components/mode.h"
#include "src/utils/utils.h"

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

struct server
{


@@ 24,6 27,7 @@ struct server
	struct channel *channel;
	struct channel_list clist;
	struct channel_list ulist; // TODO: seperate privmsg
	struct ircv3_caps ircv3_caps;
	struct mode usermodes;
	struct mode_str mode_str;
	struct mode_cfg mode_cfg;


@@ 32,7 36,14 @@ struct server
	struct user_list ignore;
	unsigned ping;
	unsigned quitting : 1;
	unsigned registered : 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/components/user.c => src/components/user.c +8 -8
@@ 15,13 15,13 @@ AVL_GENERATE(user_list, user, ul, user_cmp, user_ncmp)
static inline int
user_cmp(struct user *u1, struct user *u2, void *arg)
{
	return irc_strcmp((enum casemapping_t)arg, u1->nick, u2->nick);
	return irc_strcmp(*(enum casemapping_t*)arg, u1->nick, u2->nick);
}

static inline int
user_ncmp(struct user *u1, struct user *u2, void *arg, size_t n)
{
	return irc_strncmp((enum casemapping_t)arg, u1->nick, u2->nick, n);
	return irc_strncmp(*(enum casemapping_t*)arg, u1->nick, u2->nick, n);
}

static inline void


@@ 54,7 54,7 @@ user_list_add(struct user_list *ul, enum casemapping_t cm, const char *nick, str
	if (user_list_get(ul, cm, nick, 0) != NULL)
		return USER_ERR_DUPLICATE;

	AVL_ADD(user_list, ul, user(nick, prfxmodes), (void*)cm);
	AVL_ADD(user_list, ul, user(nick, prfxmodes), &cm);
	ul->count++;

	return USER_ERR_NONE;


@@ 70,7 70,7 @@ user_list_del(struct user_list *ul, enum casemapping_t cm, const char *nick)
	if ((u = user_list_get(ul, cm, nick, 0)) == NULL)
		return USER_ERR_NOT_FOUND;

	AVL_DEL(user_list, ul, u, (void*)cm);
	AVL_DEL(user_list, ul, u, &cm);
	ul->count--;

	user_free(u);


@@ 93,8 93,8 @@ user_list_rpl(struct user_list *ul, enum casemapping_t cm, const char *nick_old,

	new = user(nick_new, old->prfxmodes);

	AVL_ADD(user_list, ul, new, (void*)cm);
	AVL_DEL(user_list, ul, old, (void*)cm);
	AVL_ADD(user_list, ul, new, &cm);
	AVL_DEL(user_list, ul, old, &cm);

	user_free(old);



@@ 107,9 107,9 @@ user_list_get(struct user_list *ul, enum casemapping_t cm, const char *nick, siz
	struct user u2 = { .nick = nick };

	if (prefix_len == 0)
		return AVL_GET(user_list, ul, &u2, (void*)cm);
		return AVL_GET(user_list, ul, &u2, &cm);
	else
		return AVL_NGET(user_list, ul, &u2, (void*)cm, prefix_len);
		return AVL_NGET(user_list, ul, &u2, &cm, prefix_len);
}

void

M src/components/user.h => src/components/user.h +2 -2
@@ 14,7 14,7 @@ enum user_err

struct user
{
	AVL_NODE(user) ul;
	TREE_NODE(user) ul;
	const char *nick;
	size_t nick_len;
	struct mode prfxmodes;


@@ 23,7 23,7 @@ struct user

struct user_list
{
	AVL_HEAD(user);
	TREE_HEAD(user);
	unsigned int count;
};


M src/draw.c => src/draw.c +302 -284
@@ 11,8 11,9 @@
#include <string.h>

#include "config.h"
#include "src/components/input.h"
#include "src/components/channel.h"
#include "src/components/input.h"
#include "src/draw.h"
#include "src/io.h"
#include "src/state.h"
#include "src/utils/utils.h"


@@ 48,10 49,9 @@
#error "BUFFER_PADDING options are 0 (no pad), 1 (padded)"
#endif

static int actv_colours[ACTIVITY_T_SIZE] = ACTIVITY_COLOURS
static int nick_colours[] = NICK_COLOURS

/* Terminal coordinate row/column boundaries (inclusive) for objects being drawn
/* Terminal coordinate row/column boundaries (inclusive)
 * for objects being drawn. The origin for terminal
 * coordinates is in the top left, indexed from 1
 *
 *   \ c0     cN
 *    +---------+


@@ 60,106 60,274 @@ static int nick_colours[] = NICK_COLOURS
 *    |         |
 *  rN|         |
 *    +---------+
 *
 * The origin for terminal coordinates is in the top left, indexed from 1
 *
 */

struct coords
{
	unsigned int c1;
	unsigned int cN;
	unsigned int r1;
	unsigned int rN;
	unsigned c0;
	unsigned cN;
	unsigned r0;
	unsigned rN;
};

struct draw_state
{
	union {
		struct {
			unsigned buffer : 1;
			unsigned input  : 1;
			unsigned nav    : 1;
			unsigned status : 1;
		};
		unsigned all;
	} bits;
	unsigned bell : 1;
};

static int _draw_fmt(char**, size_t*, size_t*, int, const char*, ...);
static void draw_bits(void);
static void draw_buffer(struct buffer*, struct coords);
static void draw_buffer_line(struct buffer_line*, struct coords, unsigned, unsigned, unsigned, unsigned);
static void draw_input(struct input*, struct coords);
static void draw_nav(struct channel*);
static void draw_status(struct channel*);

static void _draw_buffer_line(struct buffer_line*, struct coords, unsigned int, unsigned int, unsigned int, unsigned int);
static void _draw_buffer(struct buffer*, struct coords);
static void _draw_input(struct input*, struct coords);
static void _draw_nav(struct channel*);
static void _draw_status(struct channel*);
static char* draw_colour(int, int);
static int draw_fmt(char**, size_t*, size_t*, int, const char*, ...);
static unsigned nick_col(char*);
static void check_coords(struct coords);

static inline unsigned int nick_col(char*);
static inline void check_coords(struct coords);
static int actv_colours[ACTIVITY_T_SIZE] = ACTIVITY_COLOURS
static int nick_colours[] = NICK_COLOURS
static struct draw_state draw_state;

void
draw(enum draw_bit bit)
{
	switch (bit) {
		case DRAW_FLUSH:
			draw_bits();
			draw_state.bits.all = 0;
			draw_state.bell = 0;
			break;
		case DRAW_BELL:
			draw_state.bell = 1;
			break;
		case DRAW_BUFFER:
			draw_state.bits.buffer = 1;
			break;
		case DRAW_INPUT:
			draw_state.bits.input = 1;
			break;
		case DRAW_NAV:
			draw_state.bits.nav = 1;
			break;
		case DRAW_STATUS:
			draw_state.bits.status = 1;
			break;
		case DRAW_ALL:
			draw_state.bits.all = -1;
			break;
		default:
			fatal("unknown draw bit");
	}
}

static char* _colour(int, int);
void
draw_init(void)
{
	draw(DRAW_ALL);
	draw(DRAW_FLUSH);
}

void
draw(union draw draw)
draw_term(void)
{
	if (!draw.all_bits)
	printf(RESET_ATTRIBUTES);
	printf(CLEAR_FULL);
}

static void
draw_bits(void)
{
	if (draw_state.bell && BELL_ON_PINGED)
		putchar('\a');

	if (!draw_state.bits.all)
		return;

	struct coords coords;
	struct channel *c = current_channel();

	if (io_tty_cols() < COLS_MIN || io_tty_rows() < ROWS_MIN) {
		printf(CLEAR_FULL MOVE(1, 1) "rirc");
		goto no_draw;
		fflush(stdout);
		return;
	}

	printf(CURSOR_SAVE);

	if (draw.bits.buffer) _draw_buffer(&c->buffer,
		(struct coords) {
			.c1 = 1,
			.cN = io_tty_cols(),
			.r1 = 3,
			.rN = io_tty_rows() - 2
		});
	if (draw_state.bits.buffer) {
		coords.c0 = 1;
		coords.cN = io_tty_cols();
		coords.r0 = 3;
		coords.rN = io_tty_rows() - 2;
		draw_buffer(&c->buffer, coords);
	}

	if (draw.bits.nav)    _draw_nav(c);
	if (draw_state.bits.input) {
		coords.c0 = 1;
		coords.cN = io_tty_cols();
		coords.r0 = io_tty_rows();
		coords.rN = io_tty_rows();
		draw_input(&c->input, coords);
	}

	if (draw.bits.input)  _draw_input(&c->input,
		(struct coords) {
			.c1 = 1,
			.cN = io_tty_cols(),
			.r1 = io_tty_rows(),
			.rN = io_tty_rows()
		});
	if (draw_state.bits.nav)
		draw_nav(c);

	if (draw.bits.status) _draw_status(c);
	if (draw_state.bits.status)
		draw_status(c);

	printf(RESET_ATTRIBUTES);
	printf(CURSOR_RESTORE);

no_draw:

	fflush(stdout);
}

void
draw_bell(void)
static void
draw_buffer(struct buffer *b, struct coords coords)
{
	if (BELL_ON_PINGED)
		putchar('\a');
}
	/* Dynamically draw the current channel's buffer such that:
	 *
	 * - The scrollback line should always be drawn in full when possible
	 * - Lines wrap on whitespace when possible
	 * - The top-most lines draws partially when required
	 * - Buffers requiring fewer rows than available draw from the top down
	 *
	 * Rows are numbered from the top down, 1 to term_rows, so for term_rows = N,
	 * the drawable area for the buffer is bounded [r3, rN-2]:
	 *      __________________________
	 * r0   |         (nav)          |
	 * r2   |------------------------|
	 * r3   |    ::buffer start::    |
	 *      |                        |
	 * ...  |                        |
	 *      |                        |
	 * rN-2 |     ::buffer end::     |
	 * rN-1 |------------------------|
	 * rN   |________(input)_________|
	 *
	 *
	 * So the general steps for drawing are:
	 *
	 * 1. Starting from line L = scrollback, traverse backwards through the
	 *    buffer summing the rows required to draw lines, until the sum
	 *    exceeds the number of rows available
	 *
	 * 2. L now points to the top-most line to be drawn. L might not be able
	 *    to draw in full, so discard the excessive word-wrapped segments and
	 *    draw the remainder
	 *
	 * 3. Traverse forward through the buffer, drawing lines until buffer.head
	 *    is encountered
	 */

void
draw_init(void)
{
	draw_all();
	redraw();
}
	check_coords(coords);

void
draw_term(void)
{
	printf(RESET_ATTRIBUTES);
	printf(CLEAR_FULL);
	unsigned row,
	         row_count = 0,
	         row_total = coords.rN - coords.r0 + 1;

	unsigned col_total = coords.cN - coords.c0 + 1;

	unsigned buffer_i = b->scrollback,
	         head_w,
	         text_w;

	/* Clear the buffer area */
	for (row = coords.r0; row <= coords.rN; row++)
		printf(MOVE(%d, 1) CLEAR_LINE, row);

	struct buffer_line *line = buffer_line(b, buffer_i);

	if (line == NULL)
		return;

	struct buffer_line *tail = buffer_tail(b);
	struct buffer_line *head = buffer_head(b);

	/* Find top line */
	for (;;) {

		buffer_line_split(line, NULL, &text_w, col_total, b->pad);

		row_count += buffer_line_rows(line, text_w);

		if (line == tail)
			break;

		if (row_count >= row_total)
			break;

		line = buffer_line(b, --buffer_i);
	}

	/* Handle impartial top line print */
	if (row_count > row_total) {

		buffer_line_split(line, &head_w, &text_w, col_total, b->pad);

		draw_buffer_line(
			line,
			coords,
			head_w,
			text_w,
			row_count - row_total,
			BUFFER_PADDING ? (b->pad - line->from_len) : 0
		);

		coords.r0 += buffer_line_rows(line, text_w) - (row_count - row_total);

		if (line == head)
			return;

		line = buffer_line(b, ++buffer_i);
	}

	/* Draw all remaining lines */
	while (coords.r0 <= coords.rN) {

		buffer_line_split(line, &head_w, &text_w, col_total, b->pad);

		draw_buffer_line(
			line,
			coords,
			head_w,
			text_w,
			0,
			BUFFER_PADDING ? (b->pad - line->from_len) : 0
		);

		coords.r0 += buffer_line_rows(line, text_w);

		if (line == head)
			return;

		line = buffer_line(b, ++buffer_i);
	}
}

/* FIXME: works except when it doesn't.
 *
 * Fails when line headers are very long compared to text. tests/draw.c needed */
static void
_draw_buffer_line(
draw_buffer_line(
		struct buffer_line *line,
		struct coords coords,
		unsigned int head_w,
		unsigned int text_w,
		unsigned int skip,
		unsigned int pad)
		unsigned head_w,
		unsigned text_w,
		unsigned skip,
		unsigned pad)
{
	check_coords(coords);



@@ 189,19 357,19 @@ _draw_buffer_line(

		struct tm *line_tm = localtime(&line->time);

		if (!_draw_fmt(&header_ptr, &buff_n, &text_n, 0,
				_colour(BUFFER_LINE_HEADER_FG_NEUTRAL, -1)))
		if (!draw_fmt(&header_ptr, &buff_n, &text_n, 0,
				draw_colour(BUFFER_LINE_HEADER_FG_NEUTRAL, -1)))
			goto print_header;

		if (!_draw_fmt(&header_ptr, &buff_n, &text_n, 1,
		if (!draw_fmt(&header_ptr, &buff_n, &text_n, 1,
				" %02d:%02d ", line_tm->tm_hour, line_tm->tm_min))
			goto print_header;

		if (!_draw_fmt(&header_ptr, &buff_n, &text_n, 1,
		if (!draw_fmt(&header_ptr, &buff_n, &text_n, 1,
				"%*s", pad, ""))
			goto print_header;

		if (!_draw_fmt(&header_ptr, &buff_n, &text_n, 0, RESET_ATTRIBUTES))
		if (!draw_fmt(&header_ptr, &buff_n, &text_n, 0, RESET_ATTRIBUTES))
			goto print_header;

		switch (line->type) {


@@ 212,20 380,20 @@ _draw_buffer_line(
			case BUFFER_LINE_NICK:
			case BUFFER_LINE_PART:
			case BUFFER_LINE_QUIT:
				if (!_draw_fmt(&header_ptr, &buff_n, &text_n, 0,
						_colour(BUFFER_LINE_HEADER_FG_NEUTRAL, -1)))
				if (!draw_fmt(&header_ptr, &buff_n, &text_n, 0,
						draw_colour(BUFFER_LINE_HEADER_FG_NEUTRAL, -1)))
					goto print_header;
				break;

			case BUFFER_LINE_CHAT:
				if (!_draw_fmt(&header_ptr, &buff_n, &text_n, 0,
						_colour(line->cached.colour, -1)))
				if (!draw_fmt(&header_ptr, &buff_n, &text_n, 0,
						draw_colour(line->cached.colour, -1)))
					goto print_header;
				break;

			case BUFFER_LINE_PINGED:
				if (!_draw_fmt(&header_ptr, &buff_n, &text_n, 0,
						_colour(BUFFER_LINE_HEADER_FG_PINGED, BUFFER_LINE_HEADER_BG_PINGED)))
				if (!draw_fmt(&header_ptr, &buff_n, &text_n, 0,
						draw_colour(BUFFER_LINE_HEADER_FG_PINGED, BUFFER_LINE_HEADER_BG_PINGED)))
					goto print_header;
				break;



@@ 233,13 401,13 @@ _draw_buffer_line(
				fatal("Invalid line type");
		}

		if (!_draw_fmt(&header_ptr, &buff_n, &text_n, 1,
		if (!draw_fmt(&header_ptr, &buff_n, &text_n, 1,
				"%s", line->from))
			goto print_header;

print_header:
		/* Print the line header */
		printf(MOVE(%d, 1) "%s " RESET_ATTRIBUTES, coords.r1, header);
		printf(MOVE(%d, 1) "%s " RESET_ATTRIBUTES, coords.r0, header);
	}

	while (skip--)


@@ 248,19 416,19 @@ print_header:
	do {
		char *sep = " "VERTICAL_SEPARATOR" ";

		if ((coords.cN - coords.c1) >= sizeof(*sep) + text_w) {
			printf(MOVE(%d, %d), coords.r1, (int)(coords.cN - (sizeof(*sep) + text_w + 1)));
			fputs(_colour(BUFFER_LINE_HEADER_FG_NEUTRAL, -1), stdout);
		if ((coords.cN - coords.c0) >= sizeof(*sep) + text_w) {
			printf(MOVE(%d, %d), coords.r0, (int)(coords.cN - (sizeof(*sep) + text_w + 1)));
			fputs(draw_colour(BUFFER_LINE_HEADER_FG_NEUTRAL, -1), stdout);
			fputs(sep, stdout);
		}

		if (*p1) {
			printf(MOVE(%d, %d), coords.r1, head_w);
			printf(MOVE(%d, %d), coords.r0, head_w);

			print_p1 = p1;
			print_p2 = word_wrap(text_w, &p1, p2);

			fputs(_colour(line->text[0] == QUOTE_CHAR
			fputs(draw_colour(line->text[0] == QUOTE_CHAR
					? BUFFER_LINE_TEXT_FG_GREEN
					: BUFFER_LINE_TEXT_FG_NEUTRAL,
					-1),


@@ 269,132 437,71 @@ print_header:
			printf("%.*s", (int)(print_p2 - print_p1), print_p1);
		}

		coords.r1++;
		coords.r0++;

	} while (*p1 && coords.r1 <= coords.rN);
	} while (*p1 && coords.r0 <= coords.rN);
}

static void
_draw_buffer(struct buffer *b, struct coords coords)
draw_input(struct input *inp, struct coords coords)
{
	/* Dynamically draw the current channel's buffer such that:
	 *
	 * - The scrollback line should always be drawn in full when possible
	 * - Lines wrap on whitespace when possible
	 * - The top-most lines draws partially when required
	 * - Buffers requiring fewer rows than available draw from the top down
	 *
	 * Rows are numbered from the top down, 1 to term_rows, so for term_rows = N,
	 * the drawable area for the buffer is bounded [r3, rN-2]:
	 *      __________________________
	 * r1   |         (nav)          |
	 * r2   |------------------------|
	 * r3   |    ::buffer start::    |
	 *      |                        |
	 * ...  |                        |
	 *      |                        |
	 * rN-2 |     ::buffer end::     |
	 * rN-1 |------------------------|
	 * rN   |________(input)_________|
	 *
	 *
	 * So the general steps for drawing are:
	 *
	 * 1. Starting from line L = scrollback, traverse backwards through the
	 *    buffer summing the rows required to draw lines, until the sum
	 *    exceeds the number of rows available
	 *
	 * 2. L now points to the top-most line to be drawn. L might not be able
	 *    to draw in full, so discard the excessive word-wrapped segments and
	 *    draw the remainder
	 *
	 * 3. Traverse forward through the buffer, drawing lines until buffer.head
	 *    is encountered
	 */
	/* Draw the input line, or the current action message */

	check_coords(coords);

	unsigned int row,
	             row_count = 0,
	             row_total = coords.rN - coords.r1 + 1;

	unsigned int col_total = coords.cN - coords.c1 + 1;
	unsigned cols_t = coords.cN - coords.c0 + 1,
	         cursor = coords.c0;

	unsigned int buffer_i = b->scrollback,
	             head_w,
	             text_w;

	/* Clear the buffer area */
	for (row = coords.r1; row <= coords.rN; row++)
		printf(MOVE(%d, 1) CLEAR_LINE, row);

	struct buffer_line *line = buffer_line(b, buffer_i);
	printf(RESET_ATTRIBUTES);
	printf(MOVE(%d, 1) CLEAR_LINE, coords.rN);
	printf(CURSOR_SAVE);

	if (line == NULL)
	/* Insufficient columns for meaningful input drawing */
	if (cols_t < 3)
		return;

	struct buffer_line *tail = buffer_tail(b);
	struct buffer_line *head = buffer_head(b);

	/* Find top line */
	for (;;) {
	char input[cols_t + COLOUR_SIZE * 2 + 1];
	char *input_ptr = input;

		split_buffer_cols(line, NULL, &text_w, col_total, b->pad);
	size_t buff_n = sizeof(input) - 1,
	       text_n = cols_t;

		row_count += buffer_line_rows(line, text_w);
	if (sizeof(INPUT_PREFIX)) {

		if (line == tail)
			break;
		if (!draw_fmt(&input_ptr, &buff_n, &text_n, 0,
				"%s", draw_colour(INPUT_PREFIX_FG, INPUT_PREFIX_BG)))
			goto print_input;

		if (row_count >= row_total)
			break;
		cursor = coords.c0 + sizeof(INPUT_PREFIX) - 1;

		line = buffer_line(b, --buffer_i);
		if (!draw_fmt(&input_ptr, &buff_n, &text_n, 1,
				INPUT_PREFIX))
			goto print_input;
	}

	/* Handle impartial top line print */
	if (row_count > row_total) {
	if (!draw_fmt(&input_ptr, &buff_n, &text_n, 0,
			"%s", draw_colour(INPUT_FG, INPUT_BG)))
		goto print_input;

		split_buffer_cols(line, &head_w, &text_w, col_total, b->pad);
	if (action_message) {

		_draw_buffer_line(
			line,
			coords,
			head_w,
			text_w,
			row_count - row_total,
			BUFFER_PADDING ? (b->pad - line->from_len) : 0
		);
		cursor = coords.cN;

		coords.r1 += buffer_line_rows(line, text_w) - (row_count - row_total);
		if (!draw_fmt(&input_ptr, &buff_n, &text_n, 1,
				"%s", action_message))
			goto print_input;

		if (line == head)
			return;
		cursor = cols_t - text_n + 1;

		line = buffer_line(b, ++buffer_i);
	} else {
		cursor += input_frame(inp, input_ptr, text_n);
	}

	/* Draw all remaining lines */
	while (coords.r1 <= coords.rN) {

		split_buffer_cols(line, &head_w, &text_w, col_total, b->pad);

		_draw_buffer_line(
			line,
			coords,
			head_w,
			text_w,
			0,
			BUFFER_PADDING ? (b->pad - line->from_len) : 0
		);

		coords.r1 += buffer_line_rows(line, text_w);

		if (line == head)
			return;
print_input:

		line = buffer_line(b, ++buffer_i);
	}
	fputs(input, stdout);
	printf(MOVE(%d, %d), coords.rN, (cursor >= coords.c0 && cursor <= coords.cN) ? cursor : coords.cN);
	printf(CURSOR_SAVE);
}

/* TODO


@@ 408,7 515,7 @@ _draw_buffer(struct buffer *b, struct coords coords)
 *           | #chan1 #chan2 #ch... |   Left printing
 * */
static void
_draw_nav(struct channel *c)
draw_nav(struct channel *c)
{
	/* Dynamically draw the nav such that:
	 *


@@ 505,7 612,7 @@ _draw_nav(struct channel *c)

		colour = (tmp == c) ? NAV_CURRENT_CHAN : actv_colours[tmp->activity];

		if (fputs(_colour(colour, -1), stdout) < 0)
		if (fputs(draw_colour(colour, -1), stdout) < 0)
			break;

		if (printf(" %s ", tmp->name) < 0)


@@ 517,69 624,7 @@ _draw_nav(struct channel *c)
}

static void
_draw_input(struct input *inp, struct coords coords)
{
	/* Draw the input line, or the current action message */

	check_coords(coords);

	unsigned int cols_t = coords.cN - coords.c1 + 1,
	             cursor = coords.c1;

	printf(RESET_ATTRIBUTES);
	printf(MOVE(%d, 1) CLEAR_LINE, coords.rN);
	printf(CURSOR_SAVE);

	/* Insufficient columns for meaningful input drawing */
	if (cols_t < 3)
		return;

	char input[cols_t + COLOUR_SIZE * 2 + 1];
	char *input_ptr = input;

	size_t buff_n = sizeof(input) - 1,
	       text_n = cols_t;

	if (sizeof(INPUT_PREFIX)) {

		if (!_draw_fmt(&input_ptr, &buff_n, &text_n, 0,
				"%s", _colour(INPUT_PREFIX_FG, INPUT_PREFIX_BG)))
			goto print_input;

		cursor = coords.c1 + sizeof(INPUT_PREFIX) - 1;

		if (!_draw_fmt(&input_ptr, &buff_n, &text_n, 1,
				INPUT_PREFIX))
			goto print_input;
	}

	if (!_draw_fmt(&input_ptr, &buff_n, &text_n, 0,
			"%s", _colour(INPUT_FG, INPUT_BG)))
		goto print_input;

	if (action_message) {

		cursor = coords.cN;

		if (!_draw_fmt(&input_ptr, &buff_n, &text_n, 1,
				"%s", action_message))
			goto print_input;

		cursor = cols_t - text_n + 1;

	} else {
		cursor += input_frame(inp, input_ptr, text_n);
	}

print_input:

	fputs(input, stdout);
	printf(MOVE(%d, %d), coords.rN, (cursor >= coords.c1 && cursor <= coords.cN) ? cursor : coords.cN);
	printf(CURSOR_SAVE);
}

static void
_draw_status(struct channel *c)
draw_status(struct channel *c)
{
	/* TODO: channel modes, channel type_flag, servermodes */



@@ 592,9 637,9 @@ _draw_status(struct channel *c)

	float sb;
	int ret;
	unsigned int col = 0;
	unsigned int cols = io_tty_cols();
	unsigned int rows = io_tty_rows();
	unsigned col = 0;
	unsigned cols = io_tty_cols();
	unsigned rows = io_tty_rows();

	/* Insufficient columns for meaningful status */
	if (cols < 3)


@@ 686,22 731,22 @@ print_status:
		printf(HORIZONTAL_SEPARATOR);
}

static inline void
static void
check_coords(struct coords coords)
{
	/* Check coordinate validity before drawing, ensure at least one row, column */

	if (coords.r1 > coords.rN)
		fatal("row coordinates invalid (%u > %u)", coords.r1, coords.rN);
	if (coords.r0 > coords.rN)
		fatal("row coordinates invalid (%u > %u)", coords.r0, coords.rN);

	if (coords.c1 > coords.cN)
		fatal("col coordinates invalid (%u > %u)", coords.c1, coords.cN);
	if (coords.c0 > coords.cN)
		fatal("col coordinates invalid (%u > %u)", coords.c0, coords.cN);
}

static inline unsigned int
static unsigned
nick_col(char *nick)
{
	unsigned int colour = 0;
	unsigned colour = 0;

	while (*nick)
		colour += *nick++;


@@ 710,7 755,7 @@ nick_col(char *nick)
}

static char*
_colour(int fg, int bg)
draw_colour(int fg, int bg)
{
	/* Set terminal foreground and background colours to a value [0, 255],
	 * or reset colour if given anything else


@@ 739,7 784,7 @@ _colour(int fg, int bg)
}

static int
_draw_fmt(char **ptr, size_t *buff_n, size_t *text_n, int txt, const char *fmt, ...)
draw_fmt(char **ptr, size_t *buff_n, size_t *text_n, int txt, const char *fmt, ...)
{
	/* Write formatted text to a buffer for purposes of preparing an object to be drawn
	 * to the terminal.


@@ 784,30 829,3 @@ _draw_fmt(char **ptr, size_t *buff_n, size_t *text_n, int txt, const char *fmt, 

	return 1;
}

void
split_buffer_cols(
	struct buffer_line *line,
	unsigned int *head_w,
	unsigned int *text_w,
	unsigned int cols,
	unsigned int pad)
{
	unsigned int _head_w = sizeof(" HH:MM   "VERTICAL_SEPARATOR" ");

	if (BUFFER_PADDING)
		_head_w += pad;
	else
		_head_w += line->from_len;

	/* If header won't fit, split in half */
	if (_head_w >= cols)
		_head_w = cols / 2;

	_head_w -= 1;

	if (head_w)
		*head_w = _head_w;
	if (text_w)
		*text_w = cols - _head_w + 1;
}

M src/draw.h => src/draw.h +10 -19
@@ 1,29 1,20 @@
#ifndef DRAW_H
#define DRAW_H

#include "src/components/buffer.h"

/* Draw component, e.g. draw_buffer(); */
#define DRAW_BITS \
	X(buffer) \
	X(input)  \
	X(nav)    \
	X(status)

union draw
enum draw_bit
{
	struct {
		#define X(bit) unsigned int bit : 1;
		DRAW_BITS
		#undef X
	} bits;
	unsigned int all_bits;
	DRAW_INVALID,
	DRAW_FLUSH,  /* immediately draw all set bits */
	DRAW_BELL,   /* set bit to print terminal bell */
	DRAW_BUFFER, /* set bit to draw buffer */
	DRAW_INPUT,  /* set bit to draw input */
	DRAW_NAV,    /* set bit to draw nav */
	DRAW_STATUS, /* set bit to draw status */
	DRAW_ALL,    /* set all draw bits aside from bell */
};

void draw(union draw);
void draw_bell(void);
void draw(enum draw_bit);
void draw_init(void);
void draw_term(void);
void split_buffer_cols(struct buffer_line*, unsigned int*, unsigned int*, unsigned int, unsigned int);

#endif

M src/handlers/irc_ctcp.c => src/handlers/irc_ctcp.c +162 -27
@@ 25,7 25,6 @@
static int
parse_ctcp(struct server *s, const char *from, char **args, const char **cmd)
{
	char *saveptr;
	char *message = *args;
	char *command;
	char *p;


@@ 41,14 40,14 @@ parse_ctcp(struct server *s, const char *from, char **args, const char **cmd)

	*message++ = 0;

	if (!(command = strtok_r(message, " ", &saveptr)))
	if (!(command = strsep(&message)))
		failf(s, "Received empty CTCP from %s", from);

	for (p = command; *p; p++)
		*p = toupper(*p);

	*cmd = command;
	*args = saveptr;
	*args = strtrim(&message);

	return 0;
}


@@ 63,10 62,10 @@ ctcp_request(struct server *s, const char *from, const char *targ, char *message
	if ((ret = parse_ctcp(s, from, &message, &command)) != 0)
		return ret;

	if ((ctcp = ctcp_handler_lookup(command, strlen(command))))
		return ctcp->f_request(s, from, targ, message);
	if (!(ctcp = ctcp_handler_lookup(command, strlen(command))))
		failf(s, "Received unsupported CTCP request '%s' from %s", command, from);

	failf(s, "Received unsupported CTCP request '%s' from %s", command, from);
	return ctcp->f_request(s, from, targ, message);
}

int


@@ 79,15 78,24 @@ ctcp_response(struct server *s, const char *from, const char *targ, char *messag
	if ((ret = parse_ctcp(s, from, &message, &command)) != 0)
		return ret;

	if ((ctcp = ctcp_handler_lookup(command, strlen(command))))
		return ctcp->f_response(s, from, targ, message);
	if (!(ctcp = ctcp_handler_lookup(command, strlen(command))) || !ctcp->f_response)
		failf(s, "Received unsupported CTCP response '%s' from %s", command, from);

	failf(s, "Received unsupported CTCP response '%s' from %s", command, from);
	return ctcp->f_response(s, from, targ, message);
}

static int
ctcp_request_action(struct server *s, const char *from, const char *targ, char *m)
{
	/* Type:     Extended Formatting
	 * Request:  ACTION <text>
	 * Response: -- no response --
	 *
	 * This extended formatting message shows that <text> should be displayed as
	 * a third-person action or emote. If <text> is empty, clients SHOULD still
	 * include a single space after
	 */

	struct channel *c;

	if (!targ)


@@ 104,7 112,7 @@ ctcp_request_action(struct server *s, const char *from, const char *targ, char *
		failf(s, "CTCP ACTION: target '%s' not found", targ);
	}

	if (str_trim(&m))
	if (strtrim(&m))
		newlinef(c, 0, "*", "%s %s", from, m);
	else
		newlinef(c, 0, "*", "%s", from);


@@ 115,9 123,17 @@ ctcp_request_action(struct server *s, const char *from, const char *targ, char *
static int
ctcp_request_clientinfo(struct server *s, const char *from, const char *targ, char *m)
{
	/* Type:     Extended Query
	 * Request:  CLIENTINFO
	 * Response: CLIENTINFO <args>
	 *
	 * This extended query returns a list of the CTCP messages that this client
	 * supports and implements, delimited by a single ASCII space.
	 */

	UNUSED(targ);

	if (str_trim(&m))
	if (strtrim(&m))
		server_info(s, "CTCP CLIENTINFO from %s (%s)", from, m);
	else
		server_info(s, "CTCP CLIENTINFO from %s", from);


@@ 130,9 146,18 @@ ctcp_request_clientinfo(struct server *s, const char *from, const char *targ, ch
static int
ctcp_request_finger(struct server *s, const char *from, const char *targ, char *m)
{
	/* Type:     Metadata Query
	 * Request:  FINGER
	 * Response: FINGER <info>
	 *
	 * This metadata query returns miscellaneous info about the user, typically
	 * the same information that’s held in their realname field. However, some
	 * implementations return the client name and version instead.
	 */

	UNUSED(targ);

	if (str_trim(&m))
	if (strtrim(&m))
		server_info(s, "CTCP FINGER from %s (%s)", from, m);
	else
		server_info(s, "CTCP FINGER from %s", from);


@@ 145,11 170,26 @@ ctcp_request_finger(struct server *s, const char *from, const char *targ, char *
static int
ctcp_request_ping(struct server *s, const char *from, const char *targ, char *m)
{
	/* Type:     Extended Query
	 * Request:  PING <info>
	 * Response: PING <info>
	 *
	 * This extended query confirms reachability and latency to the target
	 * client. When receiving a CTCP PING, the reply MUST contain exactly
	 * the same parameters as the original query.
	 */

	UNUSED(targ);

	server_info(s, "CTCP PING from %s", from);
	if (strtrim(&m))
		server_info(s, "CTCP PING from %s (%s)", from, m);
	else
		server_info(s, "CTCP PING from %s", from);

	sendf(s, "NOTICE %s :\001PING %s\001", from, m);
	if (m)
		sendf(s, "NOTICE %s :\001PING %s\001", from, m);
	else
		sendf(s, "NOTICE %s :\001PING\001", from);

	return 0;
}


@@ 157,9 197,16 @@ ctcp_request_ping(struct server *s, const char *from, const char *targ, char *m)
static int
ctcp_request_source(struct server *s, const char *from, const char *targ, char *m)
{
	/* Type:     Metadata Query
	 * Request:  SOURCE
	 * Response: SOURCE <info>
	 *
	 * This metadata query returns the location of the source code for the client.
	 */

	UNUSED(targ);

	if (str_trim(&m))
	if (strtrim(&m))
		server_info(s, "CTCP SOURCE from %s (%s)", from, m);
	else
		server_info(s, "CTCP SOURCE from %s", from);


@@ 172,6 219,17 @@ ctcp_request_source(struct server *s, const char *from, const char *targ, char *
static int
ctcp_request_time(struct server *s, const char *from, const char *targ, char *m)
{
	/* Type:     Extended Query
	 * Request:  TIME
	 * Response: TIME <timestring>
	 *
	 * This extended query returns the client’s local time in an unspecified
	 * human-readable format. In practice, both the format output by ctime()
	 * and the format described in Section 3.3 of RFC5322 are common. Earlier
	 * specifications recommended prefixing the time string with a colon,
	 * but this is no longer recommended.
	 */

	/* ISO 8601 */
	char buf[sizeof("1970-01-01T00:00:00")];
	struct tm tm;


@@ 179,7 237,7 @@ ctcp_request_time(struct server *s, const char *from, const char *targ, char *m)

	UNUSED(targ);

	if (str_trim(&m))
	if (strtrim(&m))
		server_info(s, "CTCP TIME from %s (%s)", from, m);
	else
		server_info(s, "CTCP TIME from %s", from);


@@ 205,9 263,18 @@ ctcp_request_time(struct server *s, const char *from, const char *targ, char *m)
static int
ctcp_request_userinfo(struct server *s, const char *from, const char *targ, char *m)
{
	/* Type:     Metadata Query
	 * Request:  USERINFO
	 * Response: USERINFO <info>
	 *
	 * This metadata query returns miscellaneous info about the user, typically
	 * the same information that’s held in their realname field. However, some
	 * implementations return <nickname> (<realname>) instead.
	 */

	UNUSED(targ);

	if (str_trim(&m))
	if (strtrim(&m))
		server_info(s, "CTCP USERINFO from %s (%s)", from, m);
	else
		server_info(s, "CTCP USERINFO from %s", from);


@@ 220,9 287,17 @@ ctcp_request_userinfo(struct server *s, const char *from, const char *targ, char
static int
ctcp_request_version(struct server *s, const char *from, const char *targ, char *m)
{
	/* Type:     Metadata Query
	 * Request:  VERSION
	 * Response: VERSION <verstring>
	 *
	 * This metadata query returns the name and version of the client software in
	 * use. There is no specified format for the version string.
	 */

	UNUSED(targ);

	if (str_trim(&m))
	if (strtrim(&m))
		server_info(s, "CTCP VERSION from %s (%s)", from, m);
	else
		server_info(s, "CTCP VERSION from %s", from);


@@ 235,9 310,17 @@ ctcp_request_version(struct server *s, const char *from, const char *targ, char 
static int
ctcp_response_clientinfo(struct server *s, const char *from, const char *targ, char *m)
{
	/* Type:     Extended Query
	 * Request:  CLIENTINFO
	 * Response: CLIENTINFO <args>
	 *
	 * This extended query returns a list of the CTCP messages that this client
	 * supports and implements, delimited by a single ASCII space.
	 */

	UNUSED(targ);

	if (!str_trim(&m))
	if (!strtrim(&m))
		failf(s, "CTCP CLIENTINFO response from %s: empty message", from);

	server_info(s, "CTCP CLIENTINFO response from %s: %s", from, m);


@@ 248,9 331,18 @@ ctcp_response_clientinfo(struct server *s, const char *from, const char *targ, c
static int
ctcp_response_finger(struct server *s, const char *from, const char *targ, char *m)
{
	/* Type:     Metadata Query
	 * Request:  FINGER
	 * Response: FINGER <info>
	 *
	 * This metadata query returns miscellaneous info about the user, typically
	 * the same information that’s held in their realname field. However, some
	 * implementations return the client name and version instead.
	 */

	UNUSED(targ);

	if (!str_trim(&m))
	if (!strtrim(&m))
		failf(s, "CTCP FINGER response from %s: empty message", from);

	server_info(s, "CTCP FINGER response from %s: %s", from, m);


@@ 261,7 353,15 @@ ctcp_response_finger(struct server *s, const char *from, const char *targ, char 
static int
ctcp_response_ping(struct server *s, const char *from, const char *targ, char *m)
{
	char *saveptr;
	/* Type:     Extended Query
	 * Request:  PING <info>
	 * Response: PING <info>
	 *
	 * This extended query confirms reachability and latency to the target
	 * client. When receiving a CTCP PING, the reply MUST contain exactly
	 * the same parameters as the original query.
	 */

	const char *sec;
	const char *usec;
	long long unsigned res;


@@ 274,10 374,10 @@ ctcp_response_ping(struct server *s, const char *from, const char *targ, char *m

	UNUSED(targ);

	if (!(sec = strtok_r(m, " ", &saveptr)))
	if (!(sec = strsep(&m)))
		failf(s, "CTCP PING response from %s: sec is NULL", from);

	if (!(usec = strtok_r(NULL, " ", &saveptr)))
	if (!(usec = strsep(&m)))
		failf(s, "CTCP PING response from %s: usec is NULL", from);

	for (const char *p = sec; *p; p++) {


@@ 325,9 425,16 @@ ctcp_response_ping(struct server *s, const char *from, const char *targ, char *m
static int
ctcp_response_source(struct server *s, const char *from, const char *targ, char *m)
{
	/* Type:     Metadata Query
	 * Request:  SOURCE
	 * Response: SOURCE <info>
	 *
	 * This metadata query returns the location of the source code for the client.
	 */

	UNUSED(targ);

	if (!str_trim(&m))
	if (!strtrim(&m))
		failf(s, "CTCP SOURCE response from %s: empty message", from);

	server_info(s, "CTCP SOURCE response from %s: %s", from, m);


@@ 338,9 445,20 @@ ctcp_response_source(struct server *s, const char *from, const char *targ, char 
static int
ctcp_response_time(struct server *s, const char *from, const char *targ, char *m)
{
	/* Type:     Extended Query
	 * Request:  TIME
	 * Response: TIME <timestring>
	 *
	 * This extended query returns the client’s local time in an unspecified
	 * human-readable format. In practice, both the format output by ctime()
	 * and the format described in Section 3.3 of RFC5322 are common. Earlier
	 * specifications recommended prefixing the time string with a colon,
	 * but this is no longer recommended.
	 */

	UNUSED(targ);

	if (!str_trim(&m))
	if (!strtrim(&m))
		failf(s, "CTCP TIME response from %s: empty message", from);

	server_info(s, "CTCP TIME response from %s: %s", from, m);


@@ 351,9 469,18 @@ ctcp_response_time(struct server *s, const char *from, const char *targ, char *m
static int
ctcp_response_userinfo(struct server *s, const char *from, const char *targ, char *m)
{
	/* Type:     Metadata Query
	 * Request:  USERINFO
	 * Response: USERINFO <info>
	 *
	 * This metadata query returns miscellaneous info about the user, typically
	 * the same information that’s held in their realname field. However, some
	 * implementations return <nickname> (<realname>) instead.
	 */

	UNUSED(targ);

	if (!str_trim(&m))
	if (!strtrim(&m))
		failf(s, "CTCP USERINFO response from %s: empty message", from);

	server_info(s, "CTCP USERINFO response from %s: %s", from, m);


@@ 364,9 491,17 @@ ctcp_response_userinfo(struct server *s, const char *from, const char *targ, cha
static int
ctcp_response_version(struct server *s, const char *from, const char *targ, char *m)
{
	/* Type:     Metadata Query
	 * Request:  VERSION
	 * Response: VERSION <verstring>
	 *
	 * This metadata query returns the name and version of the client software in
	 * use. There is no specified format for the version string.
	 */

	UNUSED(targ);

	if (!str_trim(&m))
	if (!strtrim(&m))
		failf(s, "CTCP VERSION response from %s: empty message", from);

	server_info(s, "CTCP VERSION response from %s: %s", from, m);

M src/handlers/irc_recv.c => src/handlers/irc_recv.c +51 -31
@@ 6,6 6,8 @@
#include "src/handlers/irc_ctcp.h"
#include "src/handlers/irc_recv.gperf.out"
#include "src/handlers/irc_recv.h"
#include "src/handlers/ircv3.h"
#include "src/draw.h"
#include "src/io.h"
#include "src/state.h"
#include "src/utils/utils.h"


@@ 55,7 57,7 @@ static int irc_353(struct server*, struct irc_message*);
static int irc_433(struct server*, struct irc_message*);

static int irc_recv_numeric(struct server*, struct irc_message*);
static int recv_mode_chanmodes(struct irc_message*, const struct mode_cfg*, struct channel*);
static int recv_mode_chanmodes(struct irc_message*, const struct mode_cfg*, struct server*, struct channel*);
static int recv_mode_usermodes(struct irc_message*, const struct mode_cfg*, struct server*);

static const unsigned quit_threshold = QUIT_THRESHOLD;


@@ 142,7 144,7 @@ static const irc_recv_f irc_numerics[] = {
	[349] = irc_ignore, /* RPL_ENDOFEXCEPTLIST */
	[351] = irc_info,   /* RPL_VERSION */
	[352] = irc_info,   /* RPL_WHOREPLY */
	[353] = irc_353,    /* RPL_NAMREPLY */
	[353] = irc_353,    /* RPL_NAMEREPLY */
	[364] = irc_info,   /* RPL_LINKS */
	[365] = irc_ignore, /* RPL_ENDOFLINKS */
	[366] = irc_ignore, /* RPL_ENDOFNAMES */


@@ 165,6 167,7 @@ static const irc_recv_f irc_numerics[] = {
	[407] = irc_error,  /* ERR_TOOMANYTARGETS */
	[408] = irc_error,  /* ERR_NOSUCHSERVICE */
	[409] = irc_error,  /* ERR_NOORIGIN */
	[410] = irc_error,  /* ERR_INVALIDCAPCMD */
	[411] = irc_error,  /* ERR_NORECIPIENT */
	[412] = irc_error,  /* ERR_NOTEXTTOSEND */
	[413] = irc_error,  /* ERR_NOTOPLEVEL */


@@ 206,7 209,7 @@ static const irc_recv_f irc_numerics[] = {
	[491] = irc_error,  /* ERR_NOOPERHOST */
	[501] = irc_error,  /* ERR_UMODEUNKNOWNFLAG */
	[502] = irc_error,  /* ERR_USERSDONTMATCH */
	[704] = irc_ignore, /* RPL_HELPSTART */
	[704] = irc_info,   /* RPL_HELPSTART */
	[705] = irc_info,   /* RPL_HELP */
	[706] = irc_ignore, /* RPL_ENDOFHELP */
};


@@ 230,7 233,7 @@ irc_message(struct server *s, struct irc_message *m, const char *from)
{
	char *trailing;

	if (!str_trim(&m->params))
	if (!strtrim(&m->params))
		return 0;

	if (irc_message_split(m, &trailing)) {


@@ 280,6 283,8 @@ irc_001(struct server *s, struct irc_message *m)
	char *trailing;
	struct channel *c = s->channel;

	s->registered = 1;

	do {
		if (c->type == CHANNEL_T_CHANNEL && !c->parted)
			sendf(s, "JOIN %s", c->name);


@@ 349,7 354,7 @@ irc_324(struct server *s, struct irc_message *m)
	if ((c = channel_list_get(&s->clist, chan, s->casemapping)) == NULL)
		failf(s, "RPL_CHANNELMODEIS: channel '%s' not found", chan);

	return recv_mode_chanmodes(m, &(s->mode_cfg), c);
	return recv_mode_chanmodes(m, &(s->mode_cfg), s, c);
}

static int


@@ 479,9 484,8 @@ irc_333(struct server *s, struct irc_message *m)
static int
irc_353(struct server *s, struct irc_message *m)
{
	/* 353 ("="/"*"/"@") <channel> *([ "@" / "+" ]<nick>) */
	/* 353 <nick> <type> <channel> 1*(<modes><nick>) */

	char *saveptr;
	char *chan;
	char *nick;
	char *nicks;


@@ 501,26 505,34 @@ irc_353(struct server *s, struct irc_message *m)
		failf(s, "RPL_NAMEREPLY: channel '%s' not found", chan);

	if (mode_chanmode_prefix(&(c->chanmodes), &(s->mode_cfg), *type) != MODE_ERR_NONE)
		newlinef(c, 0, FROM_ERROR, "RPL_NAMEREPLY: invalid channel flag: '%c'", *type);
		failf(s, "RPL_NAMEREPLY: invalid channel flag: '%c'", *type);

	if ((nick = strtok_r(nicks, " ", &saveptr))) {
	if ((nick = strsep(&nicks))) {
		do {
			char prefix = 0;
			struct mode m = MODE_EMPTY;

			if (!irc_isnickchar(*nick, 1))
			do {
				if (irc_isnickchar(*nick, 1))
					break;

				prefix = *nick++;

			if (prefix && mode_prfxmode_prefix(&m, &(s->mode_cfg), prefix) != MODE_ERR_NONE)
				newlinef(c, 0, FROM_ERROR, "Invalid user prefix: '%c'", prefix);
				if (mode_prfxmode_prefix(&m, &(s->mode_cfg), prefix) != MODE_ERR_NONE)
					failf(s, "RPL_NAMEREPLY: invalid user prefix: '%c'", prefix);

			} while (s->ircv3_caps.multi_prefix.set);

			if (!irc_isnick(nick))
				failf(s, "RPL_NAMEREPLY: invalid nick: '%s'", nick);

			if (user_list_add(&(c->users), s->casemapping, nick, m) == USER_ERR_DUPLICATE)
				newlinef(c, 0, FROM_ERROR, "Duplicate nick: '%s'", nick);
				failf(s, "RPL_NAMEREPLY: duplicate nick: '%s'", nick);

		} while ((nick = strtok_r(NULL, " ", &saveptr)));
		} while ((nick = strsep(&nicks)));
	}

	draw_status();
	draw(DRAW_STATUS);

	return 0;
}


@@ 592,6 604,12 @@ irc_recv_numeric(struct server *s, struct irc_message *m)
}

static int
recv_cap(struct server *s, struct irc_message *m)
{
	return ircv3_recv_CAP(s, m);
}

static int
recv_error(struct server *s, struct irc_message *m)
{
	/* ERROR <message> */


@@ 656,7 674,7 @@ recv_join(struct server *s, struct irc_message *m)
		c->parted = 0;
		newlinef(c, BUFFER_LINE_JOIN, FROM_JOIN, "Joined %s", chan);
		sendf(s, "MODE %s", chan);
		draw_all();
		draw(DRAW_ALL);
		return 0;
	}



@@ 669,7 687,7 @@ recv_join(struct server *s, struct irc_message *m)
	if (!join_threshold || c->users.count <= join_threshold)
		newlinef(c, BUFFER_LINE_JOIN, FROM_JOIN, "%s!%s has joined", m->from, m->host);

	draw_status();
	draw(DRAW_STATUS);

	return 0;
}


@@ 725,7 743,7 @@ recv_kick(struct server *s, struct irc_message *m)
			newlinef(c, 0, FROM_INFO, "%s has kicked %s", m->from, user);
	}

	draw_status();
	draw(DRAW_STATUS);

	return 0;
}


@@ 765,13 783,13 @@ recv_mode(struct server *s, struct irc_message *m)
		return recv_mode_usermodes(m, &(s->mode_cfg), s);

	if ((c = channel_list_get(&s->clist, targ, s->casemapping)))
		return recv_mode_chanmodes(m, &(s->mode_cfg), c);
		return recv_mode_chanmodes(m, &(s->mode_cfg), s, c);

	failf(s, "MODE: target '%s' not found", targ);
}

static int
recv_mode_chanmodes(struct irc_message *m, const struct mode_cfg *cfg, struct channel *c)
recv_mode_chanmodes(struct irc_message *m, const struct mode_cfg *cfg, struct server *s, struct channel *c)
{
	char flag;
	char *modestring;


@@ 781,6 799,8 @@ recv_mode_chanmodes(struct irc_message *m, const struct mode_cfg *cfg, struct ch
	struct mode *chanmodes = &(c->chanmodes);
	struct user *user;

	// TODO: mode string segfaults if args out of order

	if (!irc_message_param(m, &modestring)) {
		newlinef(c, 0, FROM_ERROR, "MODE: modestring is null");
		return 1;


@@ 850,7 870,7 @@ recv_mode_chanmodes(struct irc_message *m, const struct mode_cfg *cfg, struct ch
						continue;
					}

					if (!(user = user_list_get(&(c->users), c->server->casemapping, modearg, 0))) {
					if (!(user = user_list_get(&(c->users), s->casemapping, modearg, 0))) {
						newlinef(c, 0, FROM_ERROR, "MODE: flag '%c' user '%s' not found", flag, modearg);
						continue;
					}


@@ 876,7 896,7 @@ recv_mode_chanmodes(struct irc_message *m, const struct mode_cfg *cfg, struct ch
					break;

				default:
					newlinef(c, 0, FROM_ERROR, "MODE: unhandled error, flag '%c'");
					newlinef(c, 0, FROM_ERROR, "MODE: unhandled error, flag '%c'", flag);
					continue;
			}



@@ 897,7 917,7 @@ recv_mode_chanmodes(struct irc_message *m, const struct mode_cfg *cfg, struct ch
	} while (irc_message_param(m, &modestring));

	mode_str(&(c->chanmodes), &(c->chanmodes_str));
	draw_status();
	draw(DRAW_STATUS);

	return 0;
}


@@ 947,7 967,7 @@ recv_mode_usermodes(struct irc_message *m, const struct mode_cfg *cfg, struct se
	} while (irc_message_param(m, &modestring));

	mode_str(usermodes, &(s->mode_str));
	draw_status();
	draw(DRAW_STATUS);

	return 0;
}


@@ 968,7 988,7 @@ recv_nick(struct server *s, struct irc_message *m)

	if (!strcmp(m->from, s->nick)) {
		server_nick_set(s, nick);
		newlinef(s->channel, BUFFER_LINE_NICK, FROM_NICK, "Youn nick is '%s'", nick);
		newlinef(s->channel, BUFFER_LINE_NICK, FROM_NICK, "Your nick is now '%s'", nick);
	}

	do {


@@ 1039,8 1059,8 @@ recv_notice(struct server *s, struct irc_message *m)

	if (urgent) {
		c->activity = ACTIVITY_PINGED;
		draw_bell();
		draw_nav();
		draw(DRAW_BELL);
		draw(DRAW_NAV);
	}

	return 0;


@@ 1089,7 1109,7 @@ recv_part(struct server *s, struct irc_message *m)
		}
	}

	draw_status();
	draw(DRAW_STATUS);

	return 0;
}


@@ 1172,8 1192,8 @@ recv_privmsg(struct server *s, struct irc_message *m)

	if (urgent) {
		c->activity = ACTIVITY_PINGED;
		draw_bell();
		draw_nav();
		draw(DRAW_BELL);
		draw(DRAW_NAV);
	}

	return 0;


@@ 1234,7 1254,7 @@ recv_quit(struct server *s, struct irc_message *m)
		}
	} while ((c = c->next) != s->channel);

	draw_status();
	draw(DRAW_STATUS);

	return 0;
}

M src/handlers/irc_recv.gperf => src/handlers/irc_recv.gperf +2 -0
@@ 2,6 2,7 @@
#include <string.h>

#define RECV_HANDLERS \
	X(cap) \
	X(error) \
	X(invite) \
	X(join) \


@@ 40,6 41,7 @@ struct recv_handler
%define initializer-suffix ,(irc_recv_f)0
struct recv_handler;
%%
CAP,     recv_cap
ERROR,   recv_error
INVITE,  recv_invite
JOIN,    recv_join

M src/handlers/irc_recv.h => src/handlers/irc_recv.h +3 -0
@@ 361,6 361,9 @@
 *         --- NOT IMPLEMENTED ---
 */

#include "src/components/server.h"
#include "src/utils/utils.h"

int irc_recv(struct server*, struct irc_message*);

#endif

M src/handlers/irc_send.c => src/handlers/irc_send.c +99 -72
@@ 4,6 4,7 @@
#include "config.h"
#include "src/components/buffer.h"
#include "src/components/channel.h"
#include "src/components/ircv3.h"
#include "src/components/server.h"
#include "src/handlers/irc_send.gperf.out"
#include "src/handlers/irc_send.h"


@@ 11,8 12,6 @@
#include "src/state.h"
#include "src/utils/utils.h"

// TODO: should privmsg/notice open a PRIVATE/CHANNEL buffer for the target?

#define failf(C, ...) \
	do { newlinef((C), 0, FROM_ERROR, __VA_ARGS__); \
	     return 1; \


@@ 29,24 28,28 @@ static const char* targ_or_type(struct channel*, char*, enum channel_t type);
int
irc_send_command(struct server *s, struct channel *c, char *m)
{
	char *saveptr;
	char *command, *p;
	char *command, *command_args, *p;
	const struct send_handler *send;

	if (!s)
		failf(c, "This is not a server");

	if (*m == ' ' || !(command = strtok_r(m, " ", &saveptr)))
	if (!s->registered)
		failf(c, "Not registered with server");

	if (*m == ' ' || !(command = strsep(&m)))
		failf(c, "Messages beginning with '/' require a command");

	for (p = command; *p; p++)
		*p = toupper(*p);

	command_args = strtrim(&m);

	if ((send = send_handler_lookup(command, strlen(command))))
		return send->f(s, c, saveptr);
		return send->f(s, c, command_args);

	if (str_trim(&saveptr))
		sendf(s, c, "%s %s", command, saveptr);
	if (strtrim(&command_args))
		sendf(s, c, "%s %s", command, command_args);
	else
		sendf(s, c, "%s", command);



@@ 59,6 62,9 @@ irc_send_privmsg(struct server *s, struct channel *c, char *m)
	if (!s)
		failf(c, "This is not a server");

	if (!s->registered)
		failf(c, "Not registered with server");

	if (!(c->type == CHANNEL_T_CHANNEL || c->type == CHANNEL_T_PRIVATE))
		failf(c, "This is not a channel");



@@ 78,10 84,9 @@ irc_send_privmsg(struct server *s, struct channel *c, char *m)
static const char*
targ_or_type(struct channel *c, char *m, enum channel_t type)
{
	char *saveptr;
	const char *targ;

	if ((targ = strtok_r(m, " ", &saveptr)))
	if ((targ = strsep(&m)))
		return targ;

	if (c->type == type)


@@ 91,6 96,79 @@ targ_or_type(struct channel *c, char *m, enum channel_t type)
}

static int
send_notice(struct server *s, struct channel *c, char *m)
{
	const char *targ;

	if (!(targ = strsep(&m)))
		failf(c, "Usage: /notice <target> <message>");

	if (!m || !*m)
		failf(c, "Usage: /notice <target> <message>");

	sendf(s, c, "NOTICE %s :%s", targ, m);

	return 0;
}

static int
send_part(struct server *s, struct channel *c, char *m)
{
	if (c->type != CHANNEL_T_CHANNEL)
		failf(c, "This is not a channel");

	if (strtrim(&m))
		sendf(s, c, "PART %s :%s", c->name, m);
	else
		sendf(s, c, "PART %s :%s", c->name, DEFAULT_PART_MESG);

	return 0;
}

static int
send_privmsg(struct server *s, struct channel *c, char *m)
{
	const char *targ;

	if (!(targ = strsep(&m)))
		failf(c, "Usage: /privmsg <target> <message>");

	if (!m || !*m)
		failf(c, "Usage: /privmsg <target> <message>");

	sendf(s, c, "PRIVMSG %s :%s", targ, m);

	return 0;
}

static int
send_quit(struct server *s, struct channel *c, char *m)
{
	s->quitting = 1;

	if (strtrim(&m))
		sendf(s, c, "QUIT :%s", m);
	else
		sendf(s, c, "QUIT :%s", DEFAULT_PART_MESG);

	return 0;
}

static int
send_topic(struct server *s, struct channel *c, char *m)
{
	if (c->type != CHANNEL_T_CHANNEL)
		failf(c, "This is not a channel");

	if (strtrim(&m))
		sendf(s, c, "TOPIC %s :%s", c->name, m);
	else
		sendf(s, c, "TOPIC %s", c->name);

	return 0;
}

static int
send_ctcp_action(struct server *s, struct channel *c, char *m)
{
	if (!(c->type == CHANNEL_T_CHANNEL || c->type == CHANNEL_T_PRIVATE))


@@ 138,7 216,9 @@ send_ctcp_ping(struct server *s, struct channel *c, char *m)

	(void) gettimeofday(&t, NULL);

	sendf(s, c, "PRIVMSG %s :\001PING %llu %llu\001", targ, t.tv_sec, t.tv_usec);
	sendf(s, c, "PRIVMSG %s :\001PING %llu %llu\001", targ,
		(unsigned long long)t.tv_sec,
		(unsigned long long)t.tv_usec);

	return 0;
}


@@ 196,76 276,23 @@ send_ctcp_version(struct server *s, struct channel *c, char *m)
}

static int
send_notice(struct server *s, struct channel *c, char *m)
{
	char *saveptr;
	const char *targ;

	if (!(targ = strtok_r(m, " ", &saveptr)))
		failf(c, "Usage: /notice <target> <message>");

	if (*saveptr == 0)
		failf(c, "Usage: /notice <target> <message>");

	sendf(s, c, "NOTICE %s :%s", targ, saveptr);

	return 0;
}

static int
send_part(struct server *s, struct channel *c, char *m)
send_ircv3_cap_ls(struct server *s, struct channel *c, char *m)
{
	if (c->type != CHANNEL_T_CHANNEL)
		failf(c, "This is not a channel");
	if (strtrim(&m))
		failf(c, "Usage: /cap-ls");

	if (str_trim(&m))
		sendf(s, c, "PART %s :%s", c->name, m);
	else
		sendf(s, c, "PART %s :%s", c->name, DEFAULT_PART_MESG);
	sendf(s, c, "CAP LS " IRCV3_CAP_VERSION);

	return 0;
}

static int
send_privmsg(struct server *s, struct channel *c, char *m)
send_ircv3_cap_list(struct server *s, struct channel *c, char *m)
{
	char *saveptr;
	const char *targ;
	if (strtrim(&m))
		failf(c, "Usage: /cap-list");

	if (!(targ = strtok_r(m, " ", &saveptr)))
		failf(c, "Usage: /privmsg <target> <message>");

	if (*saveptr == 0)
		failf(c, "Usage: /privmsg <target> <message>");

	sendf(s, c, "PRIVMSG %s :%s", targ, saveptr);

	return 0;
}

static int
send_quit(struct server *s, struct channel *c, char *m)
{
	s->quitting = 1;

	if (str_trim(&m))
		sendf(s, c, "QUIT :%s", m);
	else
		sendf(s, c, "QUIT :%s", DEFAULT_PART_MESG);

	return 0;
}

static int
send_topic(struct server *s, struct channel *c, char *m)
{
	if (c->type != CHANNEL_T_CHANNEL)
		failf(c, "This is not a channel");

	if (str_trim(&m))
		sendf(s, c, "TOPIC %s :%s", c->name, m);
	else
		sendf(s, c, "TOPIC %s", c->name);
	sendf(s, c, "CAP LIST");

	return 0;
}

M src/handlers/irc_send.gperf => src/handlers/irc_send.gperf +10 -0
@@ 18,6 18,10 @@
	X(userinfo) \
	X(version)

#define SEND_IRCV3_CAP_HANDLERS \
	X(ls) \
	X(list)

#define X(cmd) static int send_##cmd(struct server*, struct channel*, char*);
SEND_HANDLERS
#undef X


@@ 26,6 30,10 @@ SEND_HANDLERS
SEND_CTCP_HANDLERS
#undef X

#define X(cmd) static int send_ircv3_cap_##cmd(struct server*, struct channel*, char*);
SEND_IRCV3_CAP_HANDLERS
#undef X

typedef int (*irc_send_f)(struct server*, struct channel*, char*);

struct send_handler


@@ 46,6 54,8 @@ struct send_handler
%define initializer-suffix ,(irc_send_f)0
struct send_handler;
%%
CAP-LS,          send_ircv3_cap_ls
CAP-LIST,        send_ircv3_cap_list
CTCP-ACTION,     send_ctcp_action
CTCP-CLIENTINFO, send_ctcp_clientinfo
CTCP-FINGER,     send_ctcp_finger

M src/handlers/irc_send.h => src/handlers/irc_send.h +3 -0
@@ 1,6 1,9 @@
#ifndef IRC_SEND_H
#define IRC_SEND_H

#include "src/components/channel.h"
#include "src/components/server.h"

int irc_send_command(struct server*, struct channel*, char*);
int irc_send_privmsg(struct server*, struct channel*, char*);


A src/handlers/ircv3.c => src/handlers/ircv3.c +356 -0
@@ 0,0 1,356 @@
#include <string.h>

#include "src/handlers/ircv3.h"
#include "src/io.h"
#include "src/state.h"

#define failf(S, ...) \
	do { server_error((S), __VA_ARGS__); \
	     return 1; \
	} while (0)

#define sendf(S, ...) \
	do { int ret; \
	     if ((ret = io_sendf((S)->connection, __VA_ARGS__))) \
	         failf((S), "Send fail: %s", io_err(ret)); \
	} while (0)

#define IRCV3_RECV_HANDLERS \
	X(LIST) \
	X(LS) \
	X(ACK) \
	X(NAK) \
	X(DEL) \
	X(NEW)

#define X(CMD) \
static int ircv3_recv_cap_##CMD(struct server*, struct irc_message*);
IRCV3_RECV_HANDLERS
#undef X

static int ircv3_cap_req_count(struct ircv3_caps*);

int
ircv3_recv_CAP(struct server *s, struct irc_message *m)
{
	char *targ;
	char *cmnd;

	if (!irc_message_param(m, &targ))
		failf(s, "CAP: target is null");

	if (!irc_message_param(m, &cmnd))
		failf(s, "CAP: command is null");

	#define X(CMD) \
	if (!strcmp(cmnd, #CMD)) \
		return ircv3_recv_cap_##CMD(s, m);
	IRCV3_RECV_HANDLERS
	#undef X

	failf(s, "CAP: unrecognized subcommand '%s'", cmnd);
}

static int
ircv3_recv_cap_LS(struct server *s, struct irc_message *m)
{
	/* If no capabilities are available, an empty
	 * parameter MUST be sent.
	 *
	 * Servers MAY send multiple lines in response to
	 * CAP LS and CAP LIST. If the reply contains
	 * multiple lines, all but the last reply MUST
	 * have a parameter containing only an asterisk (*)
	 * preceding the capability list
	 *
	 * CAP <targ> LS [*] :[<cap_1> [...]]
	 */

	char *cap;
	char *caps;
	char *multiline;

	irc_message_param(m, &multiline);
	irc_message_param(m, &caps);

	if (!multiline)
		failf(s, "CAP LS: parameter is null");

	if (!strcmp(multiline, "*") && !caps)
		failf(s, "CAP LS: parameter is null");

	if (strcmp(multiline, "*") && caps)
		failf(s, "CAP LS: invalid parameters");

	if (!caps) {
		caps = multiline;
		multiline = NULL;
	}

	if (s->registered) {
		server_info(s, "CAP LS: %s", (*caps ? caps : "(no capabilities)"));
		return 0;
	}

	while ((cap = strsep(&(caps)))) {

		struct ircv3_cap *c;

		if ((c = ircv3_cap_get(&(s->ircv3_caps), cap)))
			c->supported = 1;
	}

	if (multiline)
		return 0;

	#define X(CAP, VAR, ATTRS)     \
	if (s->ircv3_caps.VAR.supported && s->ircv3_caps.VAR.req_auto) { \
		s->ircv3_caps.VAR.req = 1; \
		sendf(s, "CAP REQ :" CAP); \
	}
	IRCV3_CAPS
	#undef X

	if (!ircv3_cap_req_count(&(s->ircv3_caps)))
		sendf(s, "CAP END");

	return 0;
}

static int
ircv3_recv_cap_LIST(struct server *s, struct irc_message *m)
{
	/* If no capabilities are available, an empty
	 * parameter MUST be sent.
	 *
	 * Servers MAY send multiple lines in response to
	 * CAP LS and CAP LIST. If the reply contains
	 * multiple lines, all but the last reply MUST
	 * have a parameter containing only an asterisk (*)
	 * preceding the capability list
	 *
	 * CAP <targ> LIST [*] :[<cap_1> [...]]
	 */

	char *caps;
	char *multiline;

	irc_message_param(m, &multiline);
	irc_message_param(m, &caps);

	if (!multiline)
		failf(s, "CAP LIST: parameter is null");

	if (multiline && caps && strcmp(multiline, "*"))
		failf(s, "CAP LIST: invalid parameters");

	if (!strcmp(multiline, "*") && !caps)
		failf(s, "CAP LIST: parameter is null");

	if (!caps)
		caps = multiline;

	server_info(s, "CAP LIST: %s", (*caps ? caps : "(no capabilities)"));

	return 0;
}

static int
ircv3_recv_cap_ACK(struct server *s, struct irc_message *m)
{
	/* Each capability name may be prefixed with a
	 * dash (-), indicating that this capability has
	 * been disabled as requested.
	 *
	 * If an ACK reply originating from the server is
	 * spread across multiple lines, a client MUST NOT
	 * change capabilities until the last ACK of the
	 * set is received. Equally, a server MUST NOT change
	 * the capabilities of the client until the last ACK
	 * of the set has been sent.
	 *
	 * CAP <targ> ACK :[-]<cap_1> [[-]<cap_2> [...]]
	 */

	char *cap;
	char *caps;
	int errors = 0;

	if (!irc_message_param(m, &caps))
		failf(s, "CAP ACK: parameter is null");

	if (!(cap = strsep(&(caps))))
		failf(s, "CAP ACK: parameter is empty");

	do {
		int unset;
		struct ircv3_cap *c;

		if ((unset = (*cap == '-')))
			cap++;

		if (!(c = ircv3_cap_get(&(s->ircv3_caps), cap))) {
			server_error(s, "CAP ACK: '%s' not supported", cap);
			errors++;
			continue;
		}

		if (!c->req) {
			server_error(s, "CAP ACK: '%s%s' was not requested", (unset ? "-" : ""), cap);
			errors++;
			continue;
		}

		if (!unset && c->set) {
			server_error(s, "CAP ACK: '%s' was set", cap);
			errors++;
			continue;
		}

		if (unset && !c->set) {
			server_error(s, "CAP ACK: '%s' was not set", cap);
			errors++;
			continue;
		}

		c->req = 0;
		c->set = !unset;

		server_info(s, "capability change accepted: %s%s", (unset ? "-" : ""), cap);

	} while ((cap = strsep(&(caps))));

	if (errors)
		failf(s, "CAP ACK: parameter errors");

	if (!s->registered && !ircv3_cap_req_count(&(s->ircv3_caps)))
		sendf(s, "CAP END");

	return 0;
}

static int
ircv3_recv_cap_NAK(struct server *s, struct irc_message *m)
{
	/* The server MUST NOT make any change to any
	 * capabilities if it replies with a NAK subcommand.
	 *
	 * CAP <targ> NAK :<cap_1> [<cap_2> [...]]
	 */

	char *cap;
	char *caps;

	if (!irc_message_param(m, &caps))
		failf(s, "CAP NAK: parameter is null");

	if (!(cap = strsep(&(caps))))
		failf(s, "CAP NAK: parameter is empty");

	do {
		struct ircv3_cap *c;

		if ((c = ircv3_cap_get(&(s->ircv3_caps), cap)))
			c->req = 0;

		server_info(s, "capability change rejected: %s", cap);

	} while ((cap = strsep(&(caps))));

	if (!s->registered && !ircv3_cap_req_count(&(s->ircv3_caps)))
		sendf(s, "CAP END");

	return 0;
}

static int
ircv3_recv_cap_DEL(struct server *s, struct irc_message *m)
{
	/* Upon receiving a CAP DEL message, the client MUST
	 * treat the listed capabilities as cancelled and no
	 * longer available. Clients SHOULD NOT send CAP REQ
	 * messages to cancel the capabilities in CAP DEL,
	 * as they have already been cancelled by the server.
	 *
	 * CAP <targ> DEL :<cap_1> [<cap_2> [...]]
	 */

	char *cap;
	char *caps;

	if (!irc_message_param(m, &caps))
		failf(s, "CAP DEL: parameter is null");

	if (!(cap = strsep(&(caps))))
		failf(s, "CAP DEL: parameter is empty");

	do {
		struct ircv3_cap *c;

		if (!(c = ircv3_cap_get(&(s->ircv3_caps), cap)))
			continue;

		if (!c->supports_del)
			failf(s, "CAP DEL: '%s' doesn't support DEL", cap);

		c->req = 0;
		c->set = 0;
		c->supported = 0;

		server_info(s, "capability lost: %s", cap);

	} while ((cap = strsep(&(caps))));

	return 0;
}

static int
ircv3_recv_cap_NEW(struct server *s, struct irc_message *m)
{
	/* Clients that support CAP NEW messages SHOULD respond
	 * with a CAP REQ message if they wish to enable one or
	 * more of the newly-offered capabilities.
	 *
	 * CAP <targ> NEW :<cap_1> [<cap_2> [...]]
	 */

	char *cap;
	char *caps;

	if (!irc_message_param(m, &caps))
		failf(s, "CAP NEW: parameter is null");

	if (!(cap = strsep(&(caps))))
		failf(s, "CAP NEW: parameter is empty");

	do {
		struct ircv3_cap *c;

		if (!(c = ircv3_cap_get(&(s->ircv3_caps), cap)))
			continue;

		c->supported = 1;

		if (!c->set && !c->req && c->req_auto) {
			c->req = 1;
			sendf(s, "CAP REQ :%s", cap);
		}

		server_info(s, "new capability: %s", cap);

	} while ((cap = strsep(&(caps))));

	return 0;
}

static int
ircv3_cap_req_count(struct ircv3_caps *caps)
{
	int ret = 0;
	#define X(CAP, VAR, ATTRS) \
	if (caps->VAR.req) \
		ret++;
	IRCV3_CAPS
	#undef X
	return ret;
}

A src/handlers/ircv3.h => src/handlers/ircv3.h +9 -0
@@ 0,0 1,9 @@
#ifndef IRCV3_H
#define IRCV3_H

#include "src/components/server.h"
#include "src/utils/utils.h"

int ircv3_recv_CAP(struct server*, struct irc_message*);

#endif

M src/io.c => src/io.c +657 -412
@@ 1,6 1,7 @@
#include <ctype.h>
#include "src/io.h"

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


@@ 9,17 10,19 @@
#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 "config.h"
#include "src/io.h"
#include "rirc.h"
#include "utils/utils.h"

#define IO_RECV_SIZE 4096
#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


@@ 62,21 65,34 @@
#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(&io_cb_mutex);   \
		io_cb(__VA_ARGS__); \
		PT_UL(&io_cb_mutex);   \
	} while (0)

/* state transition */
#define ST_X(OLD, NEW) (((OLD) << 3) | (NEW))

#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 100,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 117,151 @@ 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];
	} st_cur, /* current thread state */
	  st_new; /* new thread state */
	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;
	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_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*, unsigned);
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_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 pthread_mutex_t cb_mutex = PTHREAD_MUTEX_INITIALIZER;
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 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 void
io_check_fatal(const char *f, int ret)
{
	if (ret < 0)
		fatal("%s: %s", f, strerror(ret));
}
static const char* io_strerror(char*, size_t);
static int io_net_connect(struct connection*);
static void io_net_close(int);

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;
}
/* 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);

static void
io_soc_close(int *soc)
struct connection*
connection(const void *obj, const char *host, const char *port, uint32_t flags)
{
	if (*soc >= 0 && close(*soc) < 0) {
		fatal("close: %s", strerror(errno));
	}
	*soc = -1;
	struct connection *cx;

	if ((cx = calloc(1U, sizeof(*cx))) == NULL)
		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;
	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));
	PT_UL(&(cx->mtx));

	lock->predicate = 0;

	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)


@@ 246,375 272,387 @@ io_sendf(struct connection *c, const char *fmt, ...)
	if (len >= sizeof(sendbuf) - 2)
		return IO_ERR_TRUNC;

	debug("send: (%zu) %s", len, sendbuf);
	debug_send(len, sendbuf);

	sendbuf[len++] = '\r';
	sendbuf[len++] = '\n';

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

	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);
	} 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;
}

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_tls_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(&io_cb_mutex);
			io_cb_read_inp(buf, ret);
			PT_UL(&io_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)
{
	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_stop(void)
{
	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;

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

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

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

		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));

	if ((cx->soc = io_net_connect(cx)) < 0)
		return IO_ST_RXNG;
	}

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

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

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

		io_soc_close(&soc);
	}

	if (p == NULL) {
		PT_CB(IO_CB_ERR, c->obj, "Error connecting: %s", io_strerror(c, errno));
		freeaddrinfo(res);
	if ((cx->flags & IO_TLS_ENABLED) && io_tls_establish(cx) < 0)
		return IO_ST_RXNG;
	}

	c->soc = soc;

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

		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));

		*c->ip = 0;
	}

	freeaddrinfo(res);
	return IO_ST_CXED;
}

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;

	while ((ret = recv(c->soc, c->read.tmp, sizeof(c->read.tmp), 0)) > 0)
		io_recv(c, c->read.tmp, (size_t) ret);
	while ((ret = io_cx_read(cx, IO_PING_MIN)) > 0)
		continue;

	if (CHECK_BLOCK(errno)) {
	if (ret == MBEDTLS_ERR_SSL_TIMEOUT)
		return IO_ST_PING;
	}

	// 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:
			break;
		case MBEDTLS_ERR_SSL_PEER_CLOSE_NOTIFY:
			io_cb_info(cx, "connection closed gracefully");
			break;
		case MBEDTLS_ERR_NET_CONN_RESET:
		case 0:
			io_cb_err(cx, "connection reset by peer");
			break;
		default:
			io_cb_err(cx, "connection tls error");
			break;
	}

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

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

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 ret;

	for (;;) {
	if (cx->ping >= IO_PING_MAX)
		return IO_ST_CXNG;

		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;
		}
	if ((ret = io_cx_read(cx, IO_PING_REFRESH)) > 0)
		return IO_ST_CXED;

		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 == MBEDTLS_ERR_SSL_TIMEOUT)
		return IO_ST_PING;

		break;
	switch (ret) {
		case MBEDTLS_ERR_SSL_WANT_READ:
		case MBEDTLS_ERR_SSL_WANT_WRITE:
			break;
		case MBEDTLS_ERR_SSL_PEER_CLOSE_NOTIFY:
			io_cb_info(cx, "connection closed gracefully");
			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->tls_fd));
	mbedtls_ssl_config_free(&(cx->tls_conf));
	mbedtls_ssl_free(&(cx->tls_ctx));

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

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_cur = IO_ST_CXNG;

		enum io_state_t (*st_fn)(struct connection*);
	do {
		enum io_state_t st_cur;
		enum io_state_t st_new;

		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 ((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;
			case IO_ST_RXNG: st_new = io_state_rxng(cx); 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);

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

		if (c->st_f != IO_ST_INVALID) {
			c->st_c = c->st_f;
			c->st_f = IO_ST_INVALID;
		} else {
			c->st_c = st_t;
		PT_LK(&(cx->mtx));

		/* 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_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_dxed(cx);
				break;
			case ST_X(IO_ST_RXNG, IO_ST_DXED): /* B1 */
			case ST_X(IO_ST_CXNG, IO_ST_DXED): /* B2 */
				io_cb_info(cx, "Connection cancelled");
				break;
			case ST_X(IO_ST_CXED, IO_ST_DXED): /* B3 */
			case ST_X(IO_ST_PING, IO_ST_DXED): /* B4 */
				io_cb_info(cx, "Connection closed");
				io_cb_dxed(cx);
				break;
			case ST_X(IO_ST_CXNG, IO_ST_CXED): /* D */
				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");
				break;
			case ST_X(IO_ST_CXED, IO_ST_PING): /* G */
				cx->ping = IO_PING_MIN;
				io_cb_ping_1(cx, cx->ping);
				break;
			case ST_X(IO_ST_PING, IO_ST_PING): /* H */
				cx->ping += IO_PING_REFRESH;
				io_cb_ping_n(cx, cx->ping);
				break;
			case ST_X(IO_ST_PING, IO_ST_CXED): /* I */
				cx->ping = 0;
				io_cb_ping_0(cx, cx->ping);
				break;
			default:
				fatal("BAD ST_X from: %d to: %d", st_cur, st_new);
		}

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

		if (st_f == IO_ST_PING && st_t == IO_ST_CXED)
			PT_CB(IO_CB_PING_0, c->obj, 0);

		if (st_f == IO_ST_CXED && st_t == IO_ST_PING)
			PT_CB(IO_CB_PING_1, c->obj, IO_PING_MIN);

		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_t == IO_ST_DXED || (st_f == IO_ST_CXNG && st_t == IO_ST_CXED))
			c->rx_backoff = 0;
	}
	} while (cx->st_cur != IO_ST_DXED);

	return NULL;
}

static void
io_recv(struct connection *c, const char *buf, size_t n)
static int
io_cx_read(struct connection *cx, unsigned timeout)
{
	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);

			ci = 0;
		} else if (ci < IO_MESG_LEN && (isprint(cc) || cc == 0x01)) {
			c->read.buf[ci++] = cc;
	int ret;
	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);
		}
	}

	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


@@ 636,7 674,7 @@ io_tty_init(void)
	if (tcsetattr(STDIN_FILENO, TCSANOW, &nterm) < 0)
		fatal("tcsetattr: %s", strerror(errno));

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



@@ 649,86 687,293 @@ io_tty_term(void)
		fatal_noexit("tcsetattr: %s", strerror(errno));
}

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

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

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

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

		if (ret > 0) {
			PT_LK(&cb_mutex);
			io_cb_read_inp(buf, ret);
			PT_UL(&cb_mutex);
		}
	if ((ret = getaddrinfo(cx->host, cx->port, &hints, &res))) {

		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");
			}
		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;
}

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

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

	errno = errno_save;
}

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

	if (flag_tty_resized == 0) {
		flag_tty_resized = 1;
	return buf;
}

		if (ioctl(0, TIOCGWINSZ, &tty_ws) < 0)
			fatal("ioctl: %s", strerror(errno));
static int
io_tls_establish(struct connection *cx)
{
	int ret;

		io_rows = tty_ws.ws_row;
		io_cols = tty_ws.ws_col;
	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;
}

unsigned
io_tty_cols(void)
static int
io_tls_x509_vrfy(struct connection *cx)
{
	io_tty_winsize();
	return io_cols;
	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;
}

unsigned
io_tty_rows(void)
static const char*
io_tls_err(int err)
{
	io_tty_winsize();
	return io_rows;
	const char *str;

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

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

	return "Unknown error";
}

const char*
io_err(int err)
static void
io_tls_init(void)
{
	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";
	char buf[512];
	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))
		fatal("atexit");

	if (timespec_get(&ts, TIME_UTC) != TIME_UTC)
		fatal("timespec_get");

	if (snprintf(buf, sizeof(buf), "rirc-%lu-%lu", ts.tv_sec, ts.tv_nsec) < 0)
		fatal("snprintf");

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

	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
io_tls_term(void)
{
	/* Exit handler, must return normally */

	mbedtls_ctr_drbg_free(&tls_ctr_drbg);
	mbedtls_entropy_free(&tls_entropy);
	mbedtls_x509_crt_free(&tls_x509_crt);
}

M src/io.h => src/io.h +76 -51
@@ 12,33 12,33 @@
 *  - cxed: connected    ~ Socket connected
 *  - ping: timing out   ~ Socket connected, network state in question
 *
 *                            +--------+
 *                 +----(B)-- |  rxng  |
 *                 |          +--------+
 *                 |           |      ^
 *   INIT          |         (A,C)    |
 *    v            |           |     (E)
 *    |            v           v      |
 *    |    +--------+ --(A)-> +--------+
 *    +--> |  dxed  |         |  cxng  | <--+
 *         +--------+ <-(B)-- +--------+    |
 *          ^      ^           |      ^    (F)
 *          |      |          (D)     |     |
 *          |      |           |     (F)    |
 *          |      |           v      |     |
 *          |      |          +--------+    |
 *          |      +----(B)-- |  cxed  |    |
 *          |                 +--------+    |
 *          |                  |      ^     |
 *          |                 (G)     |     |
 *          |                  |     (I)    |
 *          |                  v      |     |
 *          |                 +--------+    |
 *          +-----------(B)-- |  ping  | ---+
 *                            +--------+
 *                             v      ^
 *                             |      |
 *                             +--(H)-+
 *                             +--------+
 *                 +----(B1)-- |  rxng  |
 *                 |           +--------+
 *                 |            |      ^
 *   INIT          |         (A2,C)    |
 *    v            |            |     (E)
 *    |            v            v      |
 *    |    +--------+ --(A1)-> +--------+
 *    +--> |  dxed  |          |  cxng  | <--+
 *         +--------+ <-(B2)-- +--------+    |
 *          ^      ^            |      ^   (F2)
 *          |      |           (D)     |     |
 *          |      |            |    (F1)    |
 *          |      |            v      |     |
 *          |      |           +--------+    |
 *          |      +----(B3)-- |  cxed  |    |
 *          |                  +--------+    |
 *          |                   |      ^     |
 *          |                  (G)     |     |
 *          |                   |     (I)    |
 *          |                   v      |     |
 *          |                  +--------+    |
 *          +-----------(B4)-- |  ping  | ---+
 *                             +--------+
 *                              v      ^
 *                              |      |
 *                              +--(H)-+
 *
 * This module exposes functions for explicitly directing network
 * state as well declaring callback functions for state transitions


@@ 69,25 69,21 @@
 *   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
#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,
	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> */


@@ 97,13 93,38 @@ 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 io_free(struct connection*);
void connection_free(struct connection*);

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


@@ 112,16 133,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 145,14 @@ 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*);

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

#endif

M src/rirc.c => src/rirc.c +118 -49
@@ 7,55 7,64 @@
#include <unistd.h>

#include "config.h"
#include "src/draw.h"
#include "src/io.h"
#include "src/state.h"

#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);
static const char* getpwuid_pw_name(void);
static int parse_args(int, char**);

#ifndef DEBUG
const char *runtime_name = "rirc";
#ifdef CA_CERT_PATH
const char *ca_cert_path = CA_CERT_PATH;
#else
const char *runtime_name = "rirc.debug";
#error "CA_CERT_PATH required"
#endif

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

#ifndef DEFAULT_USERNAME
#ifdef DEFAULT_USERNAME
const char *default_username = DEFAULT_USERNAME;
#else
const char *default_username;
#endif

#ifndef DEFAULT_REALNAME
#ifdef DEFAULT_REALNAME
const char *default_realname = DEFAULT_REALNAME;
#else
const char *default_realname;
#endif

static const char *const rirc_usage =
#ifndef NDEBUG
const char *runtime_name = "rirc.debug";
#else
const char *runtime_name = "rirc";
#endif

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"


@@ 63,10 72,16 @@ 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 =
#ifdef DEBUG
#ifndef NDEBUG
"rirc v"VERSION" (debug build)";
#else
"rirc v"VERSION;


@@ 83,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);
	}


@@ 110,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}
	};



@@ 138,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[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))) {

		switch (opt_c) {


@@ 151,58 171,92 @@ 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].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':


@@ 233,9 287,18 @@ parse_args(int argc, char **argv)
		default_realname = getpwuid_pw_name();

	state_init();
	draw_init();

	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,


@@ 244,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);


@@ 260,6 323,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);



@@ 272,9 337,13 @@ main(int argc, char **argv)
{
	int ret;

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

	srand(time(NULL));

	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 +107 -77
@@ 12,19 12,19 @@
#include <stdio.h>

#include "src/draw.h"
#include "src/handlers/irc_recv.h"
#include "src/handlers/irc_send.h"
#include "src/io.h"
#include "src/rirc.h"
#include "src/state.h"
#include "src/utils/utils.h"
#include "src/handlers/irc_recv.h"
#include "src/handlers/irc_send.h"

/* See: https://vt100.net/docs/vt100-ug/chapter3.html */
#define CTRL(k) ((k) & 0x1f)

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);



@@ 43,7 43,6 @@ static struct
	struct channel *current_channel; /* the current channel being drawn */
	struct channel *default_channel; /* the default rirc channel at startup */
	struct server_list servers;
	union draw draw;
} state;

struct server_list*


@@ 60,6 59,8 @@ current_channel(void)

/* List of IRC commands for tab completion */
static const char *irc_list[] = {
	"cap-ls",
	"cap-list",
	"ctcp-action",
	"ctcp-clientinfo",
	"ctcp-finger",


@@ 79,26 80,7 @@ static const char *irc_list[] = {
// TODO: from command handler list
/* List of rirc commands for tab completeion */
static const char *cmd_list[] = {
	"clear", "close", "connect", "quit", "set", NULL};

/* Set draw bits */
#define X(BIT) void draw_##BIT(void) { state.draw.bits.BIT = 1; }
DRAW_BITS
#undef X

void
draw_all(void)
{
	state.draw.all_bits = -1;
}

void
redraw(void)
{
	draw(state.draw);

	state.draw.all_bits = 0;
}
	"clear", "close", "connect", "disconnect", "quit", "set", NULL};

void
state_init(void)


@@ 113,7 95,7 @@ state_init(void)
	newline(state.default_channel, 0, "--", "");
	newline(state.default_channel, 0, "--", " - version " VERSION);
	newline(state.default_channel, 0, "--", " - compiled " __DATE__ ", " __TIME__);
#ifdef DEBUG
#ifndef NDEBUG
	newline(state.default_channel, 0, "--", " - compiled with DEBUG flags");
#endif
}


@@ 133,7 115,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);
}


@@ 205,10 187,10 @@ _newline(struct channel *c, enum buffer_line_t type, const char *from, const cha
		prefix);

	if (c == current_channel()) {
		draw_buffer();
		draw(DRAW_BUFFER);
	} else {
		c->activity = MAX(c->activity, ACTIVITY_ACTIVE);
		draw_nav();
		draw(DRAW_NAV);
	}
}



@@ 252,7 234,7 @@ void
channel_clear(struct channel *c)
{
	memset(&(c->buffer), 0, sizeof(c->buffer));
	draw_buffer();
	draw(DRAW_BUFFER);
}

/* WIP:


@@ 337,11 319,11 @@ 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);

		draw_all();
		draw(DRAW_ALL);

		return 1;
	}


@@ 369,13 351,13 @@ action(int (*a_handler)(char), const char *fmt, ...)
	} else {
		action_handler = a_handler;
		action_message = action_buff;
		draw_input();
		draw(DRAW_INPUT);
	}
}
/* 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 411,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


@@ 480,7 462,7 @@ channel_close(struct channel *c)
		if (c == current_channel()) {
			channel_set_current(c->next);
		} else {
			draw_nav();
			draw(DRAW_NAV);
		}

		channel_list_del(&c->server->clist, c);


@@ 512,7 494,7 @@ buffer_scrollback_back(struct channel *c)
	/* Find top line */
	for (;;) {

		split_buffer_cols(line, NULL, &text_w, cols, b->pad);
		buffer_line_split(line, NULL, &text_w, cols, b->pad);

		count += buffer_line_rows(line, text_w);



@@ 531,8 513,8 @@ buffer_scrollback_back(struct channel *c)
	if (count == rows && line != buffer_tail(b))
		b->scrollback--;

	draw_buffer();
	draw_status();
	draw(DRAW_BUFFER);
	draw(DRAW_STATUS);
}

void


@@ 556,7 538,7 @@ buffer_scrollback_forw(struct channel *c)
	/* Find top line */
	for (;;) {