#define _POSIX_C_SOURCE 200809L /* getopt(>=2), dprintf(>=200809L), strtok_r(*), getaddrinfo(>=200112L) */ #include #include #include #include #include #include #include #include #include #include #define DEFAULT_PING 60000 /*ms*/ #define DEFAULT_TIMEOUT 2000 /*ms*/ #define DEFAULT_TLS NO_TLS #define DEFAULT_PORT_TCP "6667" #define DEFAULT_PORT_TLS "6697" #define POLL_TIMEOUT 100 #define STR_(x) #x #define STR(x) STR_(x) #define OR_DIE < 0 && (perror(__FILE__ ":" STR(__LINE__)), exit(1), 0) #define OR_DIE_gai(err) if (err) {fprintf(stderr, __FILE__ ":" STR(__LINE__) ": %s\n", gai_strerror(err));exit(1);} #define OR_DIE_tls(ctx) < 0 && (exit((fprintf(stderr, __FILE__ ":" STR(__LINE__) ": %s\n", tls_error(ctx)), 1)), 0) #define OR_DIE_irc < 0 && (exit((fprintf(stderr, __FILE__ ":" STR(__LINE__) ": %s\n", "got IRC error"), 1)), 0) enum pass_type_e { NO_PASSWD, SERVER_PASSWD, SASL_PLAIN_PASSWD }; enum tls_use_e { NO_TLS, USE_TLS, INSECURE_TLS }; typedef struct { int fd; /* always contains the underlying file descriptor */ struct tls *tls; /* tls context, or NULL with plain socket */ } sock_t; #define _IMPLFN(fn, sock, buf, sz) ( \ sock.tls \ ? tls_ ## fn(sock.tls, buf, sz) \ : fn(sock.fd, buf, sz) \ ) #define READ(sock, buf, sz) _IMPLFN(read, sock, buf, sz) #define WRITE(sock, buf, sz) _IMPLFN(write, sock, buf, sz) void irc_help(const char *exe, const int code) { fprintf(stderr, "Usage: %s [-pP] [-sSk] [-n NICK] [-j CHAN] HOST [PORT]\n", exe); exit(code); } sock_t irc_connect(const char *host, const char *port, const int tls, const char *ca_file) { sock_t sock; struct addrinfo *results, *r; int err = getaddrinfo(host, port, NULL, &results); OR_DIE_gai(err); /*unable to resolve*/ for (r = results; r != NULL; r = r->ai_next) { sock.fd = socket(r->ai_family, SOCK_STREAM, 0); if (sock.fd < 0) continue; /* try next; todo: should check errno */ if (connect(sock.fd, r->ai_addr, r->ai_addrlen) == 0) break; /* successfully connected */ close(sock.fd); /* failed, try next addr */ } if (r == NULL) { /* all failed; abort. */ sock.fd = -1; } else { /* connection established. */ if (tls != NO_TLS) { struct tls *ctx = tls_client(); struct tls_config *cfg = tls_config_new(); if (tls == INSECURE_TLS) { tls_config_insecure_noverifycert(cfg); tls_config_insecure_noverifyname(cfg); tls_config_insecure_noverifytime(cfg); tls_config_set_ciphers(cfg, "legacy"); /* even more: 'insecure' */ } tls_config_set_dheparams(cfg, "auto") OR_DIE_tls(ctx); if (ca_file) tls_config_set_ca_file(cfg, ca_file) OR_DIE_tls(ctx); /* todo: if ca_file ends in /, call tls_config_set_ca_path() instead */ /* todo: otherwise, set to tls_default_ca_cert_file() iff libtls (not libretls) */ tls_configure(ctx, cfg) OR_DIE_tls(ctx); tls_config_free(cfg); tls_connect_socket(ctx, sock.fd, host) OR_DIE_tls(ctx); tls_handshake(ctx) OR_DIE_tls(ctx); sock.tls = ctx; } else sock.tls = NULL; /* connect timeout here */ } freeaddrinfo(results); return sock; } enum { /* requested command: */ NO_CMD = 0, NICK = 1<<0, JOIN = 1<<1, PING = 1<<2, ERRS = 1<<3 }; int irc_answer(const sock_t sock, char *buf, const unsigned int command) { unsigned int seen = 0; char *saveptr; char *line = strtok_r(buf, "\n", &saveptr); /*TODO: it often happens that we take multiple calls to read() all the available lines (e.g. large motd, NAMES message). when this happens, one call to read() will return an incomplete line, and the next will start in the middle of a line. this can't be parsed properly! we need to check if the last line ends with a newline. that's hard because we use strtok which removes newlines. on the second read we should either skip over the first partial line or better, defer parsing the last line of the first read until we have the complete line.*/ do { /* skip over prefix (servername): */ if (line[0] == ':') while (*line && *line++ != ' '); /* look for command responses or error numerics, if any: */ switch (command) { case PING: seen |= PING * (strncmp(line, "PONG ", 5)==0); break; case JOIN: seen |= JOIN * (strncmp(line, "JOIN ", 5)==0); seen |= ERRS * (strncmp(line, "403 ", 4)==0); seen |= ERRS * (strncmp(line, "405 ", 4)==0); seen |= ERRS * (strncmp(line, "471 ", 4)==0); seen |= ERRS * (strncmp(line, "473 ", 4)==0); seen |= ERRS * (strncmp(line, "474 ", 4)==0); seen |= ERRS * (strncmp(line, "475 ", 4)==0); seen |= ERRS * (strncmp(line, "476 ", 4)==0); break; case NICK: seen |= NICK * (strncmp(line, "001 ", 4)==0); seen |= ERRS * (strncmp(line, "432 ", 4)==0); seen |= ERRS * (strncmp(line, "433 ", 4)==0); seen |= ERRS * (strncmp(line, "436 ", 4)==0); seen |= ERRS * (strncmp(line, "464 ", 4)==0); seen |= ERRS * (strncmp(line, "902 ", 4)==0); seen |= ERRS * (strncmp(line, "904 ", 4)==0); break; } /* look for fatal error, if any */ if (strncmp(line, "ERROR ", 6)==0) seen |= ERRS; if (seen & ERRS) { /* TODO: set &buf to line to preserve error across function call */ return seen; } /* reply to pings: */ if (strncmp(line, "PING ", 5) == 0) { line[1] = 'O'; /* PING :foo -> PONG :foo */ WRITE(sock, line, strlen(line)); WRITE(sock, "\r\n", 2); } } while ((line = strtok_r(NULL, "\n", &saveptr))); return seen; } int irc_base64(char *buf, int n) { const char *b = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; int i, o, v, l = ((n+(3-n%3)%3)/3)*4; if (l >= 400) return -1; /* lazy */ buf[n+1] = buf[n+2] = buf[l] = '\0'; for (i=(n+(3-n%3)%3)-3, o=l-4; i>=0 && o>=0; i-=3, o-=4) { v = buf[i+0]<<16 | buf[i+1]<<8 | buf[i+2]<<0; buf[o+0] = b[v>>18 & 0x3f]; buf[o+1] = b[v>>12 & 0x3f]; buf[o+2] = (i+1>06 & 0x3f]:'='; buf[o+3] = (i+2>00 & 0x3f]:'='; } return l; } int irc_setup(const sock_t sock, const int outfd, const char *nick, const char *pass, int pass_type, const char *chan) { char buf[BUFSIZ]; int n; struct pollfd fds[1]; fds[0].fd = sock.fd; fds[0].events = POLLIN; if (pass_type == SASL_PLAIN_PASSWD) { n = snprintf(buf, BUFSIZ, "CAP REQ :sasl\r\n"); WRITE(sock, buf, n); } else if (pass_type == SERVER_PASSWD) { n = snprintf(buf, BUFSIZ, "PASS %s\r\n", pass); WRITE(sock, buf, n); } n = snprintf(buf, BUFSIZ, "NICK %s\r\n", nick); WRITE(sock, buf, n); n = snprintf(buf, BUFSIZ, "USER %s 0 * :%s\r\n", nick, nick); WRITE(sock, buf, n); if (pass_type == SASL_PLAIN_PASSWD) { int n2; /* should wait for 'CAP ACK :<...>' */ WRITE(sock, "AUTHENTICATE PLAIN\r\n", 20); /* server sends 'AUTHENTICATE +' */ /* split base64-output into 400 byte chunks; if last is exactly 400 bytes, send empty msg ('+') afterwards */ n = snprintf(buf, BUFSIZ, "AUTHENTICATE "); n2 = snprintf(buf+n, BUFSIZ-n, "%s%c%s%c%s", nick, 0, nick, 0, pass); irc_base64(buf+n, n2) OR_DIE; snprintf(buf+n+n2, BUFSIZ-n-n2, "\r\n"); WRITE(sock, buf, n+n2+2); /* wait for response 900+903 (ok) or 904 (err) */ WRITE(sock, "CAP END\r\n", 9); } /* block until we get a RPL_WELCOME or an error: */ for (;;) { if (poll(fds, 1, POLL_TIMEOUT)) { n = READ(sock, buf, BUFSIZ); write(outfd, buf, n); n = irc_answer(sock, buf, NICK); if (n & NICK) break; else if (n & ERRS) return -1; /* mostly for 433, 464 */ } } if (chan) { n = snprintf(buf, BUFSIZ, "JOIN %s\r\n", chan); WRITE(sock, buf, n); /* block until we get a JOIN response or an error: */ /* todo: dedup this block with NICK/RPL_WELCOME */ for (;;) { if (poll(fds, 1, POLL_TIMEOUT)) { n = READ(sock, buf, BUFSIZ); write(outfd, buf, n); n = irc_answer(sock, buf, JOIN); if (n & JOIN) break; else if (n & ERRS) return -1; /* mostly for 403, 471-475 */ } } } return 0; } int irc_poll(const sock_t sock, const int infd, const int outfd) { int n; char buf[BUFSIZ]; enum { IRC, CLI }; struct pollfd fds[2]; fds[IRC].fd = sock.fd; fds[IRC].events = POLLIN; fds[CLI].fd = infd; fds[CLI].events = POLLIN; for (;;) { poll(fds, 2, POLL_TIMEOUT) OR_DIE; /* XXX: long responses don't get fully processed until user input */ /* XXX: must handle TLS_WANT_POLLIN and TLS_WANT_POLLOUT for READ and WRITE! */ if (fds[IRC].revents & POLLIN) { n = READ(sock, buf, BUFSIZ); if (n == 0) return -1; /* server closed connection */ fds[IRC].events = POLLIN | (n==TLS_WANT_POLLOUT?POLLOUT:0); write(outfd, buf, n); irc_answer(sock, buf, NO_CMD); /* update last-msg-rcvd here */ } if (fds[CLI].revents & POLLIN) { n = read(infd, buf, BUFSIZ); if (n == 0) return 0; /* we closed connection */ n = WRITE(sock, buf, n); fds[IRC].events = POLLIN | (n==TLS_WANT_POLLOUT?POLLOUT:0); } if (fds[IRC].revents & POLLOUT) { /* needed for TLS only */ n = WRITE(sock, buf, n); fds[IRC].events = POLLIN | (n==TLS_WANT_POLLOUT?POLLOUT:0); } /* TODO: if read/write on either irc or cli returns -1 and errno is EAGAIN or EINTR, retry. otherwise, return with error */ /* send ping here */ /* dprintf(sockfd, "PING\r\n"); // poll-read if (irc_answer(sockfd, buf, NICK) & NICK) break; */ } } void irc_cleanup(const sock_t sock) { WRITE(sock, "QUIT :ircpipe\r\n", 15); if (sock.tls) tls_close(sock.tls); shutdown(sock.fd, SHUT_RDWR); close(sock.fd); } int main(int argc, char **argv) { char *host = NULL; char *port = NULL; char *nick = NULL; char *pass = NULL; char *chan = NULL; size_t ping_iv = DEFAULT_PING; /* interval between outgoing pings */ size_t resp_to = DEFAULT_TIMEOUT; /* how long to wait for command response (connect, ping, auth, ...) */ int tls = DEFAULT_TLS; char *ca_file = NULL; int pass_type = NO_PASSWD; sock_t sock; int rv; int opt; opterr = 0; pass = getenv("IRC_PASSWD"); ca_file = getenv("IRC_CAFILE"); while ((opt = getopt(argc, argv, "n:j:pPsSkh")) != -1) { switch (opt) { case 'n': nick = optarg; break; case 'p': pass_type = SERVER_PASSWD; break; case 'P': pass_type = SASL_PLAIN_PASSWD; break; case 's': tls = USE_TLS; break; case 'S': tls = NO_TLS; break; case 'k': tls = INSECURE_TLS; break; case 'j': chan = optarg; break; default: irc_help(argv[0], opt != 'h'); } } if (optind < argc) { host = argv[optind++]; } else { /* too few positional arguments */ fprintf(stderr, "missing HOST\n"); irc_help(argv[0], 1); } if (optind < argc) { port = argv[optind++]; } else { port = (tls == NO_TLS) ? DEFAULT_PORT_TCP : DEFAULT_PORT_TLS; } if (optind < argc) { /* too many positional arguments */ fprintf(stderr, "too many args\n"); irc_help(argv[0], 1); } if (pass_type != NO_PASSWD && pass == NULL) { fprintf(stderr, "must set IRC_PASSWD envvar to use -p/-P\n"); exit(1); } sock = irc_connect(host, port, tls, ca_file); sock.fd OR_DIE; irc_setup(sock, 1, nick, pass, pass_type, chan) OR_DIE_irc; rv = irc_poll(sock, 0, 1); irc_cleanup(sock); return (rv < 0); }