cio

a simple irc client
Download | Log | Files | Refs | README | LICENSE

cio.c (23798B)


      1 /* See LICENSE file for copyright and license details.
      2  *
      3  * cio is driven by a synchronous select(2) loop. It multiplexes stdin, the
      4  * network socket, and SIGWINCH for terminal resizing. Each channel's scrollback
      5  * is stored in an exponentially-reallocating heap buffer.
      6  *
      7  * Channels are organized in a fixed-size array. The '0' channel is reserved for
      8  * server messages (status), while others are created dynamically upon JOIN or
      9  * PRIVMSG. IRC protocol parsing is handled via a string tokenizer, dispatching
     10  * commands through a lookup table in scmd().
     11  *
     12  * UI rendering is handled by ncurses. To support UTF-8, the client manually
     13  * decodes runes before passing them to the wide-character add_wch function.
     14  *
     15  * To understand the lifecycle start reading main().
     16  */
     17 #include <assert.h>
     18 #include <ctype.h>
     19 #include <limits.h>
     20 #include <signal.h>
     21 #include <stdio.h>
     22 #include <stdlib.h>
     23 #include <stdarg.h>
     24 #include <string.h>
     25 #include <time.h>
     26 #include <errno.h>
     27 
     28 #include <curses.h>
     29 #include <unistd.h>
     30 #include <arpa/inet.h>
     31 #include <sys/types.h>
     32 #include <sys/socket.h>
     33 #include <sys/select.h>
     34 #include <sys/ioctl.h>
     35 #include <netinet/in.h>
     36 #include <netinet/tcp.h>
     37 #include <netdb.h>
     38 #include <locale.h>
     39 #include <openssl/ssl.h>
     40 #include <openssl/x509v3.h>
     41 
     42 #undef CTRL
     43 #define CTRL(x)	(x & 037)
     44 #define GET_ARG(i)	((argc > (i)) ? argv[i] : "")
     45 
     46 #define SCROLL   15
     47 #define INDENT   23
     48 #define DATEFMT  "%H:%M"
     49 #define PFMT     "  %-12s < %s"
     50 #define PFMTHIGH "> %-12s < %s"
     51 #define SRV      "irc.oftc.net"
     52 #define PORT     "6697"
     53 
     54 typedef enum {
     55 	CMD_UNKNOWN, NOTICE, PRIVMSG, PING, PONG, PART, JOIN, QUIT, NICK, CAP,
     56 	AUTHENTICATE, RPL_TOPIC, RPL_TOPICWHOTIME, RPL_NAMREPLY, RPL_ENDOFNAMES,
     57 	ERR_NICKNAMEINUSE, ERR_CHANNELISFULL, ERR_INVITEONLYCHAN, ERR_BADCHANNELKEY,
     58 	ERR_NOCHANMODES, SASL_OK, SASL_ERR
     59 } IrcCmd;
     60 
     61 struct {
     62 	const char *name;
     63 	IrcCmd id;
     64 } cmd_map[] = {
     65 	{ "NOTICE", NOTICE }, { "PRIVMSG", PRIVMSG }, { "PING", PING },
     66 	{ "PONG", PONG }, { "PART", PART }, { "JOIN", JOIN }, { "QUIT", QUIT },
     67 	{ "NICK", NICK}, { "CAP", CAP }, { "AUTHENTICATE", AUTHENTICATE },
     68 	{ "332", RPL_TOPIC }, { "333", RPL_TOPICWHOTIME }, { "353", RPL_NAMREPLY },
     69 	{ "366", RPL_ENDOFNAMES }, { "477", ERR_NOCHANMODES },
     70 	{ "433", ERR_NICKNAMEINUSE }, { "471", ERR_CHANNELISFULL },
     71 	{ "473", ERR_INVITEONLYCHAN }, { "475", ERR_BADCHANNELKEY },
     72 	{ "903", SASL_OK }, { "904", SASL_ERR }
     73 };
     74 
     75 enum {
     76 	ChanLen		= 64,
     77 	LineLen		= 512,
     78 	MaxChans	= 16,
     79 	MaxParams	= 15,
     80 	BufSz		= 2048,
     81 	LogSz		= 4096,
     82 	MaxRecons	= 10, /* -1 for infinitely many */
     83 	PingDelay	= 6,
     84 	UtfSz		= 4,
     85 	RuneInvalid	= 0xFFFD,
     86 };
     87 
     88 typedef wchar_t Rune;
     89 
     90 static struct {
     91 	size_t x, y;
     92 	WINDOW *sw, *mw, *iw;
     93 } scr;
     94 
     95 static struct Chan {
     96 	char name[ChanLen];
     97 	char *buf, *eol;
     98 	int n;     /* Scroll offset. */
     99 	size_t sz; /* Size of buf. */
    100 	char high; /* Nick highlight. */
    101 	char new;  /* New message. */
    102 	char join; /* Channel was 'j'-oined. */
    103 } chl[MaxChans];
    104 
    105 static int ssl = 1;
    106 static int sslverify = 1;
    107 static struct {
    108 	int fd;
    109 	SSL *ssl;
    110 	SSL_CTX *ctx;
    111 } srv;
    112 static char nick[64];
    113 static char cert[PATH_MAX];
    114 static char key[512];
    115 static int quit, winchg;
    116 static int nch, ch; /* Current number of channels, and current channel. */
    117 static char outb[BufSz], *outp = outb; /* Output buffer. */
    118 static FILE *logfp;
    119 
    120 static const unsigned char utfbyte[UtfSz + 1] = {0x80, 0, 0xC0, 0xE0, 0xF0};
    121 static const unsigned char utfmask[UtfSz + 1] = {0xC0, 0x80, 0xE0, 0xF0, 0xF8};
    122 static const Rune utfmin[UtfSz + 1] = {0, 0, 0x80, 0x800, 0x10000};
    123 static const Rune utfmax[UtfSz + 1] = {0x10FFFF, 0x7F, 0x7FF, 0xFFFF, 0x10FFFF};
    124 
    125 static void scmd(char *, char *, int, char **);
    126 static void tdrawbar(void);
    127 static void tredraw(void);
    128 static void treset(void);
    129 
    130 static void
    131 die(const char *fmt, ...)
    132 {
    133 	va_list ap;
    134 
    135 	treset();
    136 	va_start(ap, fmt);
    137 	vfprintf(stderr, fmt, ap);
    138 	va_end(ap);
    139 	if (fmt[0] && fmt[strlen(fmt)-1] == ':')
    140 		fprintf(stderr, " %s", strerror(errno));
    141 	fputc('\n', stderr);
    142 	exit(1);
    143 }
    144 
    145 static size_t
    146 utf8validate(Rune *u, size_t i)
    147 {
    148 	if (*u < utfmin[i] || *u > utfmax[i] || (0xD800 <= *u && *u <= 0xDFFF))
    149 		*u = RuneInvalid;
    150 	for (i = 1; *u > utfmax[i]; ++i)
    151 		;
    152 	return i;
    153 }
    154 
    155 static Rune
    156 utf8decodebyte(unsigned char c, size_t *i)
    157 {
    158 	for (*i = 0; *i < UtfSz + 1; ++(*i))
    159 		if ((c & utfmask[*i]) == utfbyte[*i])
    160 			return c & ~utfmask[*i];
    161 	return 0;
    162 }
    163 
    164 static size_t
    165 utf8decode(const char *c, Rune *u, size_t clen)
    166 {
    167 	size_t i, j, len, type;
    168 	Rune udecoded;
    169 
    170 	*u = RuneInvalid;
    171 	if (!clen)
    172 		return 0;
    173 	udecoded = utf8decodebyte(c[0], &len);
    174 	if (len < 1 || len > UtfSz)
    175 		return 1;
    176 	for (i = 1, j = 1; i < clen && j < len; ++i, ++j) {
    177 		udecoded = (udecoded << 6) | utf8decodebyte(c[i], &type);
    178 		if (type != 0)
    179 			return j;
    180 	}
    181 	if (j < len)
    182 		return 0;
    183 	*u = udecoded;
    184 	utf8validate(u, len);
    185 	return len;
    186 }
    187 
    188 static int
    189 empty(const char *str) {
    190 	return (str == NULL || str[0] == '\0');
    191 }
    192 
    193 static char *
    194 b64_enc(const unsigned char *in, size_t len)
    195 {
    196 	static char out[BufSz];
    197 	const char *tab =
    198 		"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
    199 	char *p = out;
    200 	size_t i;
    201 
    202 	for (i = 0; i < len; i += 3) {
    203 		unsigned int v =
    204 			in[i] << 16 | (i+1 < len ? in[i+1] << 8 : 0) | (i+2 < len ? in[i+2] : 0);
    205 		*p++ = tab[(v >> 18) & 0x3F];
    206 		*p++ = tab[(v >> 12) & 0x3F];
    207 		*p++ = (i+1 < len) ? tab[(v >> 6) & 0x3F] : '=';
    208 		*p++ = (i+2 < len) ? tab[v & 0x3F] : '=';
    209 	}
    210 	*p = '\0';
    211 	return out;
    212 }
    213 
    214 static void
    215 sndf(const char *fmt, ...)
    216 {
    217 	va_list vl;
    218 	size_t n, l = BufSz - (outp - outb);
    219 
    220 	if (l < 2)
    221 		return;
    222 	va_start(vl, fmt);
    223 	n = vsnprintf(outp, l - 2, fmt, vl);
    224 	va_end(vl);
    225 	outp += n > l - 2 ? l - 2 : n;
    226 	*outp++ = '\r';
    227 	*outp++ = '\n';
    228 }
    229 
    230 static int
    231 srd(void)
    232 {
    233 	static char l[BufSz], *p = l;
    234 	char *s, *usr, *cmd, *argv[MaxParams];
    235 	int rd, argc;
    236 
    237 	if (p - l >= BufSz)
    238 		p = l; /* Input buffer overflow, there should something better to do. */
    239 	if (ssl)
    240 		rd = SSL_read(srv.ssl, p, BufSz - (p - l));
    241 	else
    242 		rd = read(srv.fd, p, BufSz - (p - l));
    243 	if (rd <= 0) return 0;
    244 	p += rd;
    245 	for (;;) { /* Cycle on all received lines. */
    246 		if (!(s = memchr(l, '\n', p - l)))
    247 			return 1;
    248 		if (s > l && s[-1] == '\r') s[-1] = 0;
    249 		*s++ = 0;
    250 		char *tok = l;
    251 		usr = NULL;
    252 		if (*tok == ':') {
    253 			usr = tok + 1;
    254 			if (!(tok = strchr(tok, ' '))) goto lskip;
    255 			*tok++ = 0;
    256 		}
    257 		while (*tok == ' ') tok++;
    258 		cmd = tok;
    259 		if ((tok = strchr(tok, ' ')))
    260 			*tok++ = 0;
    261 		argc = 0;
    262 		while (tok && *tok && argc < MaxParams) {
    263 			while (*tok == ' ') tok++;
    264 			if (*tok == ':') {
    265 				argv[argc++] = tok + 1;
    266 				break;
    267 			}
    268 			argv[argc++] = tok;
    269 			if ((tok = strchr(tok, ' ')))
    270 				*tok++ = 0;
    271 		}
    272 		if (cmd) scmd(usr, cmd, argc, argv);
    273 	lskip:
    274 		memmove(l, s, p - s);
    275 		p -= s - l;
    276 	}
    277 }
    278 
    279 static void
    280 sinit(const char *nick, const char *user)
    281 {
    282 	sndf("CAP LS 302");
    283 	sndf("NICK %s", nick);
    284 	sndf("USER %s 8 * :%s", user, user);
    285 }
    286 
    287 static char *
    288 dial(const char *host, const char *service)
    289 {
    290 	struct addrinfo hints, *res = NULL, *rp;
    291 	int fd = -1;
    292 
    293 	memset(&hints, 0, sizeof(hints));
    294 	hints.ai_family = AF_UNSPEC;     /* allow IPv4 or IPv6 */
    295 	hints.ai_flags = AI_NUMERICSERV; /* avoid name lookup for port */
    296 	hints.ai_socktype = SOCK_STREAM;
    297 	if (getaddrinfo(host, service, &hints, &res))
    298 		return "getaddrinfo failed";
    299 	for (rp = res; rp; rp = rp->ai_next) {
    300 		fd = socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol);
    301 		if (fd == -1)
    302 			continue;
    303 		if (connect(fd, rp->ai_addr, rp->ai_addrlen) == 0)
    304 			break;
    305 		close(fd);
    306 		fd = -1;
    307 	}
    308 	freeaddrinfo(res);
    309 	if ((srv.fd = fd) == -1)
    310 		return "cannot connect to host";
    311 	if (ssl) {
    312 		SSL_load_error_strings();
    313 		SSL_library_init();
    314 		srv.ctx = SSL_CTX_new(TLS_client_method());
    315 		if (!srv.ctx) return "could not initialize ssl context";
    316 		if (sslverify) {
    317 			SSL_CTX_set_verify(srv.ctx, SSL_VERIFY_PEER, NULL);
    318 			if (SSL_CTX_set_default_verify_paths(srv.ctx) != 1)
    319 				return "could not load default system trust store";
    320 		}
    321 		srv.ssl = SSL_new(srv.ctx);
    322 		if (!srv.ssl) return "could not create SSL object";
    323 		if (sslverify) {
    324 			SSL_set_hostflags(srv.ssl, X509_CHECK_FLAG_NO_PARTIAL_WILDCARDS);
    325 			if (!SSL_set1_host(srv.ssl, host))
    326 				return "could not set expected hostname";
    327 			SSL_set_tlsext_host_name(srv.ssl, host);
    328 		} else {
    329 			SSL_CTX_set_verify(srv.ctx, SSL_VERIFY_NONE, NULL);
    330 		}
    331 		if (!empty(cert)) {
    332 			if (SSL_use_certificate_chain_file(srv.ssl, cert) <= 0)
    333 				return "failed to load certificate chain";
    334 			if (SSL_use_PrivateKey_file(srv.ssl, cert, SSL_FILETYPE_PEM) <= 0)
    335 				return "failed to load private key";
    336 		}
    337 		SSL_set_fd(srv.ssl, srv.fd);
    338 		if (SSL_connect(srv.ssl) <= 0)  {
    339 			long verify_err = SSL_get_verify_result(srv.ssl);
    340 			if (verify_err != X509_V_OK)
    341 				return (char *)X509_verify_cert_error_string(verify_err);
    342 			return "SSL handshake failed";
    343 		}
    344 	}
    345 	return 0;
    346 }
    347 
    348 static void
    349 hangup(void)
    350 {
    351 	if (srv.ssl) SSL_shutdown(srv.ssl);
    352 	SSL_free(srv.ssl);
    353 	if (srv.fd > 0) close(srv.fd);
    354 	SSL_CTX_free(srv.ctx);
    355 	memset(&srv, 0, sizeof(srv));
    356 }
    357 
    358 static inline int
    359 chfind(const char *name)
    360 {
    361 	int i;
    362 
    363 	assert(name);
    364 	for (i = nch - 1; i > 0; i--)
    365 		if (!strcasecmp(chl[i].name, name))
    366 			break;
    367 	return i;
    368 }
    369 
    370 static int
    371 chadd(const char *name, int joined)
    372 {
    373 	int n;
    374 
    375 	if (nch >= MaxChans || strlen(name) >= ChanLen)
    376 		return -1;
    377 	if ((n = chfind(name)) > 0)
    378 		return n;
    379 	strlcpy(chl[nch].name, name, sizeof(chl[nch].name));
    380 	chl[nch].sz = LogSz;
    381 	chl[nch].buf = malloc(LogSz);
    382 	if (!chl[nch].buf)
    383 		die("cio: malloc:");
    384 	chl[nch].eol = chl[nch].buf;
    385 	chl[nch].n = 0;
    386 	chl[nch].join = joined;
    387 	if (joined)
    388 		ch = nch;
    389 	nch++;
    390 	tdrawbar();
    391 	return nch - 1;
    392 }
    393 
    394 static int
    395 chdel(const char *name)
    396 {
    397 	int n = chfind(name);
    398 	if (n <= 0) return 0;
    399 	free(chl[n].buf);
    400 
    401 	if (n < nch - 1)
    402 		memmove(&chl[n], &chl[n + 1], (nch - n - 1) * sizeof(struct Chan));
    403 	nch--;
    404 	if (ch > n || ch >= nch)
    405 		ch = (ch > 0) ? ch - 1 : 0;
    406 	tdrawbar();
    407 	return 1;
    408 }
    409 
    410 static char *
    411 pushl(char *p, char *e)
    412 {
    413 	size_t x;
    414 	char *w;
    415 	Rune u[2];
    416 	cchar_t cc;
    417 
    418 	u[1] = 0;
    419 	if ((w = memchr(p, '\n', e - p)))
    420 		e = w + 1;
    421 	w = p;
    422 	x = 0;
    423 	for (;;) {
    424 		if (x >= scr.x) {
    425 			waddch(scr.mw, '\n');
    426 			for (x = 0; x < INDENT; x++)
    427 				waddch(scr.mw, ' ');
    428 			if (*w == ' ')
    429 				w++;
    430 			x += p - w;
    431 		}
    432 		if (p >= e || *p == ' ' || p - w + INDENT >= (ptrdiff_t)scr.x - 1) {
    433 			while (w < p) {
    434 				w += utf8decode(w, u, UtfSz);
    435 				if (wcwidth(*u) > 0 || *u == '\n') {
    436 					setcchar(&cc, u, 0, 0, 0);
    437 					wadd_wch(scr.mw, &cc);
    438 				}
    439 			}
    440 			if (p >= e) return e;
    441 		}
    442 		p += utf8decode(p, u, UtfSz);
    443 		int cl = wcwidth(*u);
    444 		if (cl >= 0) x += cl;
    445 	}
    446 }
    447 
    448 static void
    449 pushf(int cn, const char *fmt, ...)
    450 {
    451 	struct Chan *const c = &chl[cn];
    452 	size_t n, blen = c->eol - c->buf;
    453 	va_list vl;
    454 	time_t t;
    455 	char *s;
    456 	const struct tm *tm, *gmtm;
    457 
    458 	if (blen + LineLen >= c->sz) {
    459 		c->sz *= 2;
    460 		c->buf = realloc(c->buf, c->sz);
    461 		if (!c->buf)
    462 			die("cio: realloc:");
    463 		c->eol = c->buf + blen;
    464 	}
    465 	t = time(0);
    466 	if (!(tm = localtime(&t)))
    467 		die("cio: localtime failed");
    468 	n = strftime(c->eol, LineLen, DATEFMT, tm);
    469 	if (!(gmtm = gmtime(&t)))
    470 		die("cio: gmtime failed");
    471 	c->eol[n++] = ' ';
    472 	va_start(vl, fmt);
    473 	s = c->eol + n;
    474 	n += vsnprintf(s, LineLen - n - 1, fmt, vl);
    475 	va_end(vl);
    476 	if (logfp) {
    477 		fprintf(logfp, "%-12.12s\t%04d-%02d-%02dT%02d:%02d:%02dZ\t%s\n",
    478 			c->name,
    479 			gmtm->tm_year + 1900, gmtm->tm_mon + 1, gmtm->tm_mday,
    480 			gmtm->tm_hour, gmtm->tm_min, gmtm->tm_sec, s);
    481 		fflush(logfp);
    482 	}
    483 	strlcat(c->buf, "\n", c->sz);
    484 	c->eol = c->buf + strlen(c->buf);
    485 	if (cn == ch && c->n == 0) {
    486 		char *p = c->eol - n - 1;
    487 
    488 		if (p != c->buf)
    489 			waddch(scr.mw, '\n');
    490 		pushl(p, c->eol - 1);
    491 		wrefresh(scr.mw);
    492 	}
    493 }
    494 
    495 static IrcCmd get_cmd_type(const char *cmd) {
    496 	for (size_t i = 0; i < sizeof(cmd_map)/sizeof(cmd_map[0]); i++)
    497 		if (!strcmp(cmd, cmd_map[i].name)) return cmd_map[i].id;
    498 	return CMD_UNKNOWN;
    499 }
    500 
    501 static void
    502 scmd(char *usr, char *cmd, int argc, char **argv)
    503 {
    504 	IrcCmd type = get_cmd_type(cmd);
    505 	int c = 0;
    506 
    507 	if (!usr) usr = "?";
    508 	else {
    509 		char *bang = strchr(usr, '!');
    510 		if (bang) *bang = '\0';
    511 	}
    512 	switch (type) {
    513 	case NOTICE:
    514 		if (argc < 2) break;
    515 		c = strchr("&#!+.~", argv[0][0]) ? chfind(argv[0]) : 0;
    516 		if (c < 0) c = 0;
    517 		pushf(c, PFMT, usr, argv[1]);
    518 		if (ch != c) {
    519 			chl[c].new = 1;
    520 			tdrawbar();
    521 		}
    522 		break;
    523 	case PRIVMSG:
    524 		if (argc < 2) break;
    525 		char *chan = strchr("&#!+.~", argv[0][0]) ? argv[0] : (usr ? usr : "server");
    526 		if (!(c = chfind(chan)) && (c = chadd(chan, 0)) < 0)
    527 			break;
    528 		if (strcasestr(argv[1], nick)) {
    529 			pushf(c, PFMTHIGH, usr, argv[1]);
    530 			chl[c].high |= (ch != c);
    531 		} else
    532 			pushf(c, PFMT, usr, argv[1]);
    533 		if (ch != c) {
    534 			chl[c].new = 1;
    535 			tdrawbar();
    536 		}
    537 		break;
    538 	case PING:
    539 		if (argc >= 2)
    540 			sndf("PONG %s :%s", argv[0], argv[1]);
    541 		else if (argc == 1)
    542 			sndf("PONG :%s", argv[0]);
    543 		else
    544 			sndf("PONG :%s", chl[0]);
    545 		break;
    546 	case PONG:
    547 		break;
    548 	case JOIN:
    549 		if (argc < 1 || !usr) break;
    550 		c = chfind(argv[0]);
    551 		if (!strcasecmp(usr, nick)) {
    552 			if (c <= 0)
    553 				c = chadd(argv[0], 1);
    554 			if (c > 0) {
    555 				pushf(c, "-!- You have joined %s", argv[0]);
    556 				tdrawbar();
    557 				tredraw();
    558 			}
    559 		} else if (c > 0) {
    560 			pushf(c, "-!- %s has joined %s", usr, argv[0]);
    561 		}
    562 		break;
    563 	case PART:
    564 		if (argc < 1 || !usr) break;
    565 		c = chfind(argv[0]);
    566 		if (c <= 0) break;
    567 		if (!strcasecmp(usr, nick)) {
    568 			pushf(0, "-!- You have left %s", argv[0]);
    569 			chdel(argv[0]);
    570 			tredraw();
    571 		} else {
    572 			pushf(c, "-!- %s has left %s %s", usr, argv[0], GET_ARG(1));
    573 		}
    574 		break;
    575 	case QUIT:
    576 		break;
    577 	case NICK:
    578 		if (argc < 1) break;
    579 		if (!strcasecmp(usr, nick)) {
    580 			strlcpy(nick, argv[0], sizeof(nick));
    581 			pushf(0, "-!- You are now known as %s", nick);
    582 		} else
    583 			pushf(0, "-!- %s is now known as %s", usr, argv[0]);
    584 		break;
    585 	case RPL_TOPIC:
    586 		if (argc >= 3) pushf(chfind(argv[1]), "-!- Topic: %s", argv[2]);
    587 		break;
    588 	case RPL_NAMREPLY:
    589 		if (argc >= 4) pushf(chfind(argv[2]), "-!- Names: %s", argv[3]);
    590 		break;
    591 	case RPL_TOPICWHOTIME:
    592 	case RPL_ENDOFNAMES:
    593 		break;
    594 	case ERR_NOCHANMODES:
    595 	case ERR_CHANNELISFULL:
    596 	case ERR_INVITEONLYCHAN:
    597 	case ERR_BADCHANNELKEY:
    598 		if (argc < 2) break;
    599 		chdel(argv[1]);
    600 		pushf(0, "-!- Cannot join %s (%s)", argv[1], cmd);
    601 		tredraw();
    602 		break;
    603 	case ERR_NICKNAMEINUSE:
    604 		strlcat(nick, "_", sizeof(nick));
    605 		sndf("NICK %s", nick);
    606 		break;
    607 	case CAP:
    608 		if (argc < 2) break;
    609 		if (!strcmp(argv[1], "LS"))
    610 			sndf((!empty(key) || !empty(cert)) && argc > 2 && strstr(argv[2], "sasl")
    611 				? "CAP REQ :sasl"
    612 				: "CAP END");
    613 		else if (!strcmp(argv[1], "ACK") && argc > 2 && strstr(argv[2], "sasl"))
    614 			sndf(empty(cert) ? "AUTHENTICATE PLAIN" : "AUTHENTICATE EXTERNAL");
    615 		else if (!strcmp(argv[1], "NAK") || !strcmp(argv[1], "ACK"))
    616 			sndf("CAP END");
    617 		break;
    618 	case AUTHENTICATE:
    619 		if (argc < 1 || strcmp(argv[0], "+")) break;
    620 		if (!empty(cert)) {
    621 			sndf("AUTHENTICATE +");
    622 		} else if (!empty(usr) && !empty(key)) {
    623 			unsigned char raw[512];
    624 			size_t u_len = strlen(usr);
    625 			size_t k_len = strlen(key);
    626 			raw[0] = '\0';
    627 			memcpy(raw + 1, usr, u_len);
    628 			raw[1 + u_len] = '\0';
    629 			memcpy(raw + 2 + u_len, key, k_len);
    630 			sndf("AUTHENTICATE %s", b64_enc(raw, u_len + k_len + 2));
    631 		}
    632 		break;
    633 	case SASL_OK:
    634 	case SASL_ERR:
    635 		sndf("CAP END");
    636 		pushf(0, "-!- SASL auth %s", (type == SASL_OK ? "successful" : "failed"));
    637 		break;
    638 	default:
    639 		if (isdigit(cmd[0]))
    640 			pushf(0, "%s: %s", cmd, GET_ARG(argc - 1));
    641 		else
    642 			pushf(0, "%s: %s %s", cmd, GET_ARG(0), GET_ARG(1));
    643 		break;
    644 	}
    645 }
    646 
    647 static void
    648 uparse(char *m)
    649 {
    650 	char *p = m;
    651 
    652 	if (!p[0] || (p[1] != ' ' && p[1] != 0)) {
    653 	pmsg:
    654 		if (ch == 0) return;
    655 		m += strspn(m, " ");
    656 		if (!*m) return;
    657 		pushf(ch, PFMT, nick, m);
    658 		sndf("PRIVMSG %s :%s", chl[ch].name, m);
    659 		return;
    660 	}
    661 	switch (*p) {
    662 	case 'j': /* Join channels. */
    663 		p += 1 + (p[1] == ' ');
    664 		for (char *token = strtok(p, " "); token; token = strtok(NULL, " ")) {
    665 			if (chadd(token, 1) < 0)
    666 				break;
    667 			if (strchr("&#!+", token[0]))
    668 				sndf("JOIN %s", token);
    669 		}
    670 		tredraw();
    671 		return;
    672 	case 'l': /* Leave channels. */
    673 		p += 1 + (p[1] == ' ');
    674 		if (!*p) {
    675 			if (ch == 0) return; /* Cannot leave server window */
    676 			static char buf[ChanLen];
    677 			strlcpy(buf, chl[ch].name, sizeof(buf));
    678 			p = buf;
    679 		}
    680 		for (char *token = strtok(p, " "); token; token = strtok(NULL, " "))
    681 			if (chdel(token) && strchr("&#!+", token[0]))
    682 				sndf("PART %s", token);
    683 		tredraw();
    684 		return;
    685 	case 'm': /* Private message. */
    686 		m = p + 1 + (p[1] == ' ');
    687 		if (!(p = strchr(m, ' ')))
    688 			return;
    689 		*p++ = 0;
    690 		sndf("PRIVMSG %s :%s", m, p);
    691 		return;
    692 	case 'r': /* Send raw. */
    693 		if (p[1])
    694 			sndf("%s", &p[2]);
    695 		return;
    696 	case 'q': /* Quit. */
    697 		quit = 1;
    698 		return;
    699 	default: /* Send on current channel. */
    700 		goto pmsg;
    701 	}
    702 }
    703 
    704 static void
    705 sigwinch(int sig)
    706 {
    707 	if (sig) winchg = 1;
    708 }
    709 
    710 static void
    711 tinit(void)
    712 {
    713 	setlocale(LC_ALL, "");
    714 	signal(SIGWINCH, sigwinch);
    715 	initscr();
    716 	raw();
    717 	noecho();
    718 	getmaxyx(stdscr, scr.y, scr.x);
    719 	if (scr.y < 4)
    720 		die("cio: screen too small");
    721 	if ((scr.sw = newwin(1, scr.x, 0, 0)) == 0
    722 	|| (scr.mw = newwin(scr.y - 2, scr.x, 1, 0)) == 0
    723 	|| (scr.iw = newwin(1, scr.x, scr.y - 1, 0)) == 0)
    724 		die("cio: cannot create windows");
    725 	keypad(scr.iw, 1);
    726 	scrollok(scr.mw, 1);
    727 	if (has_colors() == TRUE) {
    728 		start_color();
    729 		use_default_colors();
    730 		init_pair(1, COLOR_WHITE, COLOR_BLACK);
    731 		wbkgd(scr.sw, COLOR_PAIR(1));
    732 	}
    733 }
    734 
    735 static void
    736 tresize(void)
    737 {
    738 	struct winsize ws;
    739 
    740 	winchg = 0;
    741 	if (ioctl(0, TIOCGWINSZ, &ws) < 0)
    742 		die("cio: Ioctl (TIOCGWINSZ) failed:");
    743 	if (ws.ws_row <= 2) return;
    744 	resizeterm(scr.y = ws.ws_row, scr.x = ws.ws_col);
    745 	wresize(scr.mw, scr.y - 2, scr.x);
    746 	wresize(scr.iw, 1, scr.x);
    747 	wresize(scr.sw, 1, scr.x);
    748 	mvwin(scr.iw, scr.y - 1, 0);
    749 	tredraw();
    750 	tdrawbar();
    751 }
    752 
    753 static void
    754 tredraw(void)
    755 {
    756 	struct Chan *const c = &chl[ch];
    757 	char *q, *p;
    758 	int row_idx = -1;
    759 
    760 	if (c->eol == c->buf) {
    761 		wclear(scr.mw);
    762 		wrefresh(scr.mw);
    763 		return;
    764 	}
    765 	p = c->eol - 1;
    766 	if (c->n) {
    767 		int i = c->n;
    768 		for (; p > c->buf; p--)
    769 			if (*p == '\n' && !i--)
    770 				break;
    771 		if (p == c->buf)
    772 			c->n -= i;
    773 	}
    774 	q = p;
    775 	while (row_idx < (int)scr.y - 2) {
    776 		while (*q != '\n' && q > c->buf)
    777 			q--;
    778 		row_idx++;
    779 		if (q == c->buf)
    780 			break;
    781 		q--;
    782 	}
    783 	if (q != c->buf)
    784 		q += 2;
    785 	wclear(scr.mw);
    786 	wmove(scr.mw, 0, 0);
    787 	while (q < p)
    788 		q = pushl(q, p);
    789 	wrefresh(scr.mw);
    790 }
    791 
    792 static void
    793 tdrawbar(void)
    794 {
    795 	size_t l;
    796 	int fst = ch;
    797 
    798 	for (l = 0; fst > 0 && l < scr.x / 2; fst--)
    799 		l += strlen(chl[fst].name) + 3;
    800 	werase(scr.sw);
    801 	for (l = 0; fst < nch && l < scr.x; fst++) {
    802 		char *p = chl[fst].name;
    803 		if (fst == ch)
    804 			wattron(scr.sw, A_BOLD);
    805 		waddch(scr.sw, '['), l++;
    806 		if (chl[fst].high)
    807 			waddch(scr.sw, '>'), l++;
    808 		else if (chl[fst].new)
    809 			waddch(scr.sw, '+'), l++;
    810 		for (; *p && l < scr.x; p++, l++)
    811 			waddch(scr.sw, *p);
    812 		if (l < scr.x - 1)
    813 			waddstr(scr.sw, "] "), l += 2;
    814 		if (fst == ch)
    815 			wattroff(scr.sw, A_BOLD);
    816 	}
    817 	wrefresh(scr.sw);
    818 }
    819 
    820 static void
    821 tgetch(void)
    822 {
    823 	static char l[BufSz];
    824 	static size_t shft, cu, len;
    825 	size_t dirty = len + 1, i;
    826 	int c;
    827 
    828 	c = wgetch(scr.iw);
    829 	switch (c) {
    830 	case CTRL('n'):
    831 		ch = (ch + 1) % nch;
    832 		chl[ch].high = chl[ch].new = 0;
    833 		tdrawbar();
    834 		tredraw();
    835 		return;
    836 	case CTRL('p'):
    837 		ch = (ch + nch - 1) % nch;
    838 		chl[ch].high = chl[ch].new = 0;
    839 		tdrawbar();
    840 		tredraw();
    841 		return;
    842 	case KEY_PPAGE:
    843 		chl[ch].n += SCROLL;
    844 		tredraw();
    845 		return;
    846 	case KEY_NPAGE:
    847 		chl[ch].n -= SCROLL;
    848 		if (chl[ch].n < 0)
    849 			chl[ch].n = 0;
    850 		tredraw();
    851 		return;
    852 	case CTRL('a'):
    853 		cu = 0;
    854 		break;
    855 	case CTRL('e'):
    856 		cu = len;
    857 		break;
    858 	case CTRL('b'):
    859 	case KEY_LEFT:
    860 		if (cu)
    861 			cu--;
    862 		break;
    863 	case CTRL('f'):
    864 	case KEY_RIGHT:
    865 		if (cu < len)
    866 			cu++;
    867 		break;
    868 	case CTRL('k'):
    869 		dirty = len = cu;
    870 		break;
    871 	case CTRL('u'):
    872 		if (cu == 0) return;
    873 		len -= cu;
    874 		memmove(l, &l[cu], len);
    875 		dirty = cu = 0;
    876 		break;
    877 	case CTRL('d'):
    878 		if (cu >= len) return;
    879 		memmove(&l[cu], &l[cu + 1], len - cu - 1);
    880 		dirty = cu;
    881 		len--;
    882 		break;
    883 	case CTRL('h'):
    884 	case KEY_BACKSPACE:
    885 		if (cu == 0) return;
    886 		memmove(&l[cu - 1], &l[cu], len - cu);
    887 		dirty = --cu;
    888 		len--;
    889 		break;
    890 	case CTRL('w'):
    891 		if (cu == 0) break;
    892 		i = 1;
    893 		while (l[cu - i] == ' ' && cu - i != 0) i++;
    894 		while (l[cu - i] != ' ' && cu - i != 0) i++;
    895 		if (cu - i != 0) i--;
    896 		memmove(&l[cu - i], &l[cu], len - cu);
    897 		cu -= i;
    898 		dirty = cu;
    899 		len -= i;
    900 		break;
    901 	case '\n':
    902 		l[len] = 0;
    903 		uparse(l);
    904 		dirty = cu = len = 0;
    905 		break;
    906 	default:
    907 		if (c > CHAR_MAX || len >= BufSz - 1)
    908 			return; /* Skip other curses codes. */
    909 		memmove(&l[cu + 1], &l[cu], len - cu);
    910 		dirty = cu;
    911 		len++;
    912 		l[cu++] = c;
    913 		break;
    914 	}
    915 	while (cu < shft)
    916 		dirty = 0, shft -= shft >= scr.x / 2 ? scr.x / 2 : shft;
    917 	while (cu >= scr.x + shft)
    918 		dirty = 0, shft += scr.x / 2;
    919 	if (dirty <= shft)
    920 		i = shft;
    921 	else if (dirty > scr.x + shft || dirty > len)
    922 		goto mvcur;
    923 	else
    924 		i = dirty;
    925 	wmove(scr.iw, 0, i - shft);
    926 	wclrtoeol(scr.iw);
    927 	for (; i - shft < scr.x && i < len; i++)
    928 		waddch(scr.iw, l[i]);
    929 mvcur:	wmove(scr.iw, 0, cu - shft);
    930 }
    931 
    932 static void
    933 treset(void)
    934 {
    935 	if (scr.mw) delwin(scr.mw);
    936 	if (scr.sw) delwin(scr.sw);
    937 	if (scr.iw) delwin(scr.iw);
    938 	if (!isendwin()) endwin();
    939 }
    940 
    941 int
    942 main(int argc, char *argv[])
    943 {
    944 #ifdef __OpenBSD__
    945 if (pledge("stdio tty rpath inet dns", NULL) == -1)
    946 	die("pledge");
    947 #endif /* __OpenBSD__ */
    948 	const char *user = getenv("USER");
    949 	const char *ircnick = getenv("IRCNICK");
    950 	snprintf(key, sizeof(key), "%s", getenv("IRCPASS"));
    951 	const char *server = SRV;
    952 	const char *port = PORT;
    953 	const char *err;
    954 	int o, reconn, ping;
    955 
    956 	signal(SIGPIPE, SIG_IGN);
    957 	while ((o = getopt(argc, argv, "hvTVn:c:u:s:p:l:")) >= 0)
    958 		switch (o) {
    959 		case 'h':
    960 		usage:
    961 		  die("usage: %s [-n NICK] [-u USER] [-s SERVER] [-p PORT]\n"
    962 			   "           [-l LOGFILE] [-c certificate] [-hvTV]", argv[0]);
    963 			break;
    964 		case 'v':
    965 			die("cio-" VERSION);
    966 			break;
    967 		case 'l':
    968 			if (!(logfp = fopen(optarg, "a")))
    969 				die("cio: fopen: logfile:");
    970 			break;
    971 		case 'n':
    972 			if (strlcpy(nick, optarg, sizeof(nick)) >= sizeof(nick))
    973 				goto usage;
    974 			break;
    975 		case 'T':
    976 			ssl = 0;
    977 			break;
    978 		case 'V':
    979 			sslverify = 0;
    980 			break;
    981 		case 'u':
    982 			user = optarg;
    983 			break;
    984 		case 's':
    985 			server = optarg;
    986 			break;
    987 		case 'p':
    988 			port = optarg;
    989 			break;
    990 		case 'c':
    991 			if (strlcpy(cert, optarg, sizeof(cert)) >= sizeof(cert))
    992 				goto usage;
    993 			break;
    994 		}
    995 	if (!user)
    996 		user = "anonymous";
    997 	if (!nick[0] && ircnick)
    998 		strlcpy(nick, ircnick, sizeof(nick));
    999 	if (!nick[0])
   1000 		strlcpy(nick, user, sizeof(nick));
   1001 	if (!nick[0])
   1002 		goto usage;
   1003 	atexit(treset);
   1004 	tinit();
   1005 	err = dial(server, port);
   1006 	if (err) die("cio: %s", err);
   1007 	chadd(server, 0);
   1008 	sinit(nick, user);
   1009 	reconn = 0;
   1010 	ping = 0;
   1011 	while (!quit) {
   1012 		struct timeval t = {.tv_sec = 5};
   1013 		fd_set rfs, wfs;
   1014 		if (winchg)
   1015 			tresize();
   1016 		FD_ZERO(&wfs);
   1017 		FD_ZERO(&rfs);
   1018 		FD_SET(0, &rfs);
   1019 		if (!reconn) {
   1020 			FD_SET(srv.fd, &rfs);
   1021 			if (outp != outb)
   1022 				FD_SET(srv.fd, &wfs);
   1023 		}
   1024 		if (select(srv.fd + 1, &rfs, &wfs, 0, &t) < 0) {
   1025 			if (errno == EINTR)
   1026 				continue;
   1027 			die("cio: select failed:");
   1028 		}
   1029 		if (reconn) {
   1030 			hangup();
   1031 			if (reconn > MaxRecons)
   1032 				die("cio: link lost");
   1033 			pushf(0, "-!- Link lost, attempt %d/%d...", reconn++, MaxRecons);
   1034 			if (dial(server, port) != 0)
   1035 				continue;
   1036 			sinit(nick, user);
   1037 			for (struct Chan *c = chl; c < &chl[nch]; ++c)
   1038 				if (c->join)
   1039 					sndf("JOIN %s", c->name);
   1040 			reconn = 0;
   1041 		}
   1042 		if (FD_ISSET(srv.fd, &rfs)) {
   1043 			if (!srd()) {
   1044 				reconn = 1;
   1045 				continue;
   1046 			}
   1047 		}
   1048 		if (FD_ISSET(srv.fd, &wfs)) {
   1049 			size_t len = outp - outb;
   1050 			int wr = ssl ? SSL_write(srv.ssl, outb, len) : write(srv.fd, outb, len);
   1051 			if (wr <= 0) {
   1052 				reconn = 1;
   1053 				continue;
   1054 			}
   1055 			outp -= wr;
   1056 			memmove(outb, outb + wr, outp - outb);
   1057 		}
   1058 		if (FD_ISSET(0, &rfs)) {
   1059 			tgetch();
   1060 			wrefresh(scr.iw);
   1061 		}
   1062 		if ((!FD_ISSET(srv.fd, &wfs))
   1063 		 && (!FD_ISSET(srv.fd, &rfs))
   1064 		 && (outp == outb)
   1065 		 && (++ping >= PingDelay)) {
   1066 			sndf("PING %s", server);
   1067 			ping = 0;
   1068 		}
   1069 	}
   1070 	hangup();
   1071 	while (nch--)
   1072 		free(chl[nch].buf);
   1073 	exit(0);
   1074 }