diff --git a/Makefile b/Makefile index f59eb11..e0dc28e 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,5 @@ CFLAGS = -Wall -Werror -TARGETS = dispatch irc-filter irc-esc +TARGETS = bot all: $(TARGETS) diff --git a/bot.c b/bot.c new file mode 100644 index 0000000..fc062b6 --- /dev/null +++ b/bot.c @@ -0,0 +1,461 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "dispatch.h" + +#define MAX_ARGS 50 +#define MAX_SUBPROCS 50 +#define TARGET_MAX 20 + +#define max(a,b) ((a)>(b)?(a):(b)) + +char *handler = NULL; +char *msgdir = NULL; +struct timeval output_interval = {0}; + +void +maybe_setenv(char *key, char *val) +{ + if (val) { + setenv(key, val, 1); + } +} + +void +irc_filter(char *line) +{ + char *parts[20] = {0}; + int nparts; + char snick[20]; + char *cmd; + char *text = NULL; + char *prefix = NULL; + char *sender = NULL; + char *forum = NULL; + int i; + + /* Tokenize IRC line */ + nparts = 0; + if (':' == *line) { + prefix = line + 1; + } else { + parts[nparts++] = line; + } + while (*line) { + if (' ' == *line) { + *line++ = '\0'; + if (':' == *line) { + text = line+1; + break; + } else { + parts[nparts++] = line; + } + } else { + line += 1; + } + } + + /* Strip trailing carriage return */ + while (*line) line += 1; + if ('\r' == *(line-1)) *(line-1) = '\0'; + + /* Set command, converting to upper case */ + cmd = parts[0]; + for (i = 0; cmd[i]; i += 1) { + cmd[i] = toupper(cmd[i]); + } + + /* Extract prefix nickname */ + for (i = 0; prefix && (prefix[i] != '!'); i += 1) { + if (i == sizeof(snick) - 1) { + i = 0; + break; + } + snick[i] = prefix[i]; + } + snick[i] = '\0'; + if (i) { + sender = snick; + } + + /* Determine forum */ + if ((0 == strcmp(cmd, "PRIVMSG")) || + (0 == strcmp(cmd, "NOTICE"))) { + /* :neale!user@127.0.0.1 PRIVMSG #hydra :foo */ + switch (parts[1][0]) { + case '#': + case '&': + case '+': + case '!': + forum = parts[1]; + break; + default: + forum = snick; + break; + } + } else if ((0 == strcmp(cmd, "PART")) || + (0 == strcmp(cmd, "MODE")) || + (0 == strcmp(cmd, "TOPIC")) || + (0 == strcmp(cmd, "KICK"))) { + forum = parts[1]; + } else if (0 == strcmp(cmd, "JOIN")) { + if (0 == nparts) { + forum = text; + text = NULL; + } else { + forum = parts[1]; + } + } else if (0 == strcmp(cmd, "INVITE")) { + forum = text?text:parts[2]; + text = NULL; + } else if (0 == strcmp(cmd, "NICK")) { + sender = parts[1]; + forum = sender; + } else if (0 == strcmp(cmd, "PING")) { + printf("PONG :%s\r\n", text); + fflush(stdout); + } + + { + int _argc; + char *_argv[MAX_ARGS + 1]; + + maybe_setenv("handler", handler); + maybe_setenv("prefix", prefix); + maybe_setenv("command", cmd); + maybe_setenv("sender", sender); + maybe_setenv("forum", forum); + maybe_setenv("text", text); + + _argc = 0; + _argv[_argc++] = handler; + for (i = 1; (i < nparts) && (_argc < MAX_ARGS); i += 1) { + _argv[_argc++] = parts[i]; + } + _argv[_argc] = NULL; + + execvp(handler, _argv); + perror(handler); + } +} + +struct subproc { + int fd; /* File descriptor */ + char buf[4000]; /* Read buffer */ + size_t buflen; /* Buffer length */ +}; + +struct subproc subprocs[MAX_SUBPROCS] = { {0} }; + +void +dispatch(const char *buf, size_t buflen) +{ + int subout[2]; + struct subproc *s = NULL; + int i; + char text[512]; + + if (buflen > sizeof(text)) { + fprintf(stderr, "Ignoring message: too long (%u bytes)\n", (unsigned int) buflen); + return; + } + memcpy(text, buf, buflen - 1); /* omit newline */ + text[buflen - 1] = '\0'; + + for (i = 0; i < MAX_SUBPROCS; i += 1) { + if (0 == subprocs[i].fd) { + s = &subprocs[i]; + break; + } + } + if (!s) { + fprintf(stderr, "Ignoring message: too many subprocesses\n"); + return; + } + + if (-1 == pipe(subout)) { + perror("pipe"); + return; + } + + if (0 == fork()) { + /* + * Child + */ + int null; + + if ((-1 == (null = open("/dev/null", O_RDONLY))) || + (-1 == dup2(null, 0)) || + (-1 == dup2(subout[1], 1))) { + perror("fd setup"); + exit(EX_OSERR); + } + + /* + * We'll be a good citizen and only close file descriptors we opened. + */ + close(null); + close(subout[0]); + close(subout[1]); + for (i = 0; i < MAX_SUBPROCS; i += 1) { + if (subprocs[i].fd) { + close(subprocs[i].fd); + } + } + + irc_filter(text); + exit(0); + } + + s->fd = subout[0]; + close(subout[1]); +} + +void +delay_output() +{ + struct timeval now; + struct timeval diff; + static struct timeval output_last = { 0 }; + + gettimeofday(&now, NULL); + timersub(&now, &output_last, &diff); + if (timercmp(&diff, &output_interval, <)) { + struct timeval delay; + struct timespec ts; + int ret; + + timersub(&output_interval, &diff, &delay); + + ts.tv_sec = (time_t) delay.tv_sec; + ts.tv_nsec = (long) (delay.tv_usec * 1000); + do { + ret = nanosleep(&ts, &ts); + } while ((-1 == ret) && (EINTR == errno)); + gettimeofday(&output_last, NULL); + } else { + output_last = now; + } +} + + +/** Writes all of buf to stdout, possibly blocking. */ +void +output(const char *buf, size_t count) +{ + if (timerisset(&output_interval)) { + delay_output(); + } + + while (count) { + ssize_t len; + + do { + len = write(1, buf, count); + } while ((-1 == len) && (EINTR == errno)); + if (-1 == len) { + perror("stdout"); + exit(EX_IOERR); + } + count -= len; + buf += len; + } +} + +void +call_with_lines(char *buf, size_t * len, void (*func) (const char *, size_t)) +{ + char *b = buf; + char *p; + size_t l = *len; + + while ((p = memchr(b, '\n', l))) { + size_t n = p - b + 1; + + func(b, n); + l -= n; + b += n; + } + memmove(buf, b, l); + *len = l; +} + +char inbuf[8000]; +size_t inbuflen = 0; + +void +handle_input() +{ + ssize_t len; + + do { + len = read(0, inbuf + inbuflen, sizeof(inbuf) - inbuflen); + } while ((-1 == len) && (EINTR == errno)); + if (0 == len) { + exit(0); + } + inbuflen += len; + call_with_lines(inbuf, &inbuflen, dispatch); +} + +void +handle_subproc(struct subproc *s) +{ + ssize_t len; + + do { + len = read(s->fd, s->buf + s->buflen, sizeof(s->buf) - s->buflen); + } while ((-1 == len) && (EINTR == errno)); + + if (-1 == len) { + perror("subprocess read error"); + } else { + s->buflen += len; + call_with_lines(s->buf, &s->buflen, output); + } + + if (sizeof(s->buf) == s->buflen) { + fprintf(stderr, "subprocess buffer full, killing and discarding buffer.\n"); + len = 0; + } + + /* + * Recycle this subproc unless something was read + */ + if (0 >= len) { + if (s->buflen) { + fprintf(stderr, "warning: discarding %u characters from subprocess buffer\n", (unsigned int) s->buflen); + } + close(s->fd); + s->fd = 0; + s->buflen = 0; + } +} + +void +loop() +{ + int i; + int ret; + int nfds = 0; + fd_set rfds; + + FD_ZERO(&rfds); + FD_SET(0, &rfds); + for (i = 0; i < MAX_SUBPROCS; i += 1) { + if (subprocs[i].fd) { + FD_SET(subprocs[i].fd, &rfds); + nfds = max(nfds, subprocs[i].fd); + } + } + + do { + ret = select(nfds + 1, &rfds, NULL, NULL, NULL); + } while ((-1 == ret) && (EINTR == errno)); + if (-1 == ret) { + perror("select"); + exit(EX_IOERR); + } + + if (FD_ISSET(0, &rfds)) { + handle_input(); + } + + for (i = 0; i < MAX_SUBPROCS; i += 1) { + if (subprocs[i].fd && FD_ISSET(subprocs[i].fd, &rfds)) { + handle_subproc(&subprocs[i]); + } + } +} + + +void +sigchld(int signum) +{ + while (0 < waitpid(-1, NULL, WNOHANG)); +} + +void +usage(char *self) +{ + fprintf(stderr, "Usage: %s [OPTIONS] HANDLER\n", self); + fprintf(stderr, "\n"); + fprintf(stderr, "-h Display help.\n"); + fprintf(stderr, "-d DIR Also dispatch messages from DIR, one per file.\n"); + fprintf(stderr, "-i INTERVAL Wait at least INTERVAL microseconds between\n"); + fprintf(stderr, " sending each line.\n"); +} + +int +main(int argc, char *argv[]) +{ + /* + * Parse command line + */ + while (!handler) { + switch (getopt(argc, argv, "hd:i:")) { + case -1: + if (optind >= argc) { + fprintf(stderr, "error: must specify event handler.\n"); + usage(argv[0]); + return EX_USAGE; + } + handler = argv[optind]; + break; + case 'd': + msgdir = optarg; + break; + case 'i': + { + char *end; + long long int interval; + + interval = strtoll(optarg, &end, 10); + if (*end) { + fprintf(stderr, "error: not an integer number: %s\n", optarg); + return EX_USAGE; + } + output_interval.tv_sec = interval / 1000000; + output_interval.tv_usec = interval % 1000000; + } + break; + case 'h': + usage(argv[0]); + return 0; + default: + fprintf(stderr, "error: unknown option.\n"); + usage(argv[0]); + return EX_USAGE; + } + } + + /* + * tcpclient uses fds 6 and 7. If these aren't open, we keep the + * original fds 0 and 1. + */ + if (-1 != dup2(6, 0)) { + close(6); + } + if (-1 != dup2(7, 1)) { + close(7); + } + + signal(SIGCHLD, sigchld); + + while (1) { + loop(); + } + + return 0; +} diff --git a/dispatch.c b/dispatch.c deleted file mode 100644 index 1eb2885..0000000 --- a/dispatch.c +++ /dev/null @@ -1,349 +0,0 @@ -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "dump.h" - -#define MAX_ARGS 50 -#define MAX_SUBPROCS 50 -#define TARGET_MAX 20 - -#define max(a,b) ((a)>(b)?(a):(b)) - -struct subproc { - int fd; /* File descriptor */ - char buf[4000]; /* Read buffer */ - size_t buflen; /* Buffer length */ -}; - -struct subproc subprocs[MAX_SUBPROCS] = {{0}}; - -/* Things set by argv parser */ -char *handler = NULL; -char **handler_args; -struct timeval output_interval = {0}; -struct timeval output_last = {0}; -int fifoin = -1; -int fifoout = -1; - -void -dispatch(const char *buf, - size_t buflen) -{ - int subout[2]; - struct subproc *s = NULL; - int i; - char text[512]; - - if (buflen > sizeof(text)) { - fprintf(stderr, "Ignoring message: too long (%u bytes)\n", (unsigned int)buflen); - return; - } - memcpy(text, buf, buflen-1); /* omit newline */ - text[buflen-1] = '\0'; - - for (i = 0; i < MAX_SUBPROCS; i += 1) { - if (0 == subprocs[i].fd) { - s = &subprocs[i]; - break; - } - } - if (! s) { - fprintf(stderr, "Ignoring message: too many subprocesses\n"); - return; - } - - if (-1 == pipe(subout)) { - perror("pipe"); - return; - } - - if (0 == fork()) { - /* Child */ - char *argv[MAX_ARGS + 5]; - int null; - int i; - - if ((-1 == (null = open("/dev/null", O_RDONLY))) || - (-1 == dup2(null, 0)) || - (-1 == dup2(subout[1], 1))) { - perror("fd setup"); - exit(EX_OSERR); - } - - /* We'll be good citizens about this and only close file descriptors - we opened. */ - close(fifoout); - close(null); - close(subout[0]); - close(subout[1]); - for (i = 0; i < MAX_SUBPROCS; i += 1) { - if (subprocs[i].fd) { - close(subprocs[i].fd); - } - } - - i = 0; - argv[i++] = handler; - for (; handler_args[i-1]; i += 1) { - argv[i] = handler_args[i-1]; - } - argv[i++] = text; - argv[i] = NULL; - - execvp(handler, argv); - perror("exec"); - exit(0); - } - - s->fd = subout[0]; - close(subout[1]); -} - -void -delay_output() -{ - struct timeval now, diff; - - gettimeofday(&now, NULL); - timersub(&now, &output_last, &diff); - if (timercmp(&diff, &output_interval, <)) { - struct timeval delay; - struct timespec ts; - int ret; - - timersub(&output_interval, &diff, &delay); - - ts.tv_sec = (time_t)delay.tv_sec; - ts.tv_nsec = (long)(delay.tv_usec * 1000); - do { - ret = nanosleep(&ts, &ts); - } while ((-1 == ret) && (EINTR == errno)); - gettimeofday(&output_last, NULL); - } else { - output_last = now; - } -} - - -/** Writes all of buf to stdout, possibly blocking. */ -void -output(const char *buf, - size_t count) -{ - if (timerisset(&output_interval)) { - delay_output(); - } - - while (count) { - ssize_t len; - - do { - len = write(1, buf, count); - } while ((-1 == len) && (EINTR == errno)); - if (-1 == len) { - perror("stdout"); - exit(EX_IOERR); - } - count -= len; - buf += len; - } -} - -void -call_with_lines(char *buf, - size_t *len, - void (*func)(const char *, size_t)) -{ - char *b = buf; - char *p; - size_t l = *len; - - while ((p = memchr(b, '\n', l))) { - size_t n = p - b + 1; - - func(b, n); - l -= n; - b += n; - } - memmove(buf, b, l); - *len = l; -} - -char inbuf[8000]; -size_t inbuflen = 0; - -void -handle_input() -{ - ssize_t len; - - do { - len = read(0, inbuf + inbuflen, sizeof(inbuf) - inbuflen); - } while ((-1 == len) && (EINTR == errno)); - if (0 == len) { - exit(0); - } - inbuflen += len; - call_with_lines(inbuf, &inbuflen, dispatch); -} - -void -handle_subproc(struct subproc *s) -{ - ssize_t len; - - do { - len = read(s->fd, s->buf + s->buflen, sizeof(s->buf) - s->buflen); - } while ((-1 == len) && (EINTR == errno)); - if (-1 == len) { - perror("subprocess read error"); - } else { - s->buflen += len; - call_with_lines(s->buf, &s->buflen, output); - } - - if (sizeof(s->buf) == s->buflen) { - fprintf(stderr, "subprocess buffer full, killing and discarding buffer.\n"); - len = 0; - } - - /* Recycle this subproc unless something was read */ - if (0 >= len) { - if (s->buflen) { - fprintf(stderr, "warning: discarding %u characters from subprocess buffer\n", - (unsigned int)s->buflen); - } - close(s->fd); - s->fd = 0; - s->buflen = 0; - } -} - -void -loop() -{ - int i, ret; - int nfds = 0; - fd_set rfds; - - FD_ZERO(&rfds); - FD_SET(0, &rfds); - for (i = 0; i < MAX_SUBPROCS; i += 1) { - if (subprocs[i].fd) { - FD_SET(subprocs[i].fd, &rfds); - nfds = max(nfds, subprocs[i].fd); - } - } - - do { - ret = select(nfds+1, &rfds, NULL, NULL, NULL); - } while ((-1 == ret) && (EINTR == errno)); - if (-1 == ret) { - perror("select"); - exit(EX_IOERR); - } - - if (FD_ISSET(0, &rfds)) { - handle_input(); - } - - for (i = 0; i < MAX_SUBPROCS; i += 1) { - if (subprocs[i].fd && FD_ISSET(subprocs[i].fd, &rfds)) { - handle_subproc(&subprocs[i]); - } - } -} - -void -sigchld(int signum) -{ - while (0 < waitpid(-1, NULL, WNOHANG)); -} - -void -usage(char *self) -{ - fprintf(stderr, "Usage: %s [OPTIONS] handler [ARGS ...]\n", self); - fprintf(stderr, "\n"); - fprintf(stderr, "-f FIFO Also dispatch messages from FIFO.\n"); - fprintf(stderr, "-i INTERVAL Wait at least INTERVAL microseconds between\n"); - fprintf(stderr, " sending each line.\n"); -} - -int -main(int argc, char *argv[]) -{ - /* Parse command line */ - while (! handler) { - switch (getopt(argc, argv, "hf:i:")) { - case -1: - if (optind >= argc) { - fprintf(stderr, "error: must specify handler script.\n"); - usage(argv[0]); - return EX_USAGE; - } - if (argc - optind - 10 > MAX_ARGS) { - fprintf(stderr, "error: too many arguments to helper.\n"); - return EX_USAGE; - } - handler = argv[optind]; - handler_args = argv + (optind + 1); - break; - case 'f': - if ((-1 == (fifoin = open(optarg, O_RDONLY | O_NONBLOCK))) || - (-1 == (fifoout = open(optarg, O_WRONLY)))) { - perror("open fifo"); - return EX_IOERR; - } - subprocs[0].fd = fifoin; - break; - case 'i': - { - char *end; - long long int interval; - - interval = strtoll(optarg, &end, 10); - if (*end) { - fprintf(stderr, "error: not an integer number: %s\n", optarg); - return EX_USAGE; - } - output_interval.tv_sec = interval / 1000000; - output_interval.tv_usec = interval % 1000000; - } - break; - case 'h': - usage(argv[0]); - return 0; - default: - fprintf(stderr, "error: unknown option.\n"); - usage(argv[0]); - return EX_USAGE; - } - } - - /* tcpclient uses fds 6 and 7. If these aren't open, we keep the - original fds 0 and 1. */ - if (-1 != dup2(6, 0)) close(6); - if (-1 != dup2(7, 1)) close(7); - - signal(SIGCHLD, sigchld); - - while (1) { - loop(); - } - - return 0; -} diff --git a/irc-filter.c b/irc-filter.c deleted file mode 100644 index d297460..0000000 --- a/irc-filter.c +++ /dev/null @@ -1,154 +0,0 @@ -#include -#include -#include -#include -#include - -#include "dump.h" - -#define MAX_ARGS 50 -#define MAX_OUTARGS 60 -#define MAX_PARTS 20 - -int -main(int argc, char *argv[]) -{ - char *parts[20] = {0}; - int nparts; - char snick[20]; - char *cmd; - char *text = NULL; - char *prefix = NULL; - char *sender = NULL; - char *forum = NULL; - int i; - - if (argc < 3) { - fprintf(stderr, "Usage: %s HANDLER [ARGV ...] LINE\n", argv[0]); - fprintf(stderr, "\n"); - fprintf(stderr, "Parses LINE (an IRC message) into:\n"); - fprintf(stderr, " PREFIX Prefix part of message\n"); - fprintf(stderr, " COMMAND IRC command\n"); - fprintf(stderr, " SENDER Nickname of message's sender\n"); - fprintf(stderr, " FORUM Forum of message\n"); - fprintf(stderr, " TEXT Text part of message\n"); - fprintf(stderr, " ARGS... Arguments of message\n"); - fprintf(stderr, "\n"); - fprintf(stderr, "After parsing, exec()s\n"); - fprintf(stderr, " HANDLER ARGV... PREFIX COMMAND SENDER FORUM TEXT ARGS...\n"); - return EX_USAGE; - } else if (argc > MAX_ARGS) { - fprintf(stderr, "%s: too many arguments\n", argv[0]); - return EX_USAGE; - } - - /* Tokenize IRC line */ - { - char *line = argv[argc-1]; - - nparts = 0; - if (':' == *line) { - prefix = line + 1; - } else { - parts[nparts++] = line; - } - while (*line) { - if (' ' == *line) { - *line++ = '\0'; - if (':' == *line) { - text = line+1; - break; - } else { - parts[nparts++] = line; - } - } else { - line += 1; - } - } - - /* Strip trailing carriage return */ - while (*line) line += 1; - if ('\r' == *(line-1)) *(line-1) = '\0'; - } - - /* Set command, converting to upper case */ - cmd = parts[0]; - for (i = 0; cmd[i]; i += 1) { - cmd[i] = toupper(cmd[i]); - } - - /* Extract prefix nickname */ - for (i = 0; prefix && (prefix[i] != '!'); i += 1) { - if (i == sizeof(snick) - 1) { - i = 0; - break; - } - snick[i] = prefix[i]; - } - snick[i] = '\0'; - if (i) { - sender = snick; - } - - /* Determine forum */ - if ((0 == strcmp(cmd, "PRIVMSG")) || - (0 == strcmp(cmd, "NOTICE"))) { - /* :neale!user@127.0.0.1 PRIVMSG #hydra :foo */ - switch (parts[1][0]) { - case '#': - case '&': - case '+': - case '!': - forum = parts[1]; - break; - default: - forum = snick; - break; - } - } else if ((0 == strcmp(cmd, "PART")) || - (0 == strcmp(cmd, "MODE")) || - (0 == strcmp(cmd, "TOPIC")) || - (0 == strcmp(cmd, "KICK"))) { - forum = parts[1]; - } else if (0 == strcmp(cmd, "JOIN")) { - if (0 == nparts) { - forum = text; - text = NULL; - } else { - forum = parts[1]; - } - } else if (0 == strcmp(cmd, "INVITE")) { - forum = text?text:parts[2]; - text = NULL; - } else if (0 == strcmp(cmd, "NICK")) { - sender = parts[1]; - forum = sender; - } else if (0 == strcmp(cmd, "PING")) { - printf("PONG :%s\r\n", text); - fflush(stdout); - } - - { - int _argc; - char *_argv[MAX_OUTARGS + 1]; - - _argc = 0; - for (i = 1; i < argc-1; i += 1) { - _argv[_argc++] = argv[i]; - } - _argv[_argc++] = prefix?prefix:""; - _argv[_argc++] = cmd; - _argv[_argc++] = sender?sender:""; - _argv[_argc++] = forum?forum:""; - _argv[_argc++] = text?text:""; - for (i = 1; (i < nparts) && (_argc < MAX_OUTARGS); i += 1) { - _argv[_argc++] = parts[i]; - } - _argv[_argc] = NULL; - - execvp(_argv[0], _argv); - perror(_argv[0]); - } - - return 0; -}