/* 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 #include #include #include #include "irc.h" #define PROGNAME "sircbot" #if !defined(DEFAULT_PIDFILE_PREFIX) #define DEFAULT_PIDFILE_PREFIX "/var/run/" PROGNAME "/" PROGNAME #endif struct tq_string { char *value; struct tq_string *next; }; struct sircbot_channel { char *name; char *socket_path; int listen_fd; time_t last_data_sent; time_t last_closetime; /* last time we got kicked */ struct tq_string indata_head; /* queue of in-data */ int *fd_array; int fd_array_size; }; struct sircbot_session { struct irc_session *sess; struct sircbot_channel *chan; int numchan; time_t last_ping; int runhooks; int authed; }; struct sircbot_socket_callback { void *context; int (*callback)(struct sircbot_session *sb, struct pollfd *fds, void *ctx); }; 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 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(struct tq_string *head, const char *data) { struct tq_string *n = head; struct tq_string *item = malloc(sizeof(struct tq_string)); if (item == NULL) { log_err("malloc"); return NULL; } memset(item, 0, sizeof(struct tq_string)); if ((item->value = strdup(data)) == NULL) { log_err("strdup"); return NULL; } item->next = NULL; while (n->next != NULL) n = n->next; n->next = item; return item; } int init_channel_socket(struct sircbot_channel *chan) { struct sockaddr_un sun; unlink(chan->socket_path); memset(&sun, 0, sizeof(sun)); sun.sun_family = AF_UNIX; strncpy(sun.sun_path, chan->socket_path, sizeof(sun.sun_path)); chan->listen_fd = socket(AF_UNIX, SOCK_STREAM, 0); if (chan->listen_fd < 0) return -1; fcntl(chan->listen_fd, F_SETFD, FD_CLOEXEC); if (bind(chan->listen_fd, (struct sockaddr *) &sun, sizeof(sun)) < 0) goto err_close; if (listen(chan->listen_fd, 8) < 0) goto err_close; return 0; err_close: log_err(chan->socket_path); close(chan->listen_fd); return -1; } int close_channel_socket(struct sircbot_channel *chan, time_t closetime) { close(chan->listen_fd); chan->listen_fd = -1; chan->last_closetime = closetime; return unlink(chan->socket_path); } int join_channel(struct sircbot_session *sb, char *name) { int i; for (i = 0; i < sb->numchan; i++) { if (strcmp(sb->chan[i].name, name) == 0) return init_channel_socket(&sb->chan[i]); } return 0; } int kick_channel(struct sircbot_session *sb, char *name) { int i; time_t now = time(NULL); printf("got kicked from %s\n", name); for (i = 0; i < sb->numchan; i++) { if (strcmp(sb->chan[i].name, name) == 0) return close_channel_socket(&sb->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 sircbot_session *sb, char *user, char *cmd, char *data) { printf("DEBUG: handling response: user=%s, cmd=%s, data=%s\n", user ? user : "(null)", cmd, data ? data : "(null)"); if (strncmp(cmd, "001", 3) == 0) { sb->authed = 1; printf("DEBUG: authed\n"); } else if (strncmp(cmd, "PING", 4) == 0) { return irc_send(sb->sess, "PONG", data); } else if (strncmp(cmd, "PONG", 4) == 0) { sb->sess->last_pong = time(NULL); } else if (strncmp(cmd, "JOIN", 4) == 0 && user != NULL && strncmp(user, sb->sess->nick, strlen(sb->sess->nick)) == 0) { return join_channel(sb, data); } else if (strncmp(cmd, "KICK", 4) == 0 && data != NULL) { char *p = strchr(data, ' '); if (p) *p = '\0'; return kick_channel(sb, data); } else if (strncmp(cmd, "PRIVMSG", 7) == 0 && data != NULL) { char *p = strchr(data, ' '); if (p) { *p++ = '\0'; if (*p == ':') p++; } if (sb->runhooks) return run_hooks(user, data, p); } return 0; } int parse_line(struct sircbot_session *sb, char *line) { char *user = NULL, *p, *cmd, *data = NULL; if (line == NULL) return -1; 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++; } handle_response(sb, user, cmd, data); return 0; } int parse_irc_data(struct sircbot_session *sb, char *buf) { char *p = buf; while ((p = strsep(&buf, "\n")) != NULL) { char *c = strchr(p, '\r'); if (c != NULL) *c = '\0'; if (*p != '\0') parse_line(sb, p); } return 0; } static int irc_server_disconnect(struct irc_session *is) { syslog(LOG_ERR, "%s: connection closed", is->server); printf("%s: connection closed", is->server); close(is->fd); is->fd = -1; return 0; } /* callback functions */ static int irc_server_cb(struct sircbot_session *sb, struct pollfd *fds, void *ctx) { char buf[4096]; int r; struct irc_session *sess = (struct irc_session *) ctx; if (fds->revents & POLLERR) { log_err(sess->server); return -1; } if (fds->revents & POLLIN) { r = read(fds->fd, buf, sizeof(buf)-1); printf("DEBUG: read %i chars from server\n", r); if (r < 0) return -1; if (r == 0) return irc_server_disconnect(sess); buf[r] = '\0'; if (r) parse_irc_data(sb, buf); } if (fds->revents & POLLHUP) { printf("DEBUG: server hangup\n"); /* server hang up on us */ return irc_server_disconnect(sess); } return 1; } int channel_extend_fd_array(struct sircbot_channel *chan) { int i; int oldsize = chan->fd_array_size; chan->fd_array = realloc(chan->fd_array, (oldsize + 8) * sizeof(int)); if (chan->fd_array == NULL) return -1; chan->fd_array_size += 8; for (i = oldsize; i < chan->fd_array_size; i++) chan->fd_array[i] = -1; return 0; } void channel_add_connection(struct sircbot_channel *chan, int fd) { int i; for (i = 0; i < chan->fd_array_size; i++) { if (chan->fd_array[i] == -1) break; } if (i >= chan->fd_array_size) if (channel_extend_fd_array(chan) < 0) return; chan->fd_array[i] = fd; printf("DEBUG: new connection (fd=%i) for %s\n", fd, chan->name); } static int channel_del_connection(struct sircbot_channel *chan, int fd) { int i; for (i = 0; i < chan->fd_array_size; i++) if (chan->fd_array[i] == fd) { close(fd); chan->fd_array[i] = -1; printf("DEBUG: close connection %i for %s\n", fd, chan->name); return 1; } return 0; } static int channel_listener_cb(struct sircbot_session *sb, struct pollfd *fds, void *ctx) { int fd = 0; struct sircbot_channel *chan = ctx; if (fds->revents & POLLERR) { log_err(chan->socket_path); return -1; } fd = accept(chan->listen_fd, NULL, NULL); if (fd < 0) { log_err(chan->socket_path); return -1; } channel_add_connection(chan, fd); return 1; } static int channel_conn_cb(struct sircbot_session *sb, struct pollfd *fds, void *ctx) { int r; char buf[4096]; char *p, *n; struct sircbot_channel *chan = ctx; if (fds->revents & (POLLERR | POLLNVAL)) return channel_del_connection(chan, fds->fd); if (fds->revents & POLLIN) { r = read(fds->fd, buf, sizeof(buf)-1); if (r < 0) { log_err(chan->socket_path); return -1; } /* eof */ if (r == 0) return channel_del_connection(chan, fds->fd); buf[r] = '\0'; p = buf; while ((n = strchr(p, '\n')) != NULL) { *n = '\0'; tq_stringlist_add(&chan->indata_head, p); p = n + 1; } if (*p) tq_stringlist_add(&chan->indata_head, p); } if (fds->revents & POLLHUP) return channel_del_connection(chan, fds->fd); return 1; } /* init pollfd strucs */ static int irc_reset_pollfds(struct sircbot_session *sb, struct pollfd *fds, struct sircbot_socket_callback *cb, int maxfds) { int i, j, n = 0; /* first pollfd struc is the irc session */ fds[n].fd = sb->sess->fd; fds[n].events = POLLIN; fds[n].revents = 0; cb[n].context = sb->sess; cb[n].callback = &irc_server_cb; n++; /* channel socket listeners */ for (i = 0; i < sb->numchan && n < maxfds; i++) { if (sb->chan[i].listen_fd < 0) continue; fds[n].fd = sb->chan[i].listen_fd; fds[n].events = POLLIN; fds[n].revents = 0; cb[n].callback = &channel_listener_cb; cb[n].context = &sb->chan[i]; n++; /* open channel connections */ for (j = 0; j < sb->chan[i].fd_array_size && n < maxfds; j++) { if (sb->chan[i].fd_array[j] == -1) continue; fds[n].fd = sb->chan[i].fd_array[j]; fds[n].events = POLLIN; fds[n].revents = 0; cb[n].callback = &channel_conn_cb; cb[n].context = &sb->chan[i]; n++; } } return n; } static int send_fifo_queue(struct irc_session *sess, struct sircbot_channel *chan, time_t now) { int r; struct tq_string *item; if (chan->indata_head.next == NULL || (now - chan->last_data_sent) < flush_rate) return 0; /* nothing in queue, or too early to send */ item = chan->indata_head.next; r = irc_send_chan(sess, chan->name, item->value); chan->last_data_sent = now; chan->indata_head.next = item->next; free(item->value); free(item); return r; } static int join_channels(struct sircbot_session *sb) { time_t now = time(NULL); int i; /* wait atleast 5 secs before we join a channel */ for (i = 0; i < sb->numchan; i++) if ((now - sb->chan[i].last_closetime) > 5 && sb->chan[i].listen_fd < 0 && sb->sess != NULL) { int r = 0; printf("DEBUG: joining %s\n", sb->chan[i].name); sb->chan[i].last_closetime = now; r = irc_send(sb->sess, "JOIN", sb->chan[i].name); if (r < 0) { printf("DEBUG: error %s: %s\n", sb->sess->server, strerror(r)); return r; } } return 0; } static int irc_loop(struct sircbot_session *sb) { int i, r, n; const int maxconn = sb->numchan * 128; const int maxfds = 1 + sb->numchan + maxconn; struct pollfd fds[maxfds]; struct sircbot_socket_callback cbs[maxfds]; sigset_t sigmask; struct timespec tv; time_t now; sigemptyset(&sigmask); sigaddset(&sigmask, SIGTERM); tv.tv_sec = 1; tv.tv_nsec = 0; while (!sigterm) { n = irc_reset_pollfds(sb, fds, cbs, maxfds); r = ppoll(fds, n, &tv, &sigmask); if (r < 0) { log_err("ppoll"); return -1; } now = time(NULL); /* send a ping every 2 min */ if ((now - sb->last_ping) > 120) { irc_send_ping(sb->sess); sb->last_ping = now; } /* send data in the channel queues */ for (i = 0; i < sb->numchan; i++) if (send_fifo_queue(sb->sess, &sb->chan[i], now) < 0) goto ret_err; for (i = 0; i < n; i++) { if (fds[i].revents == 0) continue; r = cbs[i].callback(sb, &fds[i], cbs[i].context); if (r <= 0) return r; } if (sb->authed && join_channels(sb) < 0) goto ret_err; } return 0; ret_err: log_err(sb->sess->server); return -1; } void sighandler(int signal) { switch (signal) { case SIGTERM: sigterm = 1; break; } } static void usage_exit(int exitcode) { printf("sircbot " VERSION "\n" "usage: sircbot [-fmN] [-n nick] [-r flushrate] [-s server] [-p port]\n" " [-l logfile] [-u user] [-P pass] CHANNEL\n"); exit(exitcode); } int main(int argc, char *argv[]) { char pidfile_suffix[64] = ".pid"; static char pidfile[PATH_MAX] = ""; const char *server = "irc.freenode.org"; const char *nick = "sircbot"; const char *username = "sircbot"; const char *pass = NULL; const char *logfile = "/dev/null"; struct sircbot_session sb; int i, c, port = 6667, multimode = 0; sb.runhooks = 1; while ((c = getopt(argc, argv, "fl:mn:Np:P:r:s:u:")) != -1) { switch (c) { case 'f': foreground = 1; break; case 'l': logfile = optarg; break; case 'm': multimode = 1; break; case 'n': nick = optarg; break; case 'N': sb.runhooks = 0; break; case 'P': pass = optarg; break; case 'p': port = atoi(optarg); break; case 'r': flush_rate = atoi(optarg); break; case 's': server = optarg; break; case 'u': username = optarg; break; default: usage_exit(1); } } argv += optind; argc -= optind; if (argc <= 0) usage_exit(1); /* init channel strucs */ sb.numchan = argc; sb.chan = malloc(argc * sizeof(struct sircbot_channel)); if (sb.chan == NULL) err(1, "malloc"); for (i = 0; i < argc; i++) { sb.chan[i].listen_fd = -1; sb.chan[i].name = strdup(argv[i]); sb.chan[i].last_closetime = 0; if (multimode) { asprintf(&sb.chan[i].socket_path, "/var/run/sircbot/%s-%s", argv[i], nick); } else { asprintf(&sb.chan[i].socket_path, "/var/run/sircbot/%s", argv[i]); } unlink(sb.chan[i].socket_path); sb.chan[i].indata_head.next = NULL; sb.chan[i].fd_array = NULL; sb.chan[i].fd_array_size = 0; } if (multimode) snprintf(pidfile_suffix, sizeof(pidfile_suffix), "-%s.pid", nick); snprintf(pidfile, sizeof(pidfile_suffix), "%s%s", DEFAULT_PIDFILE_PREFIX, pidfile_suffix); /* daemonize */ if (!foreground) { if (daemonize(pidfile, logfile) < 0) return 1; } signal(SIGTERM, sighandler); openlog("sircbot",0, LOG_DAEMON); while (1) { sb.authed = 0; sb.sess = irc_connect(server, port, nick, username, pass); if (sb.sess == NULL) { log_err(server); sleep(10); continue; } irc_loop(&sb); irc_close(sb.sess, "bye"); /* reset channel sockets */ for (i = 0; i < argc; i++) close_channel_socket(&sb.chan[i], 0); if (sigterm) break; sleep(10); } unlink(pidfile); return 0; }