#include #include enum { Maxclients = 5, Maxchannels = 5, Nicklen = 15, Chanlen = 15, Msglen = 512, /* 510 + "\r\n" */ Timeout = 120 * 1000, }; enum { Ping = 1, Pong, Welcome, Join, Part, Quit, Privmsg, Nick, Nosuchnick, Erroneousnick, Nickinuse, Notonchannel, Topic, Notopic, Rpltopic, Rpltopicwho, Unknowncmd, }; typedef struct Item Item; typedef struct List List; typedef struct Client Client; typedef struct Chan Chan; typedef struct Reply Reply; struct Item { void *data; Item *next; }; struct List { int size; Item *head; Item *tail; }; struct Client { int fd; NetConnInfo *conninfo; char *user; char *nick; char *prefix; List channels; int pid; int pings; /* unreplied pings */ long msgtimer; /* for flood */ }; /* not to mix with thread(2) Channel:s */ struct Chan { char *name; char *topic; char *topicwho; List members; }; /* message that is sent to clients */ struct Reply { int code; char *argv[5]; }; List clients; List channels; QLock clock; char *servername; int debug; int dprint(char *fmt, ...) { va_list va; int rc; if(!debug) return 0; va_start(va, fmt); rc = vfprint(2, fmt, va); va_end(va); return rc; } void * emalloc(ulong n) { void *p; p = mallocz(n, 1); if(p == nil) sysfatal("mallocz: %r"); return p; } char * estrdup(char *s) { s = strdup(s); if(s == nil) sysfatal("strdup: %r"); return s; } void add(List *l, void *data) { Item *i; if(data == nil){ fprint(2, "add: nil data\n"); assert(0); return; } for(i = l->head; i != nil; i = i->next) if(i->data == data){ fprint(2, "add: duplicate data\n"); assert(0); return; } i = emalloc(sizeof(Item)); i->data = data; i->next = nil; if(l->head == nil) l->head = i; else l->tail->next = i; l->tail = i; l->size++; } void del(List *l, void *data) { Item *i, *prev; if(data == nil){ fprint(2, "del: nil data\n"); assert(0); return; } for(i = l->head, prev = nil; i != nil; i = i->next){ if(i->data == data){ if(prev == nil) /* first */ l->head = i->next; else prev->next = i->next; if(i->next == nil) /* last */ l->tail = prev; free(i); l->size--; return; } prev = i; } } void freelist(List *l) { Item *i, *next; for(i = l->head; i != nil; i = next){ next = i->next; free(i); } l->size = 0; l->head = nil; l->tail = nil; } Chan * addchan(char *name) { Chan *ch; dprint("adding channel %s\n", name); ch = emalloc(sizeof(Chan)); ch->name = estrdup(name); add(&channels, ch); return ch; } void delchan(Chan *ch) { dprint("deleting channel %s\n", ch->name); del(&channels, ch); if(ch->name != nil) free(ch->name); if(ch->topic != nil) free(ch->topic); if(ch->topicwho != nil) free(ch->topicwho); freelist(&ch->members); free(ch); } Client * addclient(int fd) { NetConnInfo *ci; Client *c; ci = getnetconninfo(nil, fd); if(ci == nil){ perror("getnetconninfo"); return nil; } c = emalloc(sizeof(Client)); c->fd = fd; c->conninfo = ci; c->pid = getpid(); dprint("%d: rsys=%s rserv=%s raddr=%s pid=%d\n", fd, ci->rsys, ci->rserv, ci->raddr, c->pid); qlock(&clock); add(&clients, c); qunlock(&clock); return c; } void delclient(Client *c) { Item *i; Chan *ch; dprint("deleting client %d\n", c->fd); qlock(&clock); del(&clients, c); if(c->conninfo != nil) freenetconninfo(c->conninfo); if(c->user != nil) free(c->user); if(c->nick != nil) free(c->nick); if(c->prefix != nil) free(c->prefix); for(i = c->channels.head; i != nil; i = i->next){ ch = i->data; del(&ch->members, c); if(ch->members.size == 0) delchan(ch); } freelist(&c->channels); qunlock(&clock); free(c); } Client * getclient(List *l, char *nick) { Item *i; Client *c; for(i = l->head; i != nil; i = i->next){ c = i->data; if(c->nick != nil && strcmp(c->nick, nick) == 0) return c; } return nil; } Chan * getchan(List *l, char *name) { Item *i; Chan *ch; for(i = l->head; i != nil; i = i->next){ ch = i->data; if(ch->name != nil && strcmp(ch->name, name) == 0) return ch; } return nil; } void ewrite(int fd, char *buf, int n) { if(n <= 0) return; n = write(fd, buf, n); if(n < 0) perror("write"); } void reply(Client *c, Reply *r) { static char buf[Msglen+1]; int n; n = sizeof(buf); switch(r->code){ case Ping: n = snprint(buf, n, "PING :%s\r\n", servername); break; case Pong: n = snprint(buf, n, ":%s PONG %s\r\n", servername, servername); break; case Welcome: n = snprint(buf, n, ":%s 001 %s :Welcome %s\r\n", servername, c->nick, c->prefix); break; case Join: /* r->argv is: joiner, channel */ n = snprint(buf, n, ":%s JOIN %s\r\n", r->argv[0], r->argv[1]); break; case Part: /* r->argv is: parter, channel, reason */ n = snprint(buf, n, ":%s PART %s :%s\r\n", r->argv[0], r->argv[1], r->argv[2]); break; case Quit: /* r->argv is: quitter, reason */ n = snprint(buf, n, ":%s QUIT :%s\r\n", r->argv[0], r->argv[1]); break; case Privmsg: /* r->argv is: sender, target, message */ n = snprint(buf, n, ":%s PRIVMSG %s :%s\r\n", r->argv[0], r->argv[1], r->argv[2]); break; case Nick: /* r->argv is: old prefix, new nick */ n = snprint(buf, n, ":%s NICK %s\r\n", r->argv[0], r->argv[1]); break; case Nosuchnick: /* r->argv[0] is nick/channel */ n = snprint(buf, n, ":%s 401 %s %s :No such nick/channel\r\n", servername, c->nick, r->argv[0]); break; case Erroneousnick: /* r->argv[0] is nick */ n = snprint(buf, n, ":%s 432 %s %s :Erroneous nickname\r\n", servername, c->nick, r->argv[0]); break; case Nickinuse: /* r->argv[0] is nick */ n = snprint(buf, n, ":%s 433 %s %s :Nickname is already in use\r\n", servername, c->nick, r->argv[0]); break; case Notonchannel: /* r->argv[0] is channel */ n = snprint(buf, n, ":%s 442 %s %s :You're not on that channel\r\n", servername, c->nick, r->argv[0]); break; case Topic: /* r->argv is: setter, channel, topic */ n = snprint(buf, n, ":%s TOPIC %s :%s\r\n", r->argv[0], r->argv[1], r->argv[2]); break; case Rpltopic: /* r->argv is: channel, topic */ n = snprint(buf, n, ":%s 332 %s %s :%s\r\n", servername, c->nick, r->argv[0], r->argv[1]); break; case Rpltopicwho: /* r->argv is: channel, topicwhotime */ n = snprint(buf, n, ":%s 333 %s %s :%s\r\n", servername, c->nick, r->argv[0], r->argv[1]); break; case Notopic: /* r->argv is: channel */ n = snprint(buf, n, ":%s 331 %s %s :No topic is set\r\n", servername, c->nick, r->argv[0]); break; case Unknowncmd: /* r->argv is: command */ n = snprint(buf, n, ":%s 421 %s %s :Unknown command\r\n", servername, c->nick, r->argv[0]); break; default: fprint(2, "%d: reply: unknown code %d\n", c->fd, r->code); assert(0); } if(r->code != Ping && r->code != Pong) dprint("%d: reply: %s", c->fd, buf); ewrite(c->fd, buf, n); } void replylist(List *l, Reply *r, Client *skipme) { Item *i; Client *c; for(i = l->head; i != nil; i = i->next){ c = i->data; if(c != skipme) reply(c, r); } } void replychan(Chan *ch, Reply *r, Client *skipme) { replylist(&ch->members, r, skipme); } void replychans(Client *c, Reply *r, Client *skipme) { Item *i; Chan *ch; for(i = c->channels.head; i != nil; i = i->next){ ch = i->data; replylist(&ch->members, r, skipme); } } void setprefix(Client *c) { if(c->user == nil || c->nick == nil) return; if(c->prefix != nil) free(c->prefix); c->prefix = smprint("%s!~%s@%s", c->nick, c->user, c->conninfo->rsys); if(c->prefix == nil) sysfatal("setprefix: smprint: %r"); } /* allowed chars: [0-9A-Za-z_] */ int validname(char *s, int maxlen) { int i; char c; i = 0; while((c = s[i]) != '\0' && i < maxlen){ if(!((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || (c == '_'))) return 0; i++; } if(i == 0) return 0; if(i == maxlen && c != '\0') return 0; return 1; } int validnick(char *s) { return validname(s, Nicklen); } int validchan(char *s) { return *s == '#' && validname(s+1, Chanlen-1); } int user(Client *c, char *user) { Reply r; if(c->user != nil) return 0; qlock(&clock); c->user = estrdup(user); setprefix(c); qunlock(&clock); dprint("%d: user=%s\n", c->fd, c->user); if(c->nick == nil) return 1; r.code = Welcome; reply(c, &r); return 1; } int nickinuse(char *nick) { return getclient(&clients, nick) != nil; } int nick(Client *c, char *nick) { Reply r; int first; if(!validnick(nick)){ dprint("%d: invalid nick %s\n", c->fd, nick); r.code = Erroneousnick; r.argv[0] = nick; reply(c, &r); return 1; } /* no change */ if(c->nick != nil && strcmp(nick, c->nick) == 0) return 1; first = 0; qlock(&clock); if(nickinuse(nick)){ dprint("%d: nick %s is in use\n", c->fd, nick); r.code = Nickinuse; r.argv[0] = nick; reply(c, &r); }else{ /* nick change */ if(c->nick != nil){ r.code = Nick; r.argv[0] = c->prefix; /* old prefix */ r.argv[1] = nick; /* new nick */ replychans(c, &r, c); reply(c, &r); free(c->nick); }else first = 1; c->nick = estrdup(nick); setprefix(c); dprint("%d: nick=%s\n", c->fd, c->nick); } qunlock(&clock); if(first && c->user != nil){ r.code = Welcome; reply(c, &r); } return 1; } int replynames(Client *c, Chan *ch) { static char buf[Msglen+1]; Item *i; Client *m; int n, len, room; char *p; if(ch->members.size == 0) return 1; p = buf; len = 0; room = sizeof(buf); n = snprint(p, room, ":%s 353 %s = %s :", servername, c->nick, ch->name); p += n; len += n; room -= n; for(i = ch->members.head; i != nil; i = i->next){ m = i->data; if(room < Nicklen+4){ /* nick + " \r\n\0" */ assert(room > 2); /* finish line */ n = snprint(p, room, "\r\n"); len += n; dprint("%d: reply: %s", c->fd, buf); ewrite(c->fd, buf, len); /* start new */ p = buf; len = 0; room = sizeof(buf); n = snprint(p, room, ":%s 353 %s = %s :", servername, c->nick, ch->name); p += n; len += n; room -= n; } n = snprint(p, room, "%s ", m->nick); p += n; len += n; room -= n; } /* finish line */ n = snprint(p, room, "\r\n"); len += n; dprint("%d: reply: %s", c->fd, buf); ewrite(c->fd, buf, len); /* the end */ n = snprint(buf, sizeof(buf), ":%s 366 %s %s :End of /NAMES list\r\n", servername, c->nick, ch->name); dprint("%d: reply: %s", c->fd, buf); ewrite(c->fd, buf, n); return 1; } void replytopic(Client *c, Chan *ch) { Reply r; if(ch->topic == nil) return; r.code = Rpltopic; r.argv[0] = ch->name; r.argv[1] = ch->topic; reply(c, &r); if(ch->topicwho == nil) return; r.code = Rpltopicwho; r.argv[1] = ch->topicwho; reply(c, &r); } int joined(Client *c, char *chan) { return getchan(&c->channels, chan) != nil; } int join(Client *c, char *chan) { Chan *ch; Reply r; if(joined(c, chan)) return 1; qlock(&clock); ch = getchan(&channels, chan); if(ch == nil){ if(channels.size == Maxchannels){ fprint(2, "%d: Maxchannels reached\n", c->fd); /* reply? */ goto end; } if(!validchan(chan)){ dprint("%d: invalid chan %s\n", c->fd, chan); /* reply? */ goto end; } ch = addchan(chan); } add(&ch->members, c); add(&c->channels, ch); dprint("%d: join chan=%s\n", c->fd, chan); r.code = Join; r.argv[0] = c->prefix; r.argv[1] = chan; replychan(ch, &r, nil); replytopic(c, ch); replynames(c, ch); end: qunlock(&clock); return 1; } int part(Client *c, char *chan, char *reason) { Chan *ch; Reply r; qlock(&clock); ch = getchan(&c->channels, chan); if(ch == nil){ r.code = Notonchannel; r.argv[0] = chan; reply(c, &r); }else{ dprint("%d: part chan=%s\n", c->fd, chan); r.code = Part; r.argv[0] = c->prefix; r.argv[1] = chan; r.argv[2] = reason; replychan(ch, &r, nil); del(&c->channels, ch); del(&ch->members, c); if(ch->members.size == 0) delchan(ch); } qunlock(&clock); return 1; } int topic(Client *c, char *chan, char *topic) { Chan *ch; Reply r; if(!joined(c, chan)){ r.code = Notonchannel; r.argv[0] = chan; reply(c, &r); return 1; } qlock(&clock); ch = getchan(&channels, chan); if(ch == nil){ assert(0); goto end; } if(topic == nil){ /* get */ if(ch->topic == nil){ r.code = Notopic; r.argv[0] = chan; reply(c, &r); }else{ replytopic(c, ch); } }else{ /* set */ if(ch->topic != nil) free(ch->topic); ch->topic = estrdup(topic); if(ch->topicwho != nil) free(ch->topicwho); ch->topicwho = smprint("%s %ld", c->prefix, time(0)); r.code = Topic; r.argv[0] = c->prefix; r.argv[1] = chan; r.argv[2] = topic; replychan(ch, &r, nil); } end: qunlock(&clock); return 1; } void quit(Client *c, char *reason) { Reply r; dprint("%d: quit: %s\n", c->fd, reason); r.code = Quit; r.argv[0] = c->prefix; r.argv[1] = reason; qlock(&clock); replychans(c, &r, c); qunlock(&clock); } int privmsg(Client *c, char *target, char *msg) { Client *tc; Chan *ch; Reply r; r.code = Privmsg; r.argv[0] = c->prefix; /* from */ r.argv[1] = target; r.argv[2] = msg; qlock(&clock); if(*target == '#'){ ch = getchan(&channels, target); if(ch != nil){ if(joined(c, target)) replychan(ch, &r, c); else{ r.code = Notonchannel; r.argv[0] = target; reply(c, &r); } }else{ r.code = Nosuchnick; r.argv[0] = target; reply(c, &r); } }else{ tc = getclient(&clients, target); if(tc != nil) reply(tc, &r); else{ r.code = Nosuchnick; r.argv[0] = target; reply(c, &r); } } qunlock(&clock); return 1; } int list(Client *c, char *chans) { static char buf[Msglen+1]; Item *i; Chan *ch; int nargs, n, j; char *args[5]; if(chans == nil) nargs = 0; else nargs = getfields(chans, args, 5, 0, ","); n = snprint(buf, sizeof(buf), ":%s 321 %s Channel :Users Name\r\n", servername, c->nick); dprint("%d: reply: %s", c->fd, buf); ewrite(c->fd, buf, n); qlock(&clock); for(i = channels.head; i != nil; i = i->next){ ch = i->data; if(nargs == 0) goto match; for(j = 0; j < nargs; j++) if(strcmp(ch->name, args[j]) == 0) goto match; continue; match: n = snprint(buf, sizeof(buf), ":%s 322 %s %s %d :%s\r\n", servername, c->nick, ch->name, ch->members.size, ch->topic == nil ? "" : ch->topic); dprint("%d: reply: %s", c->fd, buf); ewrite(c->fd, buf, n); } qunlock(&clock); n = snprint(buf, sizeof(buf), ":%s 323 %s :End of /LIST\r\n", servername, c->nick); dprint("%d: reply: %s", c->fd, buf); ewrite(c->fd, buf, n); return 1; } int whois(Client *c, char *nicks) { static char buf[Msglen+1]; Item *i; Client *w; int nargs, n, j; char *args[5]; if(nicks == nil || strcmp(nicks, "*") == 0) nargs = 0; else nargs = getfields(nicks, args, 5, 0, ","); qlock(&clock); for(i = clients.head; i != nil; i = i->next){ w = i->data; if(w->prefix == nil) continue; if(nargs == 0) goto match; for(j = 0; j < nargs; j++) if(strcmp(w->nick, args[j]) == 0) goto match; continue; match: /* skip realname at the end */ n = snprint(buf, sizeof(buf), ":%s 311 %s %s %s %s * :\r\n", servername, c->nick, w->nick, w->user, w->conninfo->rsys); dprint("%d: reply: %s", c->fd, buf); ewrite(c->fd, buf, n); } qunlock(&clock); n = snprint(buf, sizeof(buf), ":%s 318 %s %s :End of /WHOIS\r\n", servername, c->nick, nicks == nil ? "*" : nicks); dprint("%d: reply: %s", c->fd, buf); ewrite(c->fd, buf, n); return 1; } int process(Client *c, char *msg) { char *cmd, *tmp, *tmp2; Reply r; cmd = strtok(msg, " :\r"); if(cmd == nil) return 0; if(strcmp(cmd, "PING") == 0){ r.code = Pong; reply(c, &r); return 1; } if(strcmp(cmd, "PONG") == 0){ c->pings = 0; return 1; } if(strcmp(cmd, "NICK") == 0){ tmp = strtok(0, " \r"); /* nick */ if(tmp == nil) return 0; return nick(c, tmp); } if(strcmp(cmd, "USER") == 0){ tmp = strtok(0, " \r"); /* username */ if(tmp == nil) return 0; return user(c, tmp); } if(c->prefix == nil) /* not registered */ return 1; if(strcmp(cmd, "PRIVMSG") == 0){ tmp = strtok(0, " :"); /* target */ if(tmp == nil) return 0; tmp2 = strtok(0, "\r"); /* msg */ if(tmp2 == nil) return 0; tmp2++; return privmsg(c, tmp, tmp2); } if(strcmp(cmd, "JOIN") == 0){ tmp = strtok(0, " \r"); /* channel */ if(tmp == nil) return 0; return join(c, tmp); } if(strcmp(cmd, "PART") == 0){ tmp = strtok(0, " :"); /* channel */ if(tmp == nil) return 0; tmp2 = strtok(0, "\r"); /* reason */ if(tmp2 == nil) return 0; tmp2++; return part(c, tmp, tmp2); } if(strcmp(cmd, "TOPIC") == 0){ tmp = strtok(0, " :\r"); /* channel */ if(tmp == nil) return 0; tmp2 = strtok(0, "\r"); /* topic */ if(tmp2 != nil) tmp2++; return topic(c, tmp, tmp2); } if(strcmp(cmd, "QUIT") == 0){ tmp = strtok(0, "\r"); /* reason */ if(tmp == nil) return 0; tmp++; quit(c, tmp); return 0; } if(strcmp(cmd, "LIST") == 0){ tmp = strtok(0, " \r"); /* channels */ return list(c, tmp); } if(strcmp(cmd, "WHOIS") == 0){ tmp = strtok(0, " \r"); /* nicks */ return whois(c, tmp); } r.code = Unknowncmd; r.argv[0] = cmd; reply(c, &r); return 1; } int notehandler(void *, char *msg) { dprint("pid=%d received note: %s\n", getpid(), msg); if(strcmp(msg, "alarm") == 0) return 1; return 0; } int flood(Client *c) { long now; now = time(0); if(c->msgtimer < now){ c->msgtimer = now+2; return 0; } if(c->msgtimer < now+10){ c->msgtimer += 2; return 0; } dprint("%d: flood detected\n", c->fd); quit(c, "flooding"); return 1; } int timeout(Client *c) { Reply r; dprint("%d: checking timeout\n", c->fd); if(c->pings == 2){ dprint("%d: timed out\n", c->fd); quit(c, "timed out"); return 1; } r.code = Ping; reply(c, &r); c->pings++; return 0; } void usage(void) { fprint(2, "%s [-d] [-n servername] addr\n", argv0); exits("usage"); } void main(int argc, char *argv[]) { int afd, lfd, dfd; char adir[40], ldir[40]; int n, i, len; char buf[1024]; Client *c; char *msg; char err[ERRMAX]; debug = 0; servername = nil; ARGBEGIN{ case 'd': debug++; break; case 'n': servername = EARGF(usage()); break; default: usage(); }ARGEND if(argc != 1) usage(); if(servername == nil) servername = estrdup("ircd"); afd = announce(argv[0], adir); if(afd < 0) sysfatal("announce: %r"); dprint("addr=%s afd=%d adir=%s pid=%d\n", argv[0], afd, adir, getpid()); atnotify(notehandler, 1); for(;;){ lfd = listen(adir, ldir); if(lfd < 0) sysfatal("listen: %r"); dprint("lfd=%d ldir=%s\n", lfd, ldir); if(clients.size >= Maxclients){ fprint(2, "Maxclients reached; reject lfd=%d\n", lfd); close(lfd); continue; } switch(rfork(RFPROC|RFNOMNT|RFMEM)){ case -1: perror("fork"); close(lfd); break; case 0: dfd = accept(lfd, ldir); close(lfd); if(dfd < 0){ perror("accept"); exits("no"); } dprint("dfd=%d\n", dfd); c = addclient(dfd); if(c == nil){ close(dfd); exits("no"); } for(;;){ alarm(Timeout); n = read(dfd, buf, sizeof(buf)-1); alarm(0); if(n == 0){ quit(c, "eof"); break; } if(n < 0){ err[0] = '\0'; errstr(err, sizeof(err)); if(strcmp(err, "interrupted") == 0){ if(timeout(c)) break; }else{ perror("read"); quit(c, "read error"); break; } } msg = buf; len = 0; for(i = 0; i <= n; i++){ if(buf[i] == '\n' || i == n){ buf[i] = '\0'; if(len > 0){ dprint("%d: msg: %s\n", dfd, msg); if(flood(c)) goto bye; if(!process(c, msg)) goto bye; } msg = &buf[i+1]; len = 0; }else len++; } } bye: dprint("dfd=%d disconnected\n", dfd); delclient(c); close(dfd); exits(0); } } }