/** A simple (single-channel) IRC client written in C11 against POSIX * $Id: irc.c,v 1.10 2023/11/16 08:22:01 oj14ozun Exp $ * https://wwwcip.cs.fau.de/~oj14ozun/src+etc/irc.c * * I recommend reading https://modern.ircdocs.horse/ for details on * the IRC protocol that are assumed background for understanding this * program. * * Build: * * Just invoke "make irc", optionally set the environmental variable * CFLAGS. * * ToDo: * * - Add CLI flags * - Handle direct messages * - Handle more server messages * - Add commands like /invite or /names * * ChangeLog: * * (06Oct23) Finish first primitive version. * (28Sep23) Continued work. * (05Mar23) Continued work. * (13Nov22) Continued work. * (14Oct22) Initial sketch. */ /* Instead of invoking cc with a macro-definition * (-D_XOPEN_SOURCE=700), we declare the feature macro (see Info * manual (libc) Feature Test Macros) we wish to use here. */ #define _XOPEN_SOURCE 700 #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include /* The IRC server and port number are configured as compile-time * variables. If you want to connect to a different server, you'll * have to recompile the executable with a flag like * -DIRC_SERVER="foo.irc-network.com" or -DIRC_PORT=4123. */ #ifndef IRC_SERVER #define IRC_SERVER "irc.fau.de" #endif #ifndef IRC_PORT #define IRC_PORT "6666" #endif #define LENGTH(arr) (sizeof(arr) / sizeof(0[arr])) #define CR "\r" #define LF "\n" #define CRLF CR LF #define RPL_WELCOME "001" #define RPL_YOURHOST "002" #define RPL_CREATED "003" /* Error handling */ __attribute__((noreturn)) static inline void _die(char *file, unsigned line, char msg[static 1], ...) { va_list ap; assert(msg != NULL); #ifndef NDEBUG fprintf(stderr, "%s:%d: ", file, line); #endif if (msg[0] == ':') { perror(msg + 1); } else { va_start(ap, msg); vfprintf(stderr, msg, ap); va_end(ap); fputs("\n", stderr); } exit(EXIT_FAILURE); } #define die(msg, ...) _die(__FILE__, __LINE__, msg, ## __VA_ARGS__) #define fail(msg) die(":" # msg) /* Connection initialisation */ static void dialup(FILE ** rx, FILE ** tx) { int sock = -1, copy; struct addrinfo *info, *i; /* Establish a connection */ getaddrinfo(IRC_SERVER, IRC_PORT, &((struct addrinfo) { .ai_flags = AI_ADDRCONFIG, .ai_family = AF_UNSPEC, .ai_socktype = SOCK_STREAM, }), &info); for (i = info; i != NULL; i = i->ai_next) { if (-1 == (sock = socket(i->ai_family, i->ai_socktype, i->ai_protocol))) { continue; } if (0 == connect(sock, i->ai_addr, i->ai_addrlen)) { break; } close(sock); } if (sock < 0) die("Failed to connect to %s", IRC_SERVER); freeaddrinfo(info); /* Copy the descriptor to avoid sharing it between two file * separate pointers */ if (0 > (copy = dup(sock))) fail(dup); if (-1 == fcntl(sock, F_SETFL, O_NONBLOCK)) { fail(fnctl); } /* Create file pointers */ if (NULL == (*rx = fdopen(sock, "r"))) fail(fdopen); if (NULL == (*tx = fdopen(copy, "w"))) fail(fdopen); } void intro(FILE *tx, const char *nick, const char *chan) { struct passwd *passwd; /* Register the user */ fprintf(tx, "NICK %s" CRLF, nick); errno = 0; passwd = getpwuid(getuid()); if (NULL == passwd) { if (errno != 0) fail(getpwuid); else die("Failed to request user info"); } fprintf(tx, "USER %s -1 * :%s" CRLF, passwd->pw_name, passwd->pw_gecos); /* Try to join the channel */ fprintf(tx, "JOIN %s" CRLF, chan); /* Ensure the output has been sent */ if (EOF == fflush(tx)) { fail(fflush); } } /* Message handling */ struct cmd { char *cmd, *nick, *prefix; }; static void redirect(FILE * from, FILE * to, bool trim) { char line[BUFSIZ]; do { if (fgets(line, sizeof(line), from) == NULL) { if (ferror(from) && errno != EAGAIN) { fail(fgets); } break; } char *content = line; if (trim) { content = strchr(line, ':'); if (!content) break; content++; trim = false; } if (EOF == fputs(content, to)) { fail(fputs); } } while (!strchr(line, '\n')); if (EOF == fflush(to)) { fail(fflush); } } static void drain(FILE * f) { int c = 0; do { c = getc(f); } while (!(c == EOF || c == '\n')); if (ferror(f) && errno != EAGAIN) { fail(getc); } } static void print_msg(FILE * rx, FILE * tx, const struct cmd *const cmd, const void *const arg) { char *fmt = (char *) arg, dest[64] = {0}; unsigned i = 0; (void) tx; while ((dest[i] = fgetc(rx)) != EOF) { if (isspace(dest[i])) { break; } i++; } if (ferror(rx) && errno != EAGAIN) { fail(fgetc); } dest[i] = '\0'; if (fmt == NULL) fmt = ">>> %m"; for (i = 0; i < strlen(fmt); ++i) { if (fmt[i] == '%') { switch (fmt[++i]) { case '%': putchar('%'); break; case 'm': /* message */ redirect(rx, stdout, true); break; case 'c': /* command */ printf("%s", cmd->cmd); break; case 'n': /* nick */ printf("%s", cmd->nick); break; case 'p': /* prefix */ printf("%s", cmd->prefix); break; default: /* invalid formatting */ abort(); } } else { putchar(fmt[i]); } } } static void pong(FILE * rx, FILE * tx, const struct cmd *const cmd, const void *const arg) { (void) cmd; (void) arg; fputs("PONG ", tx); redirect(rx, tx, false); } static const struct { char *cmd; void (*func)(FILE * rx, FILE * tx, const struct cmd * const cmd, const void *const arg); void *arg; bool target; } handlers[] = { {.cmd = "PRIVMSG", .func = print_msg, .arg = "<%n> %m"}, {.cmd = "JOIN", .func = print_msg, .arg = "%n joined\n"}, {.cmd = "PART", .func = print_msg, .arg = "%n left: %m"}, {.cmd = "ERROR", .func = print_msg, .arg = "ERROR: %m"}, {.cmd = "PING", .func = pong}, {.cmd = RPL_WELCOME, .func = print_msg}, {.cmd = RPL_YOURHOST, .func = print_msg}, {.cmd = RPL_CREATED, .func = print_msg}, }; static void dispatch(FILE * rx, FILE * tx, const struct cmd *const cmd) { unsigned i; for (i = 0; i < LENGTH(handlers); ++i) { if (handlers[i].cmd == NULL || !strcmp(cmd->cmd, handlers[i].cmd)) { handlers[i].func(rx, tx, cmd, handlers[i].arg); return; } } drain(rx); } static void handle(FILE * rx, FILE * tx) { static enum { BOL, /* beginning of line */ SOURCE, /* in a :foo prefix */ NICK, /* inside source, parsing nick */ COMMAND, /* in a command name */ } state = BOL; static struct cmd cmd; static unsigned i = 0, j = 0, l; static char line[1 << 7] = { 0 }, nick[64]; int c; while ((c = fgetc(rx)) != EOF) { /* FIXME: Handle this situation in a better way than just * aborting. This state can actually occur, and shouldn't be * dismissed as illegal. */ assert(i <= LENGTH(line)); line[i] = c; if (c == '\n') { memset(&cmd, 0, sizeof(cmd)); line[i = 0] = '\0'; state = BOL; continue; } switch (state) { case BOL: if (':' == c) { state = NICK; cmd.prefix = &line[++i]; } else if (isalnum(c)) { ungetc(c, rx); state = COMMAND; cmd.cmd = &line[i]; l = i; } else { i++; } break; case NICK: if ('!' == c || '@' == c) { nick[j] = '\0'; state = SOURCE; } else { nick[j++] = c; } __attribute__((fallthrough)); case SOURCE: if (isspace(c)) { line[i] = '\0'; state = BOL; cmd.nick = nick; } i++; break; case COMMAND: if (isalnum(c)) { if (i - l >= sizeof(cmd.cmd) - 1) { break; } cmd.cmd[i++] = c; } else if (isspace(c)) { line[i] = '\0'; dispatch(rx, tx, &cmd); memset(line, '\0', sizeof(line)); state = BOL; j = i = 0; } else { drain(rx); state = BOL; break; } break; } } if (ferror(rx) && errno != EAGAIN) { fail(fgetc); } } static void privmsg(FILE * tx, char *chan) { fprintf(tx, "PRIVMSG %s :", chan); redirect(stdin, tx, false); } static void loop(FILE * rx, FILE * tx, char *chan) { struct pollfd fds[2] = { { .fd = fileno(stdin), .events = POLLIN }, { .fd = fileno(rx), .events = POLLIN } }; for (;;) { if (-1 == poll(fds, LENGTH(fds), -1)) fail(poll); if (fds[0].revents & POLLIN) { /* keyboard */ privmsg(tx, chan); } if (fds[0].revents & (POLLERR | POLLHUP)) { die("EOF"); } if (fds[1].revents & POLLIN) { /* network */ handle(rx, tx); } if (fds[1].revents & (POLLERR | POLLHUP)) { die("Network error"); } } } /* Entry point */ int main(int argc, char *argv[]) { char *nick, *chan; FILE *rx, *tx; /* receive, transmit */ if (argc == 1 || argc > 3) { fprintf(stderr, "Usage: %s []\n", argv[0]); exit(EXIT_SUCCESS); } chan = argv[1]; if (argc > 2) { nick = argv[2]; } else { uid_t self = getuid(); errno = 0; struct passwd *passwd = getpwuid(self); if (NULL == passwd) { if (errno != 0) fail(getpwuid); die("Failed to infer user name"); } nick = passwd->pw_name; } dialup(&rx, &tx); intro(tx, nick, chan); loop(rx, tx, chan); return 0; } /* Local Variables: */ /* show-trailing-whitespace: t */ /* indent-tabs-mode: nil */ /* End: */