/* Small daemon that connects to IRC server and joins given channel and sends whatever is written to the FIFO Intended usage is git hook that sends commits, etc. */ #define _GNU_SOURCE #include #include #include #include #include #include #include #include #include #include #include #include "irc.h" #define PROGNAME "sircbot" #if !defined(DEFAULT_PIDFILE) #define DEFAULT_PIDFILE "/var/run/" PROGNAME "/" PROGNAME ".pid" #endif struct tq_string { char *value; TAILQ_ENTRY(tq_string) entries; }; typedef TAILQ_HEAD(tq_stringlist, tq_string) TQ_STRINGLIST; struct sircbot_channel { char *name; char *fifo; int fd; time_t last_data_sent; time_t last_closetime; /* last time we got kicked */ TQ_STRINGLIST queue_head; /* queue of in-data */ }; static int foreground = 0; static int sigterm = 0; static int flush_rate = 2; static int write_pid(const char *file) { int fd, n; char tmp[16]; fd = open(file, O_CREAT | O_WRONLY, 0660); if (fd < 0) { perror(file); return -1; } n = snprintf(tmp, sizeof(tmp), "%d\n", getpid()); if (write(fd, tmp, n) == n) n = 0; close(fd); return n; } int daemonize(const char *pidfile, const char *logfile) { int devnull, f; int pid = fork(); if (pid < 0) { perror("fork"); return -1; } /* exit parent */ if (pid > 0) exit(0); /* detatch to controling terminal */ setsid(); pid = fork(); if (pid < 0) { perror("fork"); return -1; } /* exit parent */ if (pid > 0) exit(0); if (write_pid(pidfile) < 0) return -1; /* redirect stdin/stdout/stderr to /dev/null */ freopen("/dev/null", "r", stdin); freopen(logfile, "w", stdout); freopen(logfile, "w", stderr); return 0; } static void log_err(const char *msg) { syslog(LOG_ERR, "%s: %s", msg, strerror(errno)); } struct tq_string *tq_stringlist_add(TQ_STRINGLIST *list, const char *data) { struct tq_string *item = malloc(sizeof(struct tq_string)); if (item == NULL) { log_err("malloc"); return NULL; } if ((item->value = strdup(data)) == NULL) { log_err("strdup"); return NULL; } TAILQ_INSERT_TAIL(list, item, entries); return item; } int init_fifo(struct sircbot_channel *chan) { unlink(chan->fifo); if (mkfifo(chan->fifo, 0666) < 0) return -1; chan->fd = open(chan->fifo, O_RDONLY | O_NONBLOCK); if (chan->fd < 0) return -1; return 0; } int close_fifo(struct sircbot_channel *chan, time_t closetime) { close(chan->fd); chan->fd = -1; chan->last_closetime = closetime; return unlink(chan->fifo); } int join_channel(struct sircbot_channel *chan, int numchan, char *name) { int i; for (i = 0; i < numchan; i++) { if (strcmp(chan[i].name, name) == 0) return init_fifo(&chan[i]); } return 0; } int kick_channel(struct sircbot_channel *chan, int numchan, char *name) { int i; time_t now = time(NULL); printf("got kicked from %s\n", name); for (i = 0; i < numchan; i++) { if (strcmp(chan[i].name, name) == 0) return close_fifo(&chan[i], now); } return 0; } int run_hooks(char *user, char *rcpt, char* data) { int pid = fork(); int status; char dir[PATH_MAX]; if (pid < 0) { log_err("fork"); return -1; } if (pid == 0) { /* detatch to controling terminal */ setsid(); pid = fork(); if (pid < 0) { perror("fork"); exit(1); } /* exit parent */ if (pid > 0) exit(0); snprintf(dir, sizeof(dir), "/etc/" PROGNAME ".d/%s", rcpt); printf("DEBUG: running scripts in %s\n", dir); execlp("/bin/run-parts", "/bin/run-parts", "-a", user, "-a", data, "-a", rcpt, dir, NULL); log_err("run-parts"); exit(1); } wait(&status); return 0; } int handle_response(struct irc_session *sess, char *user, char *cmd, char *data, struct sircbot_channel *chan, int numchan) { printf("DEBUG: handling response: user=%s, cmd=%s, data=%s\n", user, cmd, data); if (strncmp(cmd, "PING", 4) == 0) { return irc_send(sess, "PONG", data); } else if (strncmp(cmd, "PONG", 4) == 0) { sess->last_pong = time(NULL); } else if (strncmp(cmd, "JOIN", 4) == 0 && strncmp(user, sess->nick, strlen(sess->nick)) == 0) { return join_channel(chan, numchan, data); } else if (strncmp(cmd, "KICK", 4) == 0) { char *p = strchr(data, ' '); if (p) *p = '\0'; return kick_channel(chan, numchan, data); } else if (strncmp(cmd, "PRIVMSG", 7) == 0) { char *p = strchr(data, ' '); if (p) { *p++ = '\0'; if (*p == ':') p++; } return run_hooks(user, data, p); } return 0; } int parse_line(struct irc_session *sess, char *line, struct sircbot_channel *chan, int numchan) { char *user = NULL, *p, *cmd, *data = NULL; printf("DEBUG: parsing: '%s'\n", line); if (line[0] == ':') { user = &line[1]; p = strchr(user, ' '); if (p == NULL) return -1; *p++ = '\0'; cmd = p; if ((p = strchr(user, '!'))) *p = '\0'; } else cmd = line; p = strchr(cmd, ' '); if (p) { *p++ = '\0'; data = p; if (*data == ':') data++; } // printf("DEBUG: user='%s', cmd='%s', data='%s'\n", user, cmd, data); handle_response(sess, user, cmd, data, chan, numchan); return 0; } int parse_irc_data(char *buf, struct sircbot_channel *chan, int numchan, struct irc_session *sess) { char *p = buf; while ((p = strsep(&buf, "\n")) != NULL) { char *c = strchr(p, '\r'); if (c != NULL) *c = '\0'; if (*p != '\0') parse_line(sess, p, chan, numchan); } return 0; } /* init pollfd strucs */ static void irc_reset_pollfds(struct irc_session *sess, struct pollfd *fds, struct sircbot_channel *chan, int numchan) { int i; /* first pollfd struc is the irc session */ fds[0].fd = sess->fd; fds[0].events = POLLIN; fds[0].revents = 0; /* rest is channel fifos */ for (i = 1; i < numchan + 1; i++) { fds[i].fd = chan[i-1].fd; fds[i].events = POLLIN; fds[i].revents = 0; } } static int send_fifo_queue(struct irc_session *sess, struct sircbot_channel *chan, time_t now) { int r; struct tq_string *item = TAILQ_FIRST(&chan->queue_head); if (item == NULL || (now - chan->last_data_sent) < flush_rate) return 0; /* nothing in queue, or too early to send */ r = irc_send_chan(sess, chan->name, item->value); chan->last_data_sent = now; /* remove from FIFO queue */ TAILQ_REMOVE(&chan->queue_head, item, entries); free(item->value); free(item); return r; } static int irc_loop(struct irc_session *sess, struct sircbot_channel *chan, int numchan) { int i, r, joined = 0; char buf[4096]; struct pollfd fds[numchan + 1]; sigset_t sigmask; struct timespec tv; time_t now, last_ping; sigemptyset(&sigmask); sigaddset(&sigmask, SIGTERM); tv.tv_sec = 1; tv.tv_nsec = 0; while (!sigterm) { now = time(NULL); /* wait atleast 5 secs before we join a channel */ for (i = 0; i < numchan; i++) if ((now - chan[i].last_closetime) > 5 && chan[i].fd < 0) { printf("DEBUG: joining %s\n", chan[i].name); chan[i].last_closetime = now; irc_send(sess, "JOIN", chan[i].name); } irc_reset_pollfds(sess, fds, chan, numchan); r = ppoll(fds, numchan+1, &tv, &sigmask); // r = poll(fds, numchan+1, 120000); if (r < 0) { log_err("ppoll"); return -1; } now = time(NULL); /* send a ping every 2 min */ if ((now - last_ping) > 120) { irc_send_ping(sess); last_ping = now; } for (i = 0; i < numchan + 1; i++) { int j; /* send data in queue for this fifo */ if (i != 0 && send_fifo_queue(sess, &chan[i-1], now) < 0) goto ret_err; if (fds[i].revents & POLLHUP) { if (i == 0) /* server hang up on us */ return 0; /* one of the fifos closed due to writer process exit. We just reopen it assuming we are still joined the channel */ close_fifo(&chan[i-1], chan[i-1].last_closetime); init_fifo(&chan[i-1]); continue; } if (fds[i].revents & POLLERR) goto ret_err; if (!(fds[i].revents & POLLIN)) continue; /* no data available for read */ printf("DEBUG: data available from fds[%i]\n", i); r = read(fds[i].fd, buf, sizeof(buf)-1); if (r < 0) goto ret_err; if (r == 0) continue; printf("DEBUG: read %i bytes\n", r); buf[r] = '\0'; if (i == 0) { /* data was from IRC server */ printf("DEBUG: data from server: %s\n", buf); parse_irc_data(buf, chan, numchan, sess); continue; } /* data was from fifos */ printf("DEBUG: data from fifo %s: %s\n", chan[i-1].name, buf); tq_stringlist_add(&chan[i-1].queue_head, buf); } } return 0; ret_err: log_err(sess->server); return -1; } void sighandler(int signal) { switch (signal) { case SIGTERM: sigterm = 1; break; } } static void usage_exit(int exitcode) { printf("sircbot " VERSION "\nusage: sircbot [-f] [-n nick] [-r flushrate] [-s server] [-p port] CHANNEL\n"); exit(exitcode); } int main(int argc, char *argv[]) { const char *pidfile = DEFAULT_PIDFILE; const char *server = "irc.freenode.org"; const char *nick = "sircbot"; const char *user = "sircbot"; const char *group = "sircbot"; const char *pass = NULL; const char *logfile = "/dev/null"; struct sircbot_channel *chan; int i, c, port = 6667; while ((c = getopt(argc, argv, "fl:n:p:P:r:s:")) != -1) { switch (c) { case 'f': foreground = 1; break; case 'l': logfile = optarg; break; case 'n': nick = optarg; break; case 'P': pass = optarg; break; case 'p': port = atoi(optarg); break; case 'r': flush_rate = atoi(optarg); break; case 's': server = optarg; break; default: usage_exit(1); } } argv += optind; argc -= optind; if (argc <= 0) usage_exit(1); /* init channel strucs */ chan = malloc(argc * sizeof(struct sircbot_channel)); if (chan == NULL) err(1, "malloc"); for (i = 0; i < argc; i++) { chan[i].fd = -1; chan[i].name = strdup(argv[i]); chan[i].last_closetime = 0; asprintf(&chan[i].fifo, "/var/run/sircbot/%s", argv[i]); unlink(chan[i].fifo); TAILQ_INIT(&chan[i].queue_head); } /* daemonize */ if (!foreground) { if (daemonize(pidfile, logfile) < 0) return 1; } signal(SIGTERM, sighandler); openlog("sircbot",0, LOG_DAEMON); while (1) { struct irc_session *s = irc_connect(server, port, nick, pass); char buf[256]; if (s == NULL) { log_err(server); sleep(10); continue; } irc_loop(s, chan, argc); irc_close(s, "bye"); /* reset fifos */ for (i = 0; i < argc; i++) close_fifo(&chan[i], 0); if (sigterm) break; sleep(10); } unlink(pidfile); return 0; }