cio

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

commit 1e98f8a44d2dda525d94f8945d2344d7f744ae54
parent 17fc6b5c307623fb94a2b5179e5732119a08704f
Author: Andrew Kloet <andrew@kloet.net>
Date:   Fri,  3 Apr 2026 01:53:00 -0400

cio release v1.0

This marks the release of cio v1.0, forked from irc.c by Quentin
Carbonneaux.

Notable changes:
- Security: Made TLS default and now verify the server hostname.
- Authentication: Add support for SASL Plain/External.
- scmd():
  * Less rigid argument parser into a generic argc/argv.
  * Use a switch instead of chaining if-elses.
  * Handle a handful of previous unhandled commands
    (yo dawg, i herd you like hands).
- Le Correctness:
  * Avoid unsafe string manipulation functions.
  * Now comes with a standard manual page.
  * Adhere closer RFC 2812: case sensitivity, nick collisions,
    server-sent JOIN/PART, +more...
  * When possible, parse errno into a string and prints.

Diffstat:
D.gitignore | 3---
ALICENSE | 13+++++++++++++
MMakefile | 47++++++++++++++++++++++++++++++++++-------------
MREADME | 33+++++++++++++++++++++------------
ATODO | 8++++++++
Acio.1 | 168+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Rirc.c -> cio.c | 662++++++++++++++++++++++++++++++++++++++++++++++++-------------------------------
Aconfig.mk | 27+++++++++++++++++++++++++++
8 files changed, 670 insertions(+), 291 deletions(-)

diff --git a/.gitignore b/.gitignore @@ -1,2 +0,0 @@ -irc -*.sw[po] -\ No newline at end of file diff --git a/LICENSE b/LICENSE @@ -0,0 +1,13 @@ +Copyright (c) 2026 Andrew Kloet <andrew@kloet.net> + +Permission to use, copy, modify, and distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. diff --git a/Makefile b/Makefile @@ -1,21 +1,42 @@ -BIN = irc +# cio - minimalist IRC client +# See LICENSE file for copyright and license details. -CFLAGS = -std=c99 -Os -D_POSIX_C_SOURCE=201112 -D_GNU_SOURCE -D_XOPEN_CURSES -D_XOPEN_SOURCE_EXTENDED=1 -D_DEFAULT_SOURCE -D_BSD_SOURCE -LDLIBS = -lncursesw -lssl -lcrypto +include config.mk -PREFIX=/usr/local +SRC = cio.c +OBJ = ${SRC:.c=.o} -all: ${BIN} +all: cio -install: ${BIN} - mkdir -p ${PREFIX}/bin - cp -f ${BIN} ${PREFIX}/bin - chmod 755 ${PREFIX}/bin/${BIN} +.c.o: + ${CC} -c ${CFLAGS} $< -uninstall: - rm -f ${PREFIX}/bin/${BIN} +{OBJ}: config.mk + +cio: ${OBJ} + ${CC} -o $@ ${OBJ} ${LDFLAGS} clean: - rm -f ${BIN} *.o + rm -f cio ${OBJ} cio-${VERSION}.tar.gz + +dist: clean + mkdir -p cio-${VERSION} + cp -R LICENSE TODO README Makefile \ + config.mk cio.1 ${SRC} cio-${VERSION} + tar -cf cio-${VERSION}.tar cio-${VERSION} + gzip cio-${VERSION}.tar + rm -rf cio-${VERSION} + +install: all + mkdir -p ${DESTDIR}${PREFIX}/bin + cp -f cio ${DESTDIR}${PREFIX}/bin + chmod 755 ${DESTDIR}${PREFIX}/bin/cio + mkdir -p ${DESTDIR}${MANPREFIX}/man1 + cp -f cio.1 ${DESTDIR}${MANPREFIX}/man1/cio.1 + chmod 644 ${DESTDIR}${MANPREFIX}/man1/cio.1 + +uninstall: + rm -f ${DESTDIR}${PREFIX}/bin/cio\ + ${DESTDIR}${MANPREFIX}/man1/cio.1 -.PHONY: all clean +.PHONY: all clean dist install uninstall diff --git a/README b/README @@ -1,19 +1,28 @@ -IRC client https://c9x.me/irc/ -=========================================================== +cio - chat input/output https://git.kloet.net/cio/ +============================================================ -This is a simple irc client, it requires the ncurses -library to compile. +cio is a simple irc client, it requires the ncurses library +and openssl to compile. -Usage: irc [OPTIONS] +cio is meant to be easily hackable; as a result, many IRC +commands of RFC 2812 do not have an explicit implementation. +*Most* users should not find this to be a problem as many of +IRC's commands are purely informative. All unimplemented +commands are printed to the server console with their +respective code and message. - OPTION DEFAULT +Usage: cio [OPTIONS] - -n NICK Sets the nick $IRCNICK - -u USER Sets the username $USER - -s SERVER Server to connect to irc.oftc.net - -p PORT Port to connect to 6667 - -l FILE File to log recieved data - -t Use a secured connection + OPTION DEFAULT + + -n NICK Sets the nick $IRCNICK + -u USER Sets the username $USER + -s SERVER Server to connect to irc.oftc.net + -p PORT Port to connect to 6697 + -l FILE File to log recieved data + -c FILE File to load client cert from + -T Disable TLS + -V Disable TLS hostname check -h Display help A password sent to the server can be specified by setting diff --git a/TODO b/TODO @@ -0,0 +1,8 @@ +- More RFC 2812 compliance(?). + 1. sndf() output should be limited to 512 chars (2.3). + 2. strcase{cmp,str} are technically inadequate per (2.2) but who's counting? +- Might be nice to be able to log the raw IRC packets. +- It would be really nice to be able to OpenBSD pledge down to tty, stdio after + server connect but reconnect needs inet, dns, rpath (for system trust store). + 1. rpath can *kind of* be solved by storing all certificate details in memory + but loading the entire CA store into memory is a little silly for cio diff --git a/cio.1 b/cio.1 @@ -0,0 +1,168 @@ +.Dd April 2, 2026 +.Dt CIO 1 +.Os +.Sh NAME +.Nm cio +.Nd a minimal IRC client +. +.Sh SYNOPSIS +.Nm cio +.Op Fl hvTV +.Op Fl l Ar logfile +.Op Fl n Ar nick +.Op Fl p Ar port +.Op Fl s Ar server +.Op Fl u Ar user +.Op Fl c Ar certfile +. +.Sh DESCRIPTION +.Nm +is a multiplexing curses interface for IRC featuring SASL authentication, +infinite scrollback, and automatic reconnection. +The options are as follows: +.Bl -tag -width Ds +.It Fl T +Disable TLS and connect using a plaintext socket. +.It Fl V +Disable TLS hostname validation. +.It Fl l Ar logfile +Append all IRC activity, including timestamps and channel names, to +.Ar logfile . +.It Fl n Ar nick +Set the IRC nickname. +If not provided, +.Nm +will check the +.Ev IRCNICK +environment variable, then the +.Ev USER +variable. +.It Fl p Ar port +Connect to the server on +.Ar port . +Defaults to 6697. +.It Fl s Ar server +Connect to the IRC server at +.Ar server . +Defaults to irc.oftc.net. +.It Fl u Ar user +Set the IRC username (ident). +Defaults to the value of the +.Ev USER +environment variable, or "anonymous". +.It Fl c Ar certfile +Use +.Ar certfile +as a TLS client certificate. +This is used for both establishing the encrypted connection and for SASL +EXTERNAL (CertFP) authentication. +.El +. +.Sh COMMANDS +.Ss Chat Commands +Chat commands are submitted with a newline. +They are not prepended with a '/'. +Any text not matching a command is sent as a message to the currently +focused channel. +.Bl -tag -width Ds +.It Ic j Ar #channel +Join a given channel +.It Ic l Op Ar #channel +Leave the given channel(s), defaults to current channel. +.It Ic m Ar recipient message +Sends a private message. +.It Ic r Ar message +Sends a raw IRC message to the server. +.It Ic q +Quits +.Nm . +.El +.Ss Interface Commands +.Bl -tag -width Ds +.It Ic ^n +Increases focused window index by 1 (wrapping). +.It Ic ^p +Decreases focused window index by 1 (wrapping). +.It Ic PAGEUP +Scrolls chat up by 15 lines. +.It Ic PAGEDOWN +Scrolls chat down by 15 lines. +.El +.Ss Line Editing +The following key bindings are available when entering text: +.Bl -tag -width Ds +.It Ic ^A +Move cursor to the beginning of the line. +.It Ic ^E +Move cursor to the end of the line. +.It Ic ^B No \&| Ic LEFTARROW +Move cursor one character to the left. +.It Ic ^F No \&| Ic RIGHTARROW +Move cursor one character to the right. +.It Ic ^K +Delete from the cursor to the end of the line. +.It Ic ^U +Delete from the beginning of the line to the cursor. +.It Ic ^D +Delete character under the cursor. +.It Ic ^H +Delete character to the left of the cursor. +.It Ic ^W +Delete word to the left of the cursor. +.It Ic BACKSPACE +Delete character to the left of the cursor. +.It Ic ENTER +Submit current line. +.El +. +.Sh ENVIRONMENT +.Bl -tag -width Ds +.It Ev USER +The username to use. +Overridden by +.Fl u . +.It Ev IRCNICK +The nickname to use. +Overridden by +.Fl n . +.It Ev IRCPASS +The password to used for SASL PLAIN authentication. +This is used if no certificate is provided via +.Fl c . +.El +. +.Sh EXIT STATUS +.Nm +exits 0 if requested by the user and >0 if any other error occurs. +.Sh EXAMPLES +.Ss Using CertFP +.Bl -enum +.It +Generate a new TLS client certificate: +.Bd -literal +$ openssl req -x509 -newkey rsa:4096 -nodes -days 365 \\ + -keyout user.pem -out user.pem -subj "/CN=CertFP" +$ chmod 600 user.pem +.Ed +.It +Connect to the server using the certificate: +.Bd -literal +$ cio -c user.pem +.Ed +.It +Identify with the server normally then add the certificate fingerprint to +your account: +.Bd -literal +m nickserv cert add +.Ed +.El +. +.Sh HISTORY +.Nm +is a fork of +.Lk https://c9x.me/irc/ irc.c +written by +.An Quentin Carbonneaux Aq Mt qcarbonneaux@gmail.com . +. +.Sh AUTHORS +.An Andrew Kloet Aq Mt andrew@kloet.net diff --git a/irc.c b/cio.c @@ -1,4 +1,21 @@ +/* See LICENSE file for copyright and license details. + * + * cio is driven by a synchronous select(2) loop. It multiplexes stdin, the + * network socket, and SIGWINCH for terminal resizing. Each channel's scrollback + * is stored in an exponentially-reallocating heap buffer. + * + * Channels are organized in a fixed-size array. The '0' channel is reserved for + * server messages (status), while others are created dynamically upon JOIN or + * PRIVMSG. IRC protocol parsing is handled via a string tokenizer, dispatching + * commands through a lookup table in scmd(). + * + * UI rendering is handled by ncurses. To support UTF-8, the client manually + * decodes runes before passing them to the wide-character add_wch function. + * + * To understand the lifecycle start reading main(). + */ #include <assert.h> +#include <ctype.h> #include <limits.h> #include <signal.h> #include <stdio.h> @@ -19,11 +36,12 @@ #include <netinet/tcp.h> #include <netdb.h> #include <locale.h> -#include <wchar.h> #include <openssl/ssl.h> +#include <openssl/x509v3.h> #undef CTRL -#define CTRL(x) (x & 037) +#define CTRL(x) (x & 037) +#define GET_ARG(i) ((argc > (i)) ? argv[i] : "") #define SCROLL 15 #define INDENT 23 @@ -31,25 +49,46 @@ #define PFMT " %-12s < %s" #define PFMTHIGH "> %-12s < %s" #define SRV "irc.oftc.net" -#define PORT "6667" +#define PORT "6697" + +typedef enum { + CMD_UNKNOWN, NOTICE, PRIVMSG, PING, PONG, PART, JOIN, QUIT, NICK, CAP, + AUTHENTICATE, RPL_TOPIC, RPL_TOPICWHOTIME, RPL_NAMREPLY, RPL_ENDOFNAMES, + ERR_NICKNAMEINUSE, ERR_CHANNELISFULL, ERR_INVITEONLYCHAN, ERR_BADCHANNELKEY, + ERR_NOCHANMODES, SASL_OK, SASL_ERR +} IrcCmd; + +struct { + const char *name; + IrcCmd id; +} cmd_map[] = { + { "NOTICE", NOTICE }, { "PRIVMSG", PRIVMSG }, { "PING", PING }, + { "PONG", PONG }, { "PART", PART }, { "JOIN", JOIN }, { "QUIT", QUIT }, + { "NICK", NICK}, { "CAP", CAP }, { "AUTHENTICATE", AUTHENTICATE }, + { "332", RPL_TOPIC }, { "333", RPL_TOPICWHOTIME }, { "353", RPL_NAMREPLY }, + { "366", RPL_ENDOFNAMES }, { "477", ERR_NOCHANMODES }, + { "433", ERR_NICKNAMEINUSE }, { "471", ERR_CHANNELISFULL }, + { "473", ERR_INVITEONLYCHAN }, { "475", ERR_BADCHANNELKEY }, + { "903", SASL_OK }, { "904", SASL_ERR } +}; enum { - ChanLen = 64, - LineLen = 512, - MaxChans = 16, - BufSz = 2048, - LogSz = 4096, - MaxRecons = 10, /* -1 for infinitely many */ - PingDelay = 6, - UtfSz = 4, - RuneInvalid = 0xFFFD, + ChanLen = 64, + LineLen = 512, + MaxChans = 16, + MaxParams = 15, + BufSz = 2048, + LogSz = 4096, + MaxRecons = 10, /* -1 for infinitely many */ + PingDelay = 6, + UtfSz = 4, + RuneInvalid = 0xFFFD, }; typedef wchar_t Rune; static struct { - int x; - int y; + size_t x, y; WINDOW *sw, *mw, *iw; } scr; @@ -63,33 +102,43 @@ static struct Chan { char join; /* Channel was 'j'-oined. */ } chl[MaxChans]; -static int ssl; +static int ssl = 1; +static int sslverify = 1; static struct { int fd; SSL *ssl; SSL_CTX *ctx; } srv; static char nick[64]; +static char cert[PATH_MAX]; +static char key[512]; static int quit, winchg; static int nch, ch; /* Current number of channels, and current channel. */ static char outb[BufSz], *outp = outb; /* Output buffer. */ static FILE *logfp; -static unsigned char utfbyte[UtfSz + 1] = {0x80, 0, 0xC0, 0xE0, 0xF0}; -static unsigned char utfmask[UtfSz + 1] = {0xC0, 0x80, 0xE0, 0xF0, 0xF8}; -static Rune utfmin[UtfSz + 1] = { 0, 0, 0x80, 0x800, 0x10000}; -static Rune utfmax[UtfSz + 1] = {0x10FFFF, 0x7F, 0x7FF, 0xFFFF, 0x10FFFF}; +static const unsigned char utfbyte[UtfSz + 1] = {0x80, 0, 0xC0, 0xE0, 0xF0}; +static const unsigned char utfmask[UtfSz + 1] = {0xC0, 0x80, 0xE0, 0xF0, 0xF8}; +static const Rune utfmin[UtfSz + 1] = {0, 0, 0x80, 0x800, 0x10000}; +static const Rune utfmax[UtfSz + 1] = {0x10FFFF, 0x7F, 0x7FF, 0xFFFF, 0x10FFFF}; -static void scmd(char *, char *, char *, char *); +static void scmd(char *, char *, int, char **); static void tdrawbar(void); static void tredraw(void); static void treset(void); static void -panic(const char *m) +die(const char *fmt, ...) { + va_list ap; + treset(); - fprintf(stderr, "Panic: %s\n", m); + va_start(ap, fmt); + vfprintf(stderr, fmt, ap); + va_end(ap); + if (fmt[0] && fmt[strlen(fmt)-1] == ':') + fprintf(stderr, " %s", strerror(errno)); + fputc('\n', stderr); exit(1); } @@ -113,7 +162,7 @@ utf8decodebyte(unsigned char c, size_t *i) } static size_t -utf8decode(char *c, Rune *u, size_t clen) +utf8decode(const char *c, Rune *u, size_t clen) { size_t i, j, len, type; Rune udecoded; @@ -136,26 +185,30 @@ utf8decode(char *c, Rune *u, size_t clen) return len; } -static char -utf8encodebyte(Rune u, size_t i) -{ - return utfbyte[i] | (u & ~utfmask[i]); +static int +empty(const char *str) { + return (str == NULL || str[0] == '\0'); } -static size_t -utf8encode(Rune u, char *c) +static char * +b64_enc(const unsigned char *in, size_t len) { - size_t len, i; + static char out[BufSz]; + const char *tab = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + char *p = out; + size_t i; - len = utf8validate(&u, 0); - if (len > UtfSz) - return 0; - for (i = len - 1; i != 0; --i) { - c[i] = utf8encodebyte(u, 0); - u >>= 6; + for (i = 0; i < len; i += 3) { + unsigned int v = + in[i] << 16 | (i+1 < len ? in[i+1] << 8 : 0) | (i+2 < len ? in[i+2] : 0); + *p++ = tab[(v >> 18) & 0x3F]; + *p++ = tab[(v >> 12) & 0x3F]; + *p++ = (i+1 < len) ? tab[(v >> 6) & 0x3F] : '='; + *p++ = (i+2 < len) ? tab[v & 0x3F] : '='; } - c[0] = utf8encodebyte(u, len); - return len; + *p = '\0'; + return out; } static void @@ -178,8 +231,8 @@ static int srd(void) { static char l[BufSz], *p = l; - char *s, *usr, *cmd, *par, *data; - int rd; + char *s, *usr, *cmd, *argv[MaxParams]; + int rd, argc; if (p - l >= BufSz) p = l; /* Input buffer overflow, there should something better to do. */ @@ -187,30 +240,36 @@ srd(void) rd = SSL_read(srv.ssl, p, BufSz - (p - l)); else rd = read(srv.fd, p, BufSz - (p - l)); - if (rd <= 0) - return 0; + if (rd <= 0) return 0; p += rd; for (;;) { /* Cycle on all received lines. */ if (!(s = memchr(l, '\n', p - l))) return 1; - if (s > l && s[-1] == '\r') - s[-1] = 0; + if (s > l && s[-1] == '\r') s[-1] = 0; *s++ = 0; - if (*l == ':') { - if (!(cmd = strchr(l, ' '))) - goto lskip; - *cmd++ = 0; - usr = l + 1; - } else { - usr = 0; - cmd = l; + char *tok = l; + usr = NULL; + if (*tok == ':') { + usr = tok + 1; + if (!(tok = strchr(tok, ' '))) goto lskip; + *tok++ = 0; } - if (!(par = strchr(cmd, ' '))) - goto lskip; - *par++ = 0; - if ((data = strchr(par, ':'))) - *data++ = 0; - scmd(usr, cmd, par, data); + while (*tok == ' ') tok++; + cmd = tok; + if ((tok = strchr(tok, ' '))) + *tok++ = 0; + argc = 0; + while (tok && *tok && argc < MaxParams) { + while (*tok == ' ') tok++; + if (*tok == ':') { + argv[argc++] = tok + 1; + break; + } + argv[argc++] = tok; + if ((tok = strchr(tok, ' '))) + *tok++ = 0; + } + if (cmd) scmd(usr, cmd, argc, argv); lskip: memmove(l, s, p - s); p -= s - l; @@ -218,70 +277,82 @@ srd(void) } static void -sinit(const char *key, const char *nick, const char *user) +sinit(const char *nick, const char *user) { - if (key) - sndf("PASS %s", key); + sndf("CAP LS 302"); sndf("NICK %s", nick); sndf("USER %s 8 * :%s", user, user); - sndf("MODE %s +i", nick); } static char * dial(const char *host, const char *service) { struct addrinfo hints, *res = NULL, *rp; - int fd = -1, e; + int fd = -1; memset(&hints, 0, sizeof(hints)); hints.ai_family = AF_UNSPEC; /* allow IPv4 or IPv6 */ hints.ai_flags = AI_NUMERICSERV; /* avoid name lookup for port */ hints.ai_socktype = SOCK_STREAM; - if ((e = getaddrinfo(host, service, &hints, &res))) - return "Getaddrinfo failed."; + if (getaddrinfo(host, service, &hints, &res)) + return "getaddrinfo failed"; for (rp = res; rp; rp = rp->ai_next) { - if ((fd = socket(res->ai_family, res->ai_socktype, res->ai_protocol)) == -1) - continue; - if (connect(fd, res->ai_addr, res->ai_addrlen) == -1) { - close(fd); + fd = socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol); + if (fd == -1) continue; - } - break; + if (connect(fd, rp->ai_addr, rp->ai_addrlen) == 0) + break; + close(fd); + fd = -1; } - if (fd == -1) - return "Cannot connect to host."; - srv.fd = fd; + freeaddrinfo(res); + if ((srv.fd = fd) == -1) + return "cannot connect to host"; if (ssl) { SSL_load_error_strings(); SSL_library_init(); - srv.ctx = SSL_CTX_new(SSLv23_client_method()); - if (!srv.ctx) - return "Could not initialize ssl context."; + srv.ctx = SSL_CTX_new(TLS_client_method()); + if (!srv.ctx) return "could not initialize ssl context"; + if (sslverify) { + SSL_CTX_set_verify(srv.ctx, SSL_VERIFY_PEER, NULL); + if (SSL_CTX_set_default_verify_paths(srv.ctx) != 1) + return "could not load default system trust store"; + } srv.ssl = SSL_new(srv.ctx); - if (SSL_set_fd(srv.ssl, srv.fd) == 0 - || SSL_connect(srv.ssl) != 1) - return "Could not connect with ssl."; + if (!srv.ssl) return "could not create SSL object"; + if (sslverify) { + SSL_set_hostflags(srv.ssl, X509_CHECK_FLAG_NO_PARTIAL_WILDCARDS); + if (!SSL_set1_host(srv.ssl, host)) + return "could not set expected hostname"; + SSL_set_tlsext_host_name(srv.ssl, host); + } else { + SSL_CTX_set_verify(srv.ctx, SSL_VERIFY_NONE, NULL); + } + if (!empty(cert)) { + if (SSL_use_certificate_chain_file(srv.ssl, cert) <= 0) + return "failed to load certificate chain"; + if (SSL_use_PrivateKey_file(srv.ssl, cert, SSL_FILETYPE_PEM) <= 0) + return "failed to load private key"; + } + SSL_set_fd(srv.ssl, srv.fd); + if (SSL_connect(srv.ssl) <= 0) { + long verify_err = SSL_get_verify_result(srv.ssl); + if (verify_err != X509_V_OK) + return (char *)X509_verify_cert_error_string(verify_err); + return "SSL handshake failed"; + } } - freeaddrinfo(res); return 0; } static void hangup(void) { - if (srv.ssl) { - SSL_shutdown(srv.ssl); - SSL_free(srv.ssl); - srv.ssl = 0; - } - if (srv.fd) { - close(srv.fd); - srv.fd = 0; - } - if (srv.ctx) { - SSL_CTX_free(srv.ctx); - srv.ctx = 0; - } + if (srv.ssl) SSL_shutdown(srv.ssl); + SSL_free(srv.ssl); + if (srv.fd > 0) close(srv.fd); + SSL_CTX_free(srv.ctx); + memset(&srv, 0, sizeof(srv)); } static inline int @@ -291,7 +362,7 @@ chfind(const char *name) assert(name); for (i = nch - 1; i > 0; i--) - if (!strcmp(chl[i].name, name)) + if (!strcasecmp(chl[i].name, name)) break; return i; } @@ -305,11 +376,11 @@ chadd(const char *name, int joined) return -1; if ((n = chfind(name)) > 0) return n; - strcpy(chl[nch].name, name); + strlcpy(chl[nch].name, name, sizeof(chl[nch].name)); chl[nch].sz = LogSz; chl[nch].buf = malloc(LogSz); if (!chl[nch].buf) - panic("Out of memory."); + die("cio: malloc:"); chl[nch].eol = chl[nch].buf; chl[nch].n = 0; chl[nch].join = joined; @@ -317,20 +388,21 @@ chadd(const char *name, int joined) ch = nch; nch++; tdrawbar(); - return nch; + return nch - 1; } static int -chdel(char *name) +chdel(const char *name) { - int n; + int n = chfind(name); + if (n <= 0) return 0; + free(chl[n].buf); - if (!(n = chfind(name))) - return 0; + if (n < nch - 1) + memmove(&chl[n], &chl[n + 1], (nch - n - 1) * sizeof(struct Chan)); nch--; - free(chl[n].buf); - memmove(&chl[n], &chl[n + 1], (nch - n) * sizeof(struct Chan)); - ch = nch - 1; + if (ch > n || ch >= nch) + ch = (ch > 0) ? ch - 1 : 0; tdrawbar(); return 1; } @@ -338,7 +410,7 @@ chdel(char *name) static char * pushl(char *p, char *e) { - int x, cl; + size_t x; char *w; Rune u[2]; cchar_t cc; @@ -357,7 +429,7 @@ pushl(char *p, char *e) w++; x += p - w; } - if (p >= e || *p == ' ' || p - w + INDENT >= scr.x - 1) { + if (p >= e || *p == ' ' || p - w + INDENT >= (ptrdiff_t)scr.x - 1) { while (w < p) { w += utf8decode(w, u, UtfSz); if (wcwidth(*u) > 0 || *u == '\n') { @@ -365,12 +437,11 @@ pushl(char *p, char *e) wadd_wch(scr.mw, &cc); } } - if (p >= e) - return e; + if (p >= e) return e; } p += utf8decode(p, u, UtfSz); - if ((cl = wcwidth(*u)) >= 0) - x += cl; + int cl = wcwidth(*u); + if (cl >= 0) x += cl; } } @@ -382,27 +453,26 @@ pushf(int cn, const char *fmt, ...) va_list vl; time_t t; char *s; - struct tm *tm, *gmtm; + const struct tm *tm, *gmtm; if (blen + LineLen >= c->sz) { c->sz *= 2; c->buf = realloc(c->buf, c->sz); if (!c->buf) - panic("Out of memory."); + die("cio: realloc:"); c->eol = c->buf + blen; } t = time(0); if (!(tm = localtime(&t))) - panic("Localtime failed."); + die("cio: localtime failed"); n = strftime(c->eol, LineLen, DATEFMT, tm); if (!(gmtm = gmtime(&t))) - panic("Gmtime failed."); + die("cio: gmtime failed"); c->eol[n++] = ' '; va_start(vl, fmt); s = c->eol + n; n += vsnprintf(s, LineLen - n - 1, fmt, vl); va_end(vl); - if (logfp) { fprintf(logfp, "%-12.12s\t%04d-%02d-%02dT%02d:%02d:%02dZ\t%s\n", c->name, @@ -410,12 +480,8 @@ pushf(int cn, const char *fmt, ...) gmtm->tm_hour, gmtm->tm_min, gmtm->tm_sec, s); fflush(logfp); } - - strcat(c->eol, "\n"); - if (n >= LineLen - 1) - c->eol += LineLen - 1; - else - c->eol += n + 1; + strlcat(c->buf, "\n", c->sz); + c->eol = c->buf + strlen(c->buf); if (cn == ch && c->n == 0) { char *p = c->eol - n - 1; @@ -426,75 +492,156 @@ pushf(int cn, const char *fmt, ...) } } +static IrcCmd get_cmd_type(const char *cmd) { + for (size_t i = 0; i < sizeof(cmd_map)/sizeof(cmd_map[0]); i++) + if (!strcmp(cmd, cmd_map[i].name)) return cmd_map[i].id; + return CMD_UNKNOWN; +} + static void -scmd(char *usr, char *cmd, char *par, char *data) +scmd(char *usr, char *cmd, int argc, char **argv) { - int s, c; - char *pm = strtok(par, " "), *chan; + IrcCmd type = get_cmd_type(cmd); + int c = 0; - if (!usr) - usr = "?"; + if (!usr) usr = "?"; else { char *bang = strchr(usr, '!'); - if (bang) - *bang = 0; + if (bang) *bang = '\0'; } - if (!strcmp(cmd, "PRIVMSG")) { - if (!pm || !data) - return; - if (strchr("&#!+.~", pm[0])) - chan = pm; - else - chan = usr; - if (!(c = chfind(chan))) { - if (chadd(chan, 0) < 0) - return; - tredraw(); + switch (type) { + case NOTICE: + if (argc < 2) break; + c = strchr("&#!+.~", argv[0][0]) ? chfind(argv[0]) : 0; + if (c < 0) c = 0; + pushf(c, PFMT, usr, argv[1]); + if (ch != c) { + chl[c].new = 1; + tdrawbar(); } - c = chfind(chan); - if (strcasestr(data, nick)) { - pushf(c, PFMTHIGH, usr, data); - chl[c].high |= ch != c; + break; + case PRIVMSG: + if (argc < 2) break; + char *chan = strchr("&#!+.~", argv[0][0]) ? argv[0] : (usr ? usr : "server"); + if (!(c = chfind(chan)) && (c = chadd(chan, 0)) < 0) + break; + if (strcasestr(argv[1], nick)) { + pushf(c, PFMTHIGH, usr, argv[1]); + chl[c].high |= (ch != c); } else - pushf(c, PFMT, usr, data); + pushf(c, PFMT, usr, argv[1]); if (ch != c) { chl[c].new = 1; tdrawbar(); } - } else if (!strcmp(cmd, "PING")) { - sndf("PONG :%s", data ? data : "(null)"); - } else if (!strcmp(cmd, "PONG")) { - /* nothing */ - } else if (!strcmp(cmd, "PART")) { - if (!pm) - return; - pushf(chfind(pm), "-!- %s has left %s", usr, pm); - } else if (!strcmp(cmd, "JOIN")) { - if (!pm) - return; - pushf(chfind(pm), "-!- %s has joined %s", usr, pm); - } else if (!strcmp(cmd, "470")) { /* Channel forwarding. */ - char *ch = strtok(0, " "), *fch = strtok(0, " "); - - if (!ch || !fch || !(s = chfind(ch))) - return; - chl[s].name[0] = 0; - strncat(chl[s].name, fch, ChanLen - 1); - tdrawbar(); - } else if (!strcmp(cmd, "471") || !strcmp(cmd, "473") - || !strcmp(cmd, "474") || !strcmp(cmd, "475")) { /* Join error. */ - if ((pm = strtok(0, " "))) { - chdel(pm); - pushf(0, "-!- Cannot join channel %s (%s)", pm, cmd); + break; + case PING: + if (argc >= 2) + sndf("PONG %s :%s", argv[0], argv[1]); + else if (argc == 1) + sndf("PONG :%s", argv[0]); + else + sndf("PONG :%s", chl[0]); + break; + case PONG: + break; + case JOIN: + if (argc < 1 || !usr) break; + c = chfind(argv[0]); + if (!strcasecmp(usr, nick)) { + if (c <= 0) + c = chadd(argv[0], 1); + if (c > 0) { + pushf(c, "-!- You have joined %s", argv[0]); + tdrawbar(); + tredraw(); + } + } else if (c > 0) { + pushf(c, "-!- %s has joined %s", usr, argv[0]); + } + break; + case PART: + if (argc < 1 || !usr) break; + c = chfind(argv[0]); + if (c <= 0) break; + if (!strcasecmp(usr, nick)) { + pushf(0, "-!- You have left %s", argv[0]); + chdel(argv[0]); tredraw(); + } else { + pushf(c, "-!- %s has left %s %s", usr, argv[0], GET_ARG(1)); } - } else if (!strcmp(cmd, "QUIT")) { /* Commands we don't care about. */ - return; - } else if (!strcmp(cmd, "NOTICE") || !strcmp(cmd, "375") - || !strcmp(cmd, "372") || !strcmp(cmd, "376")) { - pushf(0, "%s", data ? data : ""); - } else - pushf(0, "%s - %s %s", cmd, par, data ? data : "(null)"); + break; + case QUIT: + break; + case NICK: + if (argc < 1) break; + if (!strcasecmp(usr, nick)) { + strlcpy(nick, argv[0], sizeof(nick)); + pushf(0, "-!- You are now known as %s", nick); + } else + pushf(0, "-!- %s is now known as %s", usr, argv[0]); + break; + case RPL_TOPIC: + if (argc >= 3) pushf(chfind(argv[1]), "-!- Topic: %s", argv[2]); + break; + case RPL_NAMREPLY: + if (argc >= 4) pushf(chfind(argv[2]), "-!- Names: %s", argv[3]); + break; + case RPL_TOPICWHOTIME: + case RPL_ENDOFNAMES: + break; + case ERR_NOCHANMODES: + case ERR_CHANNELISFULL: + case ERR_INVITEONLYCHAN: + case ERR_BADCHANNELKEY: + if (argc < 2) break; + chdel(argv[1]); + pushf(0, "-!- Cannot join %s (%s)", argv[1], cmd); + tredraw(); + break; + case ERR_NICKNAMEINUSE: + strlcat(nick, "_", sizeof(nick)); + sndf("NICK %s", nick); + break; + case CAP: + if (argc < 2) break; + if (!strcmp(argv[1], "LS")) + sndf((!empty(key) || !empty(cert)) && argc > 2 && strstr(argv[2], "sasl") + ? "CAP REQ :sasl" + : "CAP END"); + else if (!strcmp(argv[1], "ACK") && argc > 2 && strstr(argv[2], "sasl")) + sndf(empty(cert) ? "AUTHENTICATE PLAIN" : "AUTHENTICATE EXTERNAL"); + else if (!strcmp(argv[1], "NAK") || !strcmp(argv[1], "ACK")) + sndf("CAP END"); + break; + case AUTHENTICATE: + if (argc < 1 || strcmp(argv[0], "+")) break; + if (!empty(cert)) { + sndf("AUTHENTICATE +"); + } else if (!empty(usr) && !empty(key)) { + unsigned char raw[512]; + size_t u_len = strlen(usr); + size_t k_len = strlen(key); + raw[0] = '\0'; + memcpy(raw + 1, usr, u_len); + raw[1 + u_len] = '\0'; + memcpy(raw + 2 + u_len, key, k_len); + sndf("AUTHENTICATE %s", b64_enc(raw, u_len + k_len + 2)); + } + break; + case SASL_OK: + case SASL_ERR: + sndf("CAP END"); + pushf(0, "-!- SASL auth %s", (type == SASL_OK ? "successful" : "failed")); + break; + default: + if (isdigit(cmd[0])) + pushf(0, "%s: %s", cmd, GET_ARG(argc - 1)); + else + pushf(0, "%s: %s %s", cmd, GET_ARG(0), GET_ARG(1)); + break; + } } static void @@ -504,11 +651,9 @@ uparse(char *m) if (!p[0] || (p[1] != ' ' && p[1] != 0)) { pmsg: - if (ch == 0) - return; + if (ch == 0) return; m += strspn(m, " "); - if (!*m) - return; + if (!*m) return; pushf(ch, PFMT, nick, m); sndf("PRIVMSG %s :%s", chl[ch].name, m); return; @@ -516,28 +661,25 @@ uparse(char *m) switch (*p) { case 'j': /* Join channels. */ p += 1 + (p[1] == ' '); - p = strtok(p, " "); - while (p) { - if (chadd(p, 1) < 0) + for (char *token = strtok(p, " "); token; token = strtok(NULL, " ")) { + if (chadd(token, 1) < 0) break; - sndf("JOIN %s", p); - p = strtok(0, " "); + if (strchr("&#!+", token[0])) + sndf("JOIN %s", token); } tredraw(); return; case 'l': /* Leave channels. */ p += 1 + (p[1] == ' '); if (!*p) { - if (ch == 0) - return; /* Cannot leave server window. */ - strcat(p, chl[ch].name); - } - p = strtok(p, " "); - while (p) { - if (chdel(p)) - sndf("PART %s", p); - p = strtok(0, " "); + if (ch == 0) return; /* Cannot leave server window */ + static char buf[ChanLen]; + strlcpy(buf, chl[ch].name, sizeof(buf)); + p = buf; } + for (char *token = strtok(p, " "); token; token = strtok(NULL, " ")) + if (chdel(token) && strchr("&#!+", token[0])) + sndf("PART %s", token); tredraw(); return; case 'm': /* Private message. */ @@ -562,8 +704,7 @@ uparse(char *m) static void sigwinch(int sig) { - if (sig) - winchg = 1; + if (sig) winchg = 1; } static void @@ -576,17 +717,17 @@ tinit(void) noecho(); getmaxyx(stdscr, scr.y, scr.x); if (scr.y < 4) - panic("Screen too small."); + die("cio: screen too small"); if ((scr.sw = newwin(1, scr.x, 0, 0)) == 0 || (scr.mw = newwin(scr.y - 2, scr.x, 1, 0)) == 0 || (scr.iw = newwin(1, scr.x, scr.y - 1, 0)) == 0) - panic("Cannot create windows."); + die("cio: cannot create windows"); keypad(scr.iw, 1); scrollok(scr.mw, 1); if (has_colors() == TRUE) { start_color(); use_default_colors(); - init_pair(1, COLOR_WHITE, COLOR_BLUE); + init_pair(1, COLOR_WHITE, COLOR_BLACK); wbkgd(scr.sw, COLOR_PAIR(1)); } } @@ -598,9 +739,8 @@ tresize(void) winchg = 0; if (ioctl(0, TIOCGWINSZ, &ws) < 0) - panic("Ioctl (TIOCGWINSZ) failed."); - if (ws.ws_row <= 2) - return; + die("cio: Ioctl (TIOCGWINSZ) failed:"); + if (ws.ws_row <= 2) return; resizeterm(scr.y = ws.ws_row, scr.x = ws.ws_col); wresize(scr.mw, scr.y - 2, scr.x); wresize(scr.iw, 1, scr.x); @@ -615,7 +755,7 @@ tredraw(void) { struct Chan *const c = &chl[ch]; char *q, *p; - int nl = -1; + int row_idx = -1; if (c->eol == c->buf) { wclear(scr.mw); @@ -632,10 +772,10 @@ tredraw(void) c->n -= i; } q = p; - while (nl < scr.y - 2) { + while (row_idx < (int)scr.y - 2) { while (*q != '\n' && q > c->buf) q--; - nl++; + row_idx++; if (q == c->buf) break; q--; @@ -657,7 +797,6 @@ tdrawbar(void) for (l = 0; fst > 0 && l < scr.x / 2; fst--) l += strlen(chl[fst].name) + 3; - werase(scr.sw); for (l = 0; fst < nch && l < scr.x; fst++) { char *p = chl[fst].name; @@ -730,30 +869,26 @@ tgetch(void) dirty = len = cu; break; case CTRL('u'): - if (cu == 0) - return; + if (cu == 0) return; len -= cu; memmove(l, &l[cu], len); dirty = cu = 0; break; case CTRL('d'): - if (cu >= len) - return; + if (cu >= len) return; memmove(&l[cu], &l[cu + 1], len - cu - 1); dirty = cu; len--; break; case CTRL('h'): case KEY_BACKSPACE: - if (cu == 0) - return; + if (cu == 0) return; memmove(&l[cu - 1], &l[cu], len - cu); dirty = --cu; len--; break; case CTRL('w'): - if (cu == 0) - break; + if (cu == 0) break; i = 1; while (l[cu - i] == ' ' && cu - i != 0) i++; while (l[cu - i] != ' ' && cu - i != 0) i++; @@ -797,45 +932,51 @@ mvcur: wmove(scr.iw, 0, cu - shft); static void treset(void) { - if (scr.mw) - delwin(scr.mw); - if (scr.sw) - delwin(scr.sw); - if (scr.iw) - delwin(scr.iw); - endwin(); + if (scr.mw) delwin(scr.mw); + if (scr.sw) delwin(scr.sw); + if (scr.iw) delwin(scr.iw); + if (!isendwin()) endwin(); } int main(int argc, char *argv[]) { +#ifdef __OpenBSD__ +if (pledge("stdio tty rpath inet dns", NULL) == -1) + die("pledge"); +#endif /* __OpenBSD__ */ const char *user = getenv("USER"); const char *ircnick = getenv("IRCNICK"); - const char *key = getenv("IRCPASS"); + snprintf(key, sizeof(key), "%s", getenv("IRCPASS")); const char *server = SRV; const char *port = PORT; - char *err; + const char *err; int o, reconn, ping; signal(SIGPIPE, SIG_IGN); - while ((o = getopt(argc, argv, "thk:n:u:s:p:l:")) >= 0) + while ((o = getopt(argc, argv, "hvTVn:c:u:s:p:l:")) >= 0) switch (o) { case 'h': - case '?': usage: - fputs("usage: irc [-n NICK] [-u USER] [-s SERVER] [-p PORT] [-l LOGFILE ] [-t] [-h]\n", stderr); - exit(0); + die("usage: %s [-n NICK] [-u USER] [-s SERVER] [-p PORT]\n" + " [-l LOGFILE] [-c certificate] [-hvTV]", argv[0]); + break; + case 'v': + die("cio-" VERSION); + break; case 'l': if (!(logfp = fopen(optarg, "a"))) - panic("fopen: logfile"); + die("cio: fopen: logfile:"); break; case 'n': - if (strlen(optarg) >= sizeof nick) + if (strlcpy(nick, optarg, sizeof(nick)) >= sizeof(nick)) goto usage; - strcpy(nick, optarg); break; - case 't': - ssl = 1; + case 'T': + ssl = 0; + break; + case 'V': + sslverify = 0; break; case 'u': user = optarg; @@ -846,29 +987,30 @@ main(int argc, char *argv[]) case 'p': port = optarg; break; + case 'c': + if (strlcpy(cert, optarg, sizeof(cert)) >= sizeof(cert)) + goto usage; + break; } if (!user) user = "anonymous"; - if (!nick[0] && ircnick && strlen(ircnick) < sizeof nick) - strcpy(nick, ircnick); - if (!nick[0] && strlen(user) < sizeof nick) - strcpy(nick, user); + if (!nick[0] && ircnick) + strlcpy(nick, ircnick, sizeof(nick)); + if (!nick[0]) + strlcpy(nick, user, sizeof(nick)); if (!nick[0]) goto usage; + atexit(treset); tinit(); err = dial(server, port); - if (err) - panic(err); + if (err) die("cio: %s", err); chadd(server, 0); - sinit(key, nick, user); + sinit(nick, user); reconn = 0; ping = 0; while (!quit) { struct timeval t = {.tv_sec = 5}; - struct Chan *c; fd_set rfs, wfs; - int ret; - if (winchg) tresize(); FD_ZERO(&wfs); @@ -879,21 +1021,20 @@ main(int argc, char *argv[]) if (outp != outb) FD_SET(srv.fd, &wfs); } - ret = select(srv.fd + 1, &rfs, &wfs, 0, &t); - if (ret < 0) { + if (select(srv.fd + 1, &rfs, &wfs, 0, &t) < 0) { if (errno == EINTR) continue; - panic("Select failed."); + die("cio: select failed:"); } if (reconn) { hangup(); - if (reconn++ == MaxRecons + 1) - panic("Link lost."); - pushf(0, "-!- Link lost, attempting reconnection..."); + if (reconn > MaxRecons) + die("cio: link lost"); + pushf(0, "-!- Link lost, attempt %d/%d...", reconn++, MaxRecons); if (dial(server, port) != 0) continue; - sinit(key, nick, user); - for (c = chl; c < &chl[nch]; ++c) + sinit(nick, user); + for (struct Chan *c = chl; c < &chl[nch]; ++c) if (c->join) sndf("JOIN %s", c->name); reconn = 0; @@ -905,14 +1046,10 @@ main(int argc, char *argv[]) } } if (FD_ISSET(srv.fd, &wfs)) { - int wr; - - if (ssl) - wr = SSL_write(srv.ssl, outb, outp - outb); - else - wr = write(srv.fd, outb, outp - outb); + size_t len = outp - outb; + int wr = ssl ? SSL_write(srv.ssl, outb, len) : write(srv.fd, outb, len); if (wr <= 0) { - reconn = wr < 0; + reconn = 1; continue; } outp -= wr; @@ -922,10 +1059,10 @@ main(int argc, char *argv[]) tgetch(); wrefresh(scr.iw); } - if (!FD_ISSET(srv.fd, &wfs)) - if (!FD_ISSET(srv.fd, &rfs)) - if (outp == outb) - if (++ping == PingDelay) { + if ((!FD_ISSET(srv.fd, &wfs)) + && (!FD_ISSET(srv.fd, &rfs)) + && (outp == outb) + && (++ping >= PingDelay)) { sndf("PING %s", server); ping = 0; } @@ -933,6 +1070,5 @@ main(int argc, char *argv[]) hangup(); while (nch--) free(chl[nch].buf); - treset(); exit(0); } diff --git a/config.mk b/config.mk @@ -0,0 +1,27 @@ +# cio version +VERSION = 1.0 + +# Customize below to fit your system + +# paths +PREFIX = /usr/local +MANPREFIX = ${PREFIX}/share/man + +# ncurses +NCURSESINC = $(shell pkg-config --cflags-only-I ncursesw) +NCURSESLIB = $(shell pkg-config --libs ncursesw) +# OpenBSD (uncomment) +#NCURSESINC = +#NCURSESLIB = -lncurses + +# includes and libs +INCS = ${NCURSESINC} +LIBS = -lssl -lcrypto ${NCURSESLIB} + +# flags +CPPFLAGS = -D_DEFAULT_SOURCE -D_BSD_SOURCE -D_XOPEN_SOURCE=700L -DVERSION=\"${VERSION}\" +CFLAGS = -std=c99 -pedantic -Wall -Wno-deprecated-declarations -Os ${INCS} ${CPPFLAGS} +#CFLAGS = -g -std=c99 -pedantic -Wall -O0 ${INCS} ${CPPFLAGS} +LDFLAGS = ${LIBS} + +CC = cc