cio

a simple irc client
git clone git://kloet.net/cio
Download | Log | Files | Refs | README | LICENSE

cio.c (22447B)


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