www.gnu.org/software/src-highlite
www.lorenzobettini.it
www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd
/*
* name: ircbot
* date: 08-12-2003
* version: 0.1
* license: public domain
* website: http://www.xs4all.nl/~mechiel/projects/ircbot/
* author: mechiel lukkien
* e-mail: mechiel@xs4all.nl or mechiel@ueber.net
* compile: cc -g -Wall -pthread -o ircbot ircbot.c
* xlint: lint -aabchruH -lposix ircbot.c
* usage: ./ircbot nick host port listenhost listenport nickfile >> logfile
* see also: rfc2810, rfc2811, rfc2812 and rfc2813
* tested on: OpenBSD 3.3 (extensively), FreeBSD 4.7 and some Linux
*
*
* Note that you also need a program/script `botcmd' in the same directory as
* in which you executed ircbot.
*
* This ircbot is very simplistic: For example, it will not reconnect when its
* connection disappears, and keeps only a minimal of state (e.g. it does know
* it own nick, but it doesn't know which channels it joined). There is
* virtually nothing to configure, but you can always modify the C code.
*
* When you start an instance of ircbot, it connects to host:port (as given on
* the command line) and logs in as nick. If nick is already in use, probably
* nothing useful will happen. If all goes well the bot will just stay
* connected, replying to ping messages, but not joining channels or doing
* anything on its own. Ircbot also reads nickfile (also specified on the
* command line) and keeps it in memory. The file contains nicks that are
* allowed to execute commands, one on each line. All communications are
* printed to stdout, you may want to log it. Warnings, diagnostics and errors
* are printed to stderr. Ircbot also listens on the TCP port
* listenhost:listenport, a connection to this port is called a control
* connection, since by this means you control ircbot. One can send the
* following commands over the control connection (one command per line,
* trailing \r\n or \n is stripped):
*
* - `#channel howdy folks!' sends `howdy folks!' to #channel
* - `%user howdy user!' sends `howdy user!' to user
* - `;raw-irc-command' sends raw-irc-command to ircd. e.g. `;JOIN #channel'.
* - `flush' will clear the queue for outgoing irc messages
* (except important messages like ping replies)
* - `readnicks' causes nickfile to be reread
*
* You can also have ircbot execute commands by sending commands over irc.
* Lines starting with a semicolon (;) are commands (most bots are triggered by
* !, so yes, this is different). Commands are accepted both in channels and
* in private messages (of course, ircbot checks whether the nick that send the
* command is allowed to execute commands). Ircbot has no builtin commands.
* For every command, ircbot forks, sets a number of environment variables (see
* below), and executes ./botcmd (so botcmd must be present in the directory
* ircbot was started) with the command as its only argument (but without the
* leading semicolon). In principle, a botcmd can do whatever it wants, but
* the delivered botcmd script (a shell script for the rc shell
* (http://www.star.le.ac.uk/~tjg/rc/) will execute the command and send the
* output to the channel or user by means of a control connection.
*
* Environment variables set before executing ./botcmd (and thus available in
* botcmd and whatever processes botcmd is creating):
* - target irc target to send the reply (e.g. #channel or user (actually,
* the default botcmd sets target to %$target if it doesn't start
* with a #. since the control connection needs a % prepended to
* send data to a user))
* - nick nick of the bot
* - nickfile file containing nicks allowed to execute commands
* - fromuser user sending the command
* - bothost host the bot listens on
* - botport port the bot listens on, on bothost
* - botpid pid of the bot
* Also, when a command was given in a channel, fromchannel is set to that
* channel.
*
* Hopefully, enough information has been provided to get you started with this
* program.
*
*
* Now for some comments on the implementation. Ircbot is heavily
* multithreaded, featuring the following threads:
*
* - ircreader, reading irc messages from the ircd
* - ircwriter, writing irc messages to the ircd
* - tcpacceptor, accepting incoming control connections
* - tcpreader, one thread for each established control connection
* - handlequeue, the main program that coordinates the data flow between the threads
*
* The main program/thread is the central part of ircbot. It reads events from
* an `inqueue', processes them, and possibly puts lines to be written to the
* ircd in an `outqueue'.
*
* - Ircreader simply reads irc messages (read a line and parse it), and puts it
* into the event queue.
* - Tcpacceptor does nothing with the queue, it only starts a tcpreader thread
* for each incoming control connection.
* - Tcpreader reads and parses commands from the control channel and puts them
* into the event queue.
* - Handlequeue reads an event, processes it and waits for the next event. It
* will never block (except when waiting for incoming events of course).
* Processing sometimes includes putting an irc message in the outqueue to be
* read by ircwriter.
* - Ircwriter reads lines from the outqueue and writes them to the ircd. It
* makes sure to not flood the ircd because that wouldn't be nice. This is
* done by simply keeping at least NOFLOOD_DELAY miliseconds between every
* two messages.
*
* Synchronisation is done by the queue functions using a mutex (to make sure a
* thread has exclusive access to the queue for a short period) and a non-empty
* condition (to make sure threads are woken up as soon as the queue is
* non-empty).
* http://www.llnl.gov/computing/tutorials/workshops/workshop/pthreads/MAIN.html
* has some nice examples.
*
* Possible future improvements:
* - Currently, the control command `flush' removes everything from the queue
* (except ping replies of course). It would be nice to only flush output to
* the channel/user by which the flush was executed.
* - Thread-safety of standard library functions could be an issue. I have not
* tried really hard to make sure it won't cause problems.
* - Make the exit code indicate if the error was fatal or non-fatal. With
* non-fatal meaning we can try to reconnect.
* - Find some mechanism to kill off old processes that somehow went wrong
* (e.g. when someone starts something like vim).
*/
#define _GNU_SOURCE /* to make it compile without warning on glibc systems */
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/socket.h>
#include <sys/time.h>
#include <sys/resource.h>
#include <assert.h>
#include <err.h>
#include <errno.h>
#include <fcntl.h>
#include <netdb.h>
#include <pthread.h>
#include <pwd.h>
#include <signal.h>
#include <stdarg.h>
#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <unistd.h>
#define nil NULL
#define strequiv(s1, s2) (strcasecmp(s1, s2) == 0)
#define streql(s1, s2) (strcmp(s1, s2) == 0)
#define IRCBOT_VERSION "ircbot, version 0.1, http://www.xs4all.nl/~mechiel/projects/ircbot/"
enum
{
IRCMSG_MAXLEN = 512, /* maximum length of an irc (protocol) message */
BBUFFERSIZE = IRCMSG_MAXLEN+1, /* size of the buffer for incoming irc data */
SOCK_MAX = 32, /* maximum number of sockets to listen on */
NICK_MAXLEN = 128, /* `should be more than enough' */
NOFLOOD_DELAY = 700 /* milliseconds between the sending of two irc messages */
};
/* types of the data send to the event processing main loop */
enum
{
INTYPEircmsg, /* irc message from ircd */
INTYPEraw, /* raw message to be send to the ircd */
INTYPEbuiltin /* builtin command */
};
/* type of the data send to the thread that sends irc messages to the ircd (without flooding) */
enum
{
OUTTYPEimmediate, /* accompanied data should be send immediately (e.g. for PING) */
OUTTYPEnormal /* normal data */
};
typedef unsigned long long uvlong;
typedef long long vlong;
typedef struct Source Source;
typedef struct Msg Msg;
typedef struct Elem Elem;
typedef struct Queue Queue;
typedef struct Nick Nick;
typedef struct Nicks Nicks;
typedef struct Buf Buf;
/* irc source of the message */
struct Source {
char *nick, *server; /* when server is non-nil, nick will contain the same value as server */
char *user; /* user an irc message-line originated from, may be nil */
char *host; /* host the user is on, may be nil */
};
/* irc messages as received from the ircd */
struct Msg {
Source *src; /* source of the message, may be nil */
char *cmd; /* irc command, always non-nil */
char **params; /* parameters for the command, may be nil */
int nparams; /* number of parameters */
};
/* element of the event processing queues */
struct Elem {
void *data; /* data part of the element */
int type; /* type of data (INTYPE* for incoming, OUTTYPE* for outgoing) */
Elem *next; /* next element in the list, may be nil for the last element */
};
/* event processing queue */
struct Queue {
Elem *first; /* head of the queue, may be nil */
pthread_mutex_t mutex; /* to control access to the queue */
pthread_cond_t nonempty; /* signalled when something is put in the queue */
};
/* nick with permission to let the bot execute commands */
struct Nick {
char *name;
Nick *next;
};
/* list of nicks with permission */
struct Nicks {
Nick *first;
};
/* buffer with incoming data from the ircd */
struct Buf {
char s[BBUFFERSIZE]; /* data buffer */
int slen; /* length of data in the buffer `s' */
int fd; /* file descriptor to read from when `s' is empty */
int eof; /* whether or not we have seen eof on `fd' */
};
void handlequeue(char *, const char *, const char *, const char *, Nicks *); /* main event processing loop */
void *Fircreader(void *); /* thread: reads and parses lines from ircd, then puts them in incoming queue */
void *Fircwriter(void *); /* thread: writes lines in outgoing queue to ircd */
void *Ftcpacceptor(void *); /* thread: accepts incoming tcp control connections (and creates an Ftcpreader for it) */
void *Ftcpreader(void *); /* thread: reads data from tcp control connection, data is put in the outgoing queue */
int tcpopen(const char *, const char *); /* opens connection to ircd */
void tcpbind(const char *, int []); /* binds to localhost tcp ports for accepting incoming control connections */
Msg *msg_parse(char *); /* parses a line received from the irc connection, Msg is allocated */
Msg *msg_new(void); /* allocates and initializes a Msg */
void msg_free(Msg *); /* frees any memory allocated in Msg */
Source *source_parse(char *); /* parse the source of the message (`prefix' in irc terms) */
Queue *qnew(void); /* allocate and initialize a queue */
void qappend(Queue *, int, void *); /* append message to the queue */
void qprepend(Queue *, int, void *); /* prepend message to the queue */
void *qremove(Queue *, int *); /* retrieve and remove first element from queue */
void qappendf(Queue *, int, const char *, ...); /* append printf-like string to the queue */
void qprependf(Queue *, int, const char *, ...); /* same as qappendf but prepend */
void qflushout(void); /* flushes all non-important lines in outgoing queue */
Nicks *nnew(const char *); /* initialize nicks from the specified file */
void nfree(Nicks *); /* free Nicks */
void nadd(Nicks *, char *); /* add a nick */
void nremove(Nicks *, char *); /* remove the nick */
int nhas(Nicks *, const char *); /* check if the nick is present (allowed) */
void nprint(Nicks *); /* print a list of nicks to stderr, for debugging */
void nsave(Nicks *, const char *); /* save the nicks to the specified file */
Buf *bnew(int); /* create new buffer for reading incoming data from ircd */
int breadln(Buf *, char *, int); /* read a line (an irc message) from the buffer, keeps the trailing newline */
void *xmalloc(size_t s) { void *p; p = malloc(s); if (p == nil) err(2, "xmalloc"); return p; }
void *xrealloc(void *p, size_t s) { p = realloc(p, s); if (p == nil) err(2, "xrealloc"); return p; }
char *xstrdup(char *str) { char *p; p = malloc(strlen(str) + 1); if (p == nil) err(2, "xstrdup"); strcpy(p, str); return p; }
/*
* these are the only global variables (accessed by more than one thread),
* these are protected by a pthread mutex and condition.
*/
Queue *inq;
Queue *outq;
int
main(int argc, char *argv[])
{
char *listenhost, *listenport;
pthread_t Tircreader, Tircwriter, Ttcpacceptor[SOCK_MAX];
char *host, *port;
char *nickfile;
struct passwd *pw;
int ircfd;
int bindfds[SOCK_MAX+1];
int i;
char *nick;
char *userinfo;
Nicks *nicks;
/* to make it accept `--' and print warnings for options like it should */
if (getopt(argc, argv, "") != -1) {
fprintf(stderr, "usage: bot nick host port listenhost listenport nickfile\n");
exit(1);
}
argc -= optind;
argv += optind;
if (argc != 6) {
warnx("invalid number of arguments");
fprintf(stderr, "usage: bot nick host port listenhost listenport nickfile\n");
exit(1);
}
/*
* a copy of nick is needed because the nick may change, then the
* current nick is freed and a new one allocated
*/
nick = xstrdup(argv[0]);
host = argv[1];
port = argv[2];
listenhost = argv[3];
listenport = argv[4];
nickfile = argv[5];
/* find username and info to present to ircd */
pw = getpwnam(getlogin());
if (pw == nil)
errx(1, "could not find passwd entry for %s", getlogin());
/* read nicks that are allowed to execute commands */
nicks = nnew(nickfile);
/*
* allocate and initialize event queues. one for incoming commands
* (from ircd or listenhost:listenport) and one for messages to be send
* to the ircd.
*/
inq = qnew();
outq = qnew();
/* commands to be executed need a clean environment */
if (fcntl(STDIN_FILENO, F_SETFD, 1) == -1 ||
fcntl(STDOUT_FILENO, F_SETFD, 1) == -1 ||
fcntl(STDERR_FILENO, F_SETFD, 1) == -1)
err(1, "setting close-on-exec for stdin, stdout and stderr");
/*
* connect to the ircd. start listening for incoming commands. on
* error, exit is called
*/
ircfd = tcpopen(host, port);
tcpbind(listenport, bindfds);
/* create the threads */
pthread_create(&Tircreader, nil, Fircreader, &ircfd);
pthread_create(&Tircwriter, nil, Fircwriter, &ircfd);
for (i = 0; bindfds[i] != -1; ++i)
pthread_create(&Ttcpacceptor[i], nil, Ftcpacceptor, &bindfds[i]);
/* send the login commands to get the connection started */
userinfo = pw->pw_gecos;
if (streql(userinfo, ""))
userinfo = "none";
qappendf(outq, OUTTYPEimmediate, "NICK %s\r\n", nick);
qappendf(outq, OUTTYPEimmediate, "USER %s 0 * :%s\r\n", pw->pw_name, userinfo);
/* into message handling loop, this receives incoming matches and dispatches outgoing ones */
handlequeue(nick, listenhost, listenport, nickfile, nicks);
exit(0); /* not reached */
}
void
handlequeue(char *nick, const char *listenhost, const char *listenport, const char *nickfile, Nicks *nicks)
{
void *data;
int type;
Msg *msg;
char botpidbuf[64];
char *target;
char *param;
assert(snprintf(botpidbuf, sizeof botpidbuf, "%llu", (uvlong)getpid()) != -1);
for (;;) {
/*
* this call blocks (properly with mutexes and non-empty-queue
* condition) until something needs to be processed
*/
data = qremove(inq, &type);
/* cleanup possible child processes, doesn't block */
for (;;) {
int r;
int status;
r = wait3(&status, WNOHANG, nil);
if (r == 0 || (r == -1 && errno == ECHILD))
break; /* no children at all (0) or no exited children (-1) */
if (r == -1) {
warn("wait3"); /* error in wait3 */
break;
}
}
errno = 0;
/* handle the data retrieved from the queue, based on the type of the data */
switch (type) {
case INTYPEircmsg: /* incoming irc message from ircd */
msg = (Msg *)data;
/*
* ping message from ircd
*
* <<< PING :irc.snt.utwente.nl
* >>> PONG brein irc.snt.utwente.nl
*/
if (strequiv(msg->cmd, "PING") && msg->nparams >= 1)
qprependf(outq, OUTTYPEimmediate, "PONG %s %s\r\n", nick, msg->params[0]);
/*
* handle messages telling our nick changed
*
* >>> NICK newbrein
* <<< :brein!plan@tunnel4040.ipv6.xs4all.nl NICK :newbrein
*/
if (strequiv(msg->cmd, "NICK") && msg->nparams == 1 &&
msg->src != nil && msg->src->nick != nil && strequiv(msg->src->nick, nick)) {
free(nick);
nick = xstrdup(msg->params[0]);
}
/* private messages from nicks and with an argument may contain a command or a ctcp version */
if (msg->src != nil && msg->src->nick != nil && strequiv(msg->cmd, "PRIVMSG") && msg->nparams >= 2) {
param = msg->params[1];
/*
* only accept commands from known people (both in a channel and in private)
* skip some common smileys
*
* <<< :Oksel!mechiel@tunnel4040.ipv6.xs4all.nl PRIVMSG #deadbeef :;help
* or
* <<< :Oksel!mechiel@tunnel4040.ipv6.xs4all.nl PRIVMSG brein :;wn whatsoever
*/
if (nhas(nicks, msg->src->nick) && param[0] == ';' &&
!(streql(param, ";)") || streql(param, ";-)") ||
streql(param, ";(") || streql(param, ";-("))) {
target = msg->params[0];
if (strequiv(msg->params[0], nick)) /* private message to bot, send response to nick, not channel */
target = msg->src->nick;
switch (fork()) {
case 0:
setenv("target", target, 1);
setenv("nick", nick, 1);
setenv("nickfile", nickfile, 1);
setenv("fromuser", msg->src->nick, 1);
if (target[0] == '#')
setenv("fromchannel", target, 1);
setenv("bothost", listenhost, 1);
setenv("botport", listenport, 1);
setenv("botpid", botpidbuf, 1);
/* note: all fd's have close-on-exec set */
execlp("./botcmd", "./botcmd", param + 1, (char *)nil);
warn("execlp ./botcmd");
break;
case -1:
warn("forking for execlp");
break;
}
} else if (strequiv(param, "\001VERSION\001")) {
/* handle a ctcp version request */
qappendf(outq, OUTTYPEnormal, "NOTICE %s :\001VERSION %s\001\r\n", msg->src->nick, IRCBOT_VERSION);
}
}
msg_free(msg); msg = nil;
break;
case INTYPEraw: /* raw command to send to ircd */
qappendf(outq, OUTTYPEnormal, "%s\r\n", (char *)data);
free(data); data = nil;
break;
case INTYPEbuiltin: /* built in commands */
if (streql("flush", (char *)data)) {
qflushout();
} else if (streql("readnicks", (char *)data)) {
nfree(nicks);
nicks = nnew(nickfile);
} else {
warnx("unexpected builtin command read in handlequeue: %s", (char *)data);
}
free(data); data = nil;
break;
default:
warnx("unknown message received in main, type=%d", type);
break;
}
}
}
void *
Fircreader(void *ircfdp)
{
char buf[IRCMSG_MAXLEN+1];
int count;
Buf *bin;
Msg *msg;
struct timeval now;
uvlong msecs;
bin = bnew(*(int *)ircfdp);
for (;;) {
/* read a message line from the ircd (including \r\n if present) */
count = breadln(bin, buf, sizeof buf);
if (count == 0)
errx(1, "eof from ircd");
if (count == -1)
err(1, "reading from ircd");
gettimeofday(&now, nil);
msecs = (uvlong)now.tv_sec*1000 + (uvlong)now.tv_usec/1000;
printf("<<< %llu %s", msecs, buf);
fflush(stdout);
msg = msg_parse(buf);
if (msg == nil)
continue; /* message has been printed by msg_parse */
/* insert the irc message into the event queue */
if (strequiv(msg->cmd, "PING"))
qprepend(inq, INTYPEircmsg, msg);
else
qappend(inq, INTYPEircmsg, msg);
}
}
void *
Fircwriter(void *ircfdp)
{
char *line;
int type;
uvlong msecs;
int length, count;
struct timeval lastwrite = {0, 0};
struct timeval now;
for (;;) {
line = qremove(outq, &type);
if (type == OUTTYPEimmediate) {
length = strlen(line);
count = write(*(int *)ircfdp, line, length);
if (count != length)
warnx("ircwriter: line not written entirely, length=%d count=%d", length, count);
gettimeofday(&lastwrite, nil);
} else {
if (type != OUTTYPEnormal)
warnx("ircwriter: unknown message, type=%d, writing as if type=OUTTYPEnormal", type);
gettimeofday(&now, nil);
msecs = (now.tv_sec-lastwrite.tv_sec)*1000 + (now.tv_usec-lastwrite.tv_usec)/1000;
if (msecs < NOFLOOD_DELAY)
usleep((NOFLOOD_DELAY-msecs) * 1000);
length = strlen(line);
count = write(*(int *)ircfdp, line, length);
if (count != length)
warnx("ircwriter: line not written entirely, length=%d count=%d", length, count);
gettimeofday(&lastwrite, nil);
}
msecs = (uvlong)lastwrite.tv_sec*1000 + (uvlong)lastwrite.tv_usec/1000;
printf(">>> %llu %s", msecs, line);
fflush(stdout);
free(line);
}
}
void *
Ftcpacceptor(void *bindfdp)
{
int tcpfd;
struct sockaddr sa;
socklen_t salen;
pthread_t *Ttcpreader;
for (;;) {
tcpfd = accept(*(int *)bindfdp, (struct sockaddr *)&sa, &salen);
if (tcpfd < 0) {
if (errno == EINTR)
continue;
err(1, "accept");
}
if (fcntl(tcpfd, F_SETFD, 1) == -1) {
warn("setting close-on-exec flag for incoming connection");
close(tcpfd);
continue;
}
Ttcpreader = xmalloc(sizeof Ttcpreader[0]);
pthread_create(Ttcpreader, nil, Ftcpreader, &tcpfd);
}
}
void *
Ftcpreader(void *tcpfdp)
{
char buf[BBUFFERSIZE];
int count;
Buf *bin;
char *cp;
char *errstr;
/*
* bnew and breadln were actually meant for reading irc messages with a
* maximum length of 512 characters. the tcp connection has this limit
* too now, no problem though.
*/
bin = bnew(*(int *)tcpfdp);
for (;;) {
/* read a message line from the client (including \n if present) */
count = breadln(bin, buf, sizeof buf);
if (count == 0) {
close(*(int *)tcpfdp);
pthread_exit(nil);
}
if (count == -1) {
warn("tcpreader: error reading from client");
close(*(int *)tcpfdp);
pthread_exit(nil);
}
if (buf[strlen(buf) - 1] == '\n') {
buf[strlen(buf) - 1] = '\0';
if (buf[strlen(buf) - 1] == '\r') /* telnet(1) appends an \r, remove it */
buf[strlen(buf) - 1] = '\0';
}
if (streql(buf, "flush") || streql(buf, "readnicks")) {
qprepend(inq, INTYPEbuiltin, xstrdup(buf));
} else if (buf[0] == ';') {
qappend(inq, INTYPEraw, xstrdup(buf+1));
} else if (buf[0] == '%') {
cp = strchr(buf, ' ');
if (cp == nil || cp == buf+1) {
errstr = "no message to send to nick or empty nick\n";
break;
} else {
*cp = '\0';
qappendf(inq, INTYPEraw, "PRIVMSG %s :%s", buf+1, cp+1);
}
} else if (buf[0] == '#') {
cp = strchr(buf, ' ');
if (cp == nil || cp == buf+1) {
errstr = "no message to send to channel or empty channel\n";
break;
} else {
*cp = '\0';
qappendf(inq, INTYPEraw, "PRIVMSG %s :%s", buf, cp+1);
}
} else {
errstr = "no nick or channel specified\n";
break;
}
}
if (write(*(int *)tcpfdp, errstr, strlen(errstr)) != strlen(errstr))
warnx("tcpreader: error string not written entirely to connection");
close(*(int *)tcpfdp);
pthread_exit(nil);
}
int
tcpopen(const char *host, const char *port)
{
int gaierrno;
struct addrinfo hints = { 0, PF_UNSPEC, SOCK_STREAM, 0, 0, nil, nil, nil };
struct addrinfo *res0, *res;
char *cause;
int save_errno;
int fd;
gaierrno = getaddrinfo(host, port, &hints, &res0);
if (gaierrno)
errx(1, "getaddrinfo: %s", gai_strerror(gaierrno));
fd = -1;
for (res = res0; res != nil; res = res->ai_next) {
fd = socket(res->ai_family, res->ai_socktype, res->ai_protocol);
if (fd < 0) {
fd = -1;
cause = "socket";
continue;
}
if (connect(fd, res->ai_addr, res->ai_addrlen) < 0) {
save_errno = errno;
(void)close(fd);
errno = save_errno;
fd = -1;
cause = "connect";
continue;
}
break;
}
freeaddrinfo(res0);
if (fd == -1)
err(1, "connecting to %s:%s: %s", host, port, cause);
if (fcntl(fd, F_SETFD, 1) == -1)
err(1, "setting close-on-exec flag for irc connection");
return fd;
}
void
tcpbind(const char *port, int bindfds[SOCK_MAX+1])
{
struct addrinfo hints = { AI_PASSIVE, PF_UNSPEC, SOCK_STREAM, 0, 0, nil, nil, nil };
struct addrinfo *res, *res0;
int gaierrno, save_errno;
int fd, nsock;
char *cause;
gaierrno = getaddrinfo(nil, port, &hints, &res0);
if (gaierrno)
errx(1, "getaddrinfo: %s", gai_strerror(gaierrno));
cause = nil;
nsock = 0;
for (res = res0; res != nil && nsock < SOCK_MAX; res = res->ai_next) {
fd = socket(res->ai_family, res->ai_socktype, res->ai_protocol);
if (fd < 0) {
cause = "socket";
continue;
}
if (bind(fd, res->ai_addr, res->ai_addrlen) < 0 || fcntl(fd, F_SETFD, 1) == -1) {
cause = "bind or fcntl";
save_errno = errno;
close(fd);
errno = save_errno;
continue;
}
(void)listen(fd, 5);
bindfds[nsock++] = fd;
bindfds[nsock] = -1;
}
if (nsock == 0)
err(1, cause);
freeaddrinfo(res0);
}
/*
* see rfc2812 for a detailed description of irc messages
*
* simply put, an irc message looks like this:
*
* message = (:$prefix)? $command ($param)* crlf
* prefix = servername | nickname ((!user)?@host)?
* command = string | three-digit-number
* param = string
*
* a parameter starting with a colon is special. the parameter ends at
* the end of the line, i.e. does not end with the next space.
*/
Msg *
msg_parse(char *str)
{
int stop;
char *cp;
Msg *msg;
assert(str != nil);
msg = msg_new();
if (str[0] == ':') {
++str;
cp = strchr(str, ' ');
if (cp == nil) {
warnx("parsing: message contains prefix but no space (and thus no command)");
goto error;
}
*cp = '\0';
msg->src = source_parse(str);
if (msg->src == nil)
goto error; /* diagnostics printed by source_parse */
str = cp+1;
}
stop = 0;
cp = strchr(str, ' ');
if (cp == nil) {
cp = strchr(str, '\r');
if (cp == nil) {
warnx("parsing: message doesn't contain carriage return (for end) or space (for parameter delimiter) after command");
goto error;
}
stop = 1;
}
*cp = '\0';
msg->cmd = xstrdup(str);
str = cp+1;
while (!stop) {
if (str[0] == ':') {
++str;
stop = 1;
}
/* when we saw a colon and thus `stop' is set, don't
* look for a space but for the closing carriage return */
cp = strchr(str, stop ? '\r' : ' ');
if (cp == nil) {
/* the last argument is not required to have a colon */
cp = strchr(str, '\r');
if (cp == nil) {
warnx("parsing: no carriage return after arguments");
goto error;
}
stop = 1;
}
*cp = '\0';
msg->params = xrealloc(msg->params, sizeof msg->params[0] * (msg->nparams+1));
msg->params[msg->nparams++] = xstrdup(str);
str = cp+1;
}
/* debug code... */
if (0) {
int i;
if (msg->src != nil) {
if (msg->src->host != nil) {
fprintf(stderr, "src->nick=%s\n", msg->src->nick);
if (msg->src->user != nil)
fprintf(stderr, "src->user=%s\n", msg->src->user);
else
fprintf(stderr, "src->user=(nil)\n");
fprintf(stderr, "src->host=%s\n", msg->src->host);
} else {
fprintf(stderr, "nick=server=%s\n", msg->src->nick);
}
} else {
fprintf(stderr, "src=(nil)\n");
}
fprintf(stderr, "cmd=%s\n", msg->cmd);
fprintf(stderr, "nparams=%d\n", msg->nparams);
for (i = 0; i < msg->nparams; ++i)
fprintf(stderr, "params[%d]=%s\n", i, msg->params[i]);
}
return (Msg *)msg;
error:
msg_free((Msg *)msg); msg = nil;
return nil;
}
Source *
source_parse(char *str)
{
Source *source;
char *cp;
assert(str != nil);
source = xmalloc(sizeof source[0]);
source->nick = nil;
source->server = nil;
source->user = nil;
source->host = nil;
cp = strchr(str, '@');
if (cp == nil) {
/* the source is either a servername or a nick without user/host part */
source->nick = source->server = xstrdup(str);
return source;
}
source->host = xstrdup(cp+1);
*cp = '\0'; /* make location of @ end of string, thus str only contains nick with optionally !user */
cp = strchr(str, '!');
if (cp == nil) {
source->nick = xstrdup(str);
return source;
}
source->user = xstrdup(cp+1);
*cp = '\0'; /* make location of ! end of string, thus str only contains nick */
source->nick = xstrdup(str);
return source;
}
Msg *
msg_new(void)
{
Msg *msg;
msg = xmalloc(sizeof msg[0]);
msg->src = nil;
msg->cmd = nil;
msg->params = nil;
msg->nparams= 0;
return msg;
}
void
msg_free(Msg *msg)
{
int i;
if (msg == nil)
return;
for (i = 0; i < msg->nparams; ++i)
free(msg->params[i]);
free(msg->params);
free(msg->cmd);
if (msg->src != nil) {
free(msg->src->nick);
msg->src->server = nil; /* server is always same as nick or nil */
free(msg->src->user);
free(msg->src->host);
free(msg->src);
}
free(msg);
}
Queue *
qnew(void)
{
Queue *new;
new = xmalloc(sizeof new[0]);
new->first = nil;
pthread_mutex_init(&new->mutex, nil);
pthread_cond_init(&new->nonempty, nil);
return new;
}
void
qappend(Queue *q, int type, void *data)
{
Elem *new;
Elem *oldlast;
assert(q != nil);
assert(data != nil);
new = xmalloc(sizeof new[0]);
new->data = data;
new->type = type;
new->next = nil;
pthread_mutex_lock(&q->mutex);
if (q->first == nil) {
q->first = new;
} else {
oldlast = q->first;
while (oldlast->next != nil)
oldlast = oldlast->next;
oldlast->next = new;
}
pthread_cond_signal(&q->nonempty);
pthread_mutex_unlock(&q->mutex);
}
void
qprepend(Queue *q, int type, void *data)
{
Elem *new;
assert(q != nil);
assert(data != nil);
new = xmalloc(sizeof new[0]);
new->data = data;
new->type = type;
new->next = nil;
pthread_mutex_lock(&q->mutex);
new->next = q->first;
q->first = new;
pthread_cond_signal(&q->nonempty);
pthread_mutex_unlock(&q->mutex);
}
void *
qremove(Queue *q, int *typep)
{
void *r;
Elem *newfirst;
assert(q != nil);
pthread_mutex_lock(&q->mutex);
if (q->first == nil)
pthread_cond_wait(&q->nonempty, &q->mutex);
assert(q->first != nil);
r = q->first->data;
if (typep != nil)
(*typep) = q->first->type;
newfirst = q->first->next;
free(q->first);
q->first = newfirst;
pthread_mutex_unlock(&q->mutex);
return r;
}
void
qappendf(Queue *q, int type, const char *fmt, ...)
{
char *tmp;
va_list ap;
va_start(ap, fmt);
vasprintf(&tmp, fmt, ap);
va_end(ap);
qappend(q, type, tmp);
}
void
qprependf(Queue *q, int type, const char *fmt, ...)
{
char *tmp;
va_list ap;
va_start(ap, fmt);
vasprintf(&tmp, fmt, ap);
va_end(ap);
qprepend(q, type, tmp);
}
void
qflushout(void)
{
Elem *newfirst;
Elem *newcur;
Elem *cur, *tmp;
pthread_mutex_lock(&outq->mutex);
newfirst = newcur = nil;
cur = outq->first;
while (cur != nil) {
if (cur->type == OUTTYPEimmediate) {
if (newfirst == nil) {
newfirst = newcur = cur;
newfirst->next = nil;
} else {
newcur->next = cur;
newcur = newcur->next;
newcur->next = nil;
}
cur = cur->next;
} else {
free(cur->data);
tmp = cur;
cur = cur->next;
free(tmp);
}
}
outq->first = newfirst;
pthread_mutex_unlock(&outq->mutex);
}
Nicks *
nnew(const char *file)
{
Nicks *n;
FILE *fp;
char buf[NICK_MAXLEN + 1];
n = xmalloc(sizeof n[0]);
n->first = nil;
fp = fopen(file, "r");
if (fp == nil)
err(1, "opening %s", file);
/* read nicks from file and add them to the Nicks structure */
for (;;) {
if (fgets(buf, sizeof buf, fp) == nil) {
if (ferror(fp))
err(1, "reading from %s", file);
break;
}
if (strchr(buf, '\n') != nil)
buf[strlen(buf) - 1] = '\0';
nadd(n, xstrdup(buf));
}
fclose(fp);
return n;
}
void
nfree(Nicks *n)
{
Nick *cur, *tmp;
for (cur = n->first; cur != nil; tmp = cur->next, free(cur), cur = tmp)
free(cur->name);
free(n);
}
void
nadd(Nicks *n, char *name)
{
Nick *new;
Nick **ptr;
int wslen;
/* remove leading and trailing whitespace from nick */
wslen = strspn(name, " \t\n");
memmove(name, name+wslen, strlen(name+wslen) + 1);
if (strpbrk(name, " \t\n") != nil)
*(strpbrk(name, " \t\n")) = '\0';
if (nhas(n, name) || strlen(name) == 0) /* avoid duplicates, refuse empty nicks */
return;
new = xmalloc(sizeof new[0]);
new->name = name;
new->next = nil;
ptr = &n->first;
while (*ptr != nil)
ptr = &(*ptr)->next;
*ptr = new;
}
void
nremove(Nicks *n, char *name)
{
Nick *tmp;
Nick **newnext;
newnext = &n->first;
while (*newnext != nil) {
if (strequiv((*newnext)->name, name)) {
tmp = *newnext;
*newnext = (*newnext)->next;
free(tmp->name);
free(tmp);
return;
} else {
newnext = &(*newnext)->next;
}
}
}
int
nhas(Nicks *n, const char *name)
{
Nick *cur;
for (cur = n->first; cur != nil; cur = cur->next)
if (strequiv(cur->name, name))
return 1;
return 0;
}
void
nprint(Nicks *n)
{
Nick *cur;
fprintf(stderr, "bot: nprint: ");
for (cur = n->first; cur != nil; cur = cur->next)
fprintf(stderr, "`%s' ", cur->name);
fprintf(stderr, "\n");
}
void
nsave(Nicks *n, const char *file)
{
Nick *cur;
FILE *fp;
fp = fopen(file, "w");
if (fp == nil) {
warn("opening %s", file);
return;
}
for (cur = n->first; cur != nil; cur = cur->next)
fprintf(fp, "%s\n", cur->name);
if (ferror(fp))
warnx("ferror after writing %s", file);
fclose(fp);
}
Buf *
bnew(int fd)
{
Buf *new;
assert(fd >= 0);
new = xmalloc(sizeof new[0]);
new->fd = fd;
new->s[0] = '\0';
new->slen = 0;
new->eof = 0;
return new;
}
/* trailing newline is kept if present, there could be no newline at all */
int
breadln(Buf *b, char *dest, int destsize)
{
int count;
char *newline;
int flushtonewline;
assert(destsize >= sizeof b->s);
if (b->eof)
return 0;
flushtonewline = 0;
while (strchr(b->s, '\n') == nil) {
if (b->slen == sizeof b->s - 1) {
/* line too long, flush entire line */
flushtonewline = 1;
b->s[0] = '\0';
b->slen = 0;
}
count = read(b->fd, b->s + b->slen, (BBUFFERSIZE - 1) - b->slen);
if (count == 0) {
b->eof = 1;
memmove(dest, b->s, b->slen);
dest[b->slen] = '\0';
count = b->slen;
b->s[0] = '\0';
b->slen = 0;
return count;
}
if (count == -1)
return -1;
b->slen += count;
b->s[b->slen] = '\0';
if (flushtonewline) {
newline = strchr(b->s, '\n');
if (newline == nil) {
b->s[0] = '\0';
b->slen = 0;
} else {
memmove(b->s, newline+1, b->slen - (newline - b->s + 1) + 1);
b->slen -= newline - b->s + 1;
flushtonewline = 0;
}
}
}
newline = strchr(b->s, '\n');
assert(newline != nil);
count = newline - b->s + 1;
strncpy(dest, b->s, count);
dest[count] = '\0';
memmove(b->s, b->s+count, b->slen - count + 1);
b->slen -= count;
return count;
}