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 }