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