// Written by retoor@molodetz.nl
// This source code is an IRC server implementation utilizing the epoll mechanism for handling client connections and commands efficiently. It supports basic IRC functionalities like user registration, channel creation, message broadcasting, and more.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
#include <ctype.h>
#include <unistd.h>
#include <errno.h>
#include <fcntl.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <time.h>
#include <stdarg.h>
#include <sys/epoll.h>
// MIT License
#define MAX_EVENTS 4096
#define MAX_CHANNELS 4096
#define MAX_NICKLEN 4096
#define MAX_USERLEN 4096
#define MAX_CHANNELLEN 128
#define MAX_TOPICLEN 4096
#define MAX_MSG 4096
#define MOTD_FILE "motd.txt"
#define SERVER_NAME "rirc"
#define VERSION "1.0"
struct Client;
struct Channel;
typedef struct Client {
int fd;
char nickname[MAX_NICKLEN+1];
char user[MAX_USERLEN+1];
char realname[MAX_USERLEN+1];
char host[128];
int registered;
int pass_ok;
char readbuf[MAX_MSG*2];
int readbuf_len;
char writebuf[MAX_MSG*4];
int writebuf_len;
struct Channel **channels;
int channel_count;
int channel_capacity;
time_t last_activity;
int sent_ping;
struct Client *next;
} Client;
typedef struct Channel {
char name[MAX_CHANNELLEN+1];
char topic[MAX_TOPICLEN+1];
char key[MAX_CHANNELLEN+1];
Client **members;
int member_count;
int member_capacity;
struct Channel *next;
} Channel;
static Client *clients = NULL;
static Channel *channels = NULL;
static int listen_fd;
static int epoll_fd;
static char *password = NULL;
static int logging_enabled = 0;
static void log_event(const char *fmt, ...) {
if (!logging_enabled) return;
char timebuf[32];
time_t now = time(NULL);
struct tm tm;
gmtime_r(&now, &tm);
strftime(timebuf, sizeof(timebuf), "%Y-%m-%dT%H:%M:%SZ", &tm);
fprintf(stderr, "%s ", timebuf);
va_list ap;
va_start(ap, fmt);
vfprintf(stderr, fmt, ap);
va_end(ap);
fprintf(stderr, "\n");
}
static void fatal(const char *msg) {
perror(msg);
exit(1);
}
static void set_nonblocking(int fd) {
int flags = fcntl(fd, F_GETFL, 0);
if (flags == -1) fatal("fcntl getfl");
if (fcntl(fd, F_SETFL, flags | O_NONBLOCK) == -1) fatal("fcntl setfl");
}
static void add_epoll(int fd, uint32_t events, void *ptr) {
struct epoll_event ev = {0};
ev.events = events;
ev.data.ptr = ptr;
if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, fd, &ev) < 0) fatal("epoll_ctl add");
}
static void mod_epoll(int fd, uint32_t events, void *ptr) {
if (fd < 0) return;
struct epoll_event ev = {0};
ev.events = events;
ev.data.ptr = ptr;
if (epoll_ctl(epoll_fd, EPOLL_CTL_MOD, fd, &ev) < 0) return;
}
static void del_epoll(int fd) {
if (fd < 0) return;
epoll_ctl(epoll_fd, EPOLL_CTL_DEL, fd, NULL);
}
static void update_client_events(Client *c) {
uint32_t events = EPOLLIN;
if (c->writebuf_len > 0) events |= EPOLLOUT;
mod_epoll(c->fd, events, c);
}
static void send_reply(Client *c, const char *fmt, ...) {
char buf[MAX_MSG];
va_list ap;
va_start(ap, fmt);
vsnprintf(buf, sizeof(buf), fmt, ap);
va_end(ap);
int n = snprintf(c->writebuf + c->writebuf_len, sizeof(c->writebuf) - c->writebuf_len, ":%s %s\r\n", SERVER_NAME, buf);
if (n > 0 && c->writebuf_len + n < sizeof(c->writebuf)) c->writebuf_len += n;
update_client_events(c);
}
static void send_raw(Client *c, const char *fmt, ...) {
char buf[MAX_MSG];
va_list ap;
va_start(ap, fmt);
vsnprintf(buf, sizeof(buf), fmt, ap);
va_end(ap);
int n = snprintf(c->writebuf + c->writebuf_len, sizeof(c->writebuf) - c->writebuf_len, "%s\r\n", buf);
if (n > 0 && c->writebuf_len + n < sizeof(c->writebuf)) c->writebuf_len += n;
update_client_events(c);
}
static Channel *find_channel(const char *name) {
for (Channel *ch = channels; ch; ch = ch->next)
if (strcasecmp(ch->name, name) == 0)
return ch;
return NULL;
}
static Channel *get_or_create_channel(const char *name) {
Channel *ch = find_channel(name);
if (ch) return ch;
ch = calloc(1, sizeof(Channel));
strncpy(ch->name, name, MAX_CHANNELLEN);
ch->member_capacity = 8;
ch->members = calloc(ch->member_capacity, sizeof(Client*));
ch->next = channels;
channels = ch;
return ch;
}
static void add_client_to_channel(Client *c, Channel *ch) {
if (ch->member_count == ch->member_capacity) {
ch->member_capacity *= 2;
ch->members = realloc(ch->members, ch->member_capacity * sizeof(Client*));
}
ch->members[ch->member_count++] = c;
if (c->channel_count == c->channel_capacity) {
c->channel_capacity = c->channel_capacity ? c->channel_capacity * 2 : 8;
c->channels = realloc(c->channels, c->channel_capacity * sizeof(Channel*));
}
c->channels[c->channel_count++] = ch;
log_event("user joined: %s (%s) joined channel %s", c->nickname[0]?c->nickname:"<unreg>", c->host, ch->name);
}
static void remove_client_from_channel(Client *c, Channel *ch) {
for (int i = 0; i < ch->member_count; ++i) {
if (ch->members[i] == c) {
for (int j = i; j < ch->member_count-1; ++j)
ch->members[j] = ch->members[j+1];
ch->member_count--;
log_event("user left: %s (%s) left channel %s", c->nickname[0]?c->nickname:"<unreg>", c->host, ch->name);
break;
}
}
for (int i = 0; i < c->channel_count; ++i) {
if (c->channels[i] == ch) {
for (int j = i; j < c->channel_count-1; ++j)
c->channels[j] = c->channels[j+1];
c->channel_count--;
break;
}
}
}
static Client *find_client_by_nick(const char *nick) {
for (Client *c = clients; c; c = c->next)
if (strcasecmp(c->nickname, nick) == 0)
return c;
return NULL;
}
static void broadcast_channel(Channel *ch, Client *from, const char *fmt, ...) {
char buf[MAX_MSG];
va_list ap;
va_start(ap, fmt);
vsnprintf(buf, sizeof(buf), fmt, ap);
va_end(ap);
for (int i = 0; i < ch->member_count; ++i) {
Client *c = ch->members[i];
if (c != from) send_raw(c, "%s", buf);
}
}
static void send_motd(Client *c) {
FILE *f = fopen(MOTD_FILE, "r");
if (!f) {
send_reply(c, "422 %s :MOTD File is missing", c->nickname[0]?c->nickname:"*");
return;
}
send_reply(c, "375 %s :- %s Message of the day -", c->nickname, SERVER_NAME);
char line[256];
while (fgets(line, sizeof(line), f)) {
line[strcspn(line, "\r\n")] = 0;
send_reply(c, "372 %s :- %s", c->nickname, line);
}
send_reply(c, "376 %s :End of /MOTD command", c->nickname);
fclose(f);
}
static void send_lusers(Client *c) {
int count = 0;
for (Client *cl = clients; cl; cl = cl->next) count++;
send_reply(c, "251 %s :There are %d users and 0 services on 1 server", c->nickname, count);
}
static void send_names(Client *c, Channel *ch) {
char names[MAX_MSG] = "";
for (int i = 0; i < ch->member_count; ++i) {
if (strlen(names) + strlen(ch->members[i]->nickname) + 2 >= sizeof(names)) break;
strncat(names, ch->members[i]->nickname, sizeof(names) - strlen(names) - 1);
if (i < ch->member_count-1 && strlen(names) + 2 < sizeof(names)) strncat(names, " ", sizeof(names) - strlen(names) - 1);
}
send_reply(c, "353 %s = %s :%s", c->nickname, ch->name, names);
send_reply(c, "366 %s %s :End of NAMES list", c->nickname, ch->name);
}
static void send_list(Client *c, Channel *ch) {
send_reply(c, "322 %s %s %d :%s", c->nickname, ch->name, ch->member_count, ch->topic[0]?ch->topic:"");
}
static void handle_registration(Client *c) {
if (c->registered) return;
if (!c->nickname[0] || !c->user[0]) return;
c->registered = 1;
send_reply(c, "001 %s :Hi, welcome to IRC", c->nickname);
send_reply(c, "002 %s :Your host is %s, running version rirc-%s", c->nickname, SERVER_NAME, VERSION);
send_reply(c, "003 %s :This server was created sometime", c->nickname);
send_reply(c, "004 %s %s rirc-%s o o", c->nickname, SERVER_NAME, VERSION);
send_lusers(c);
send_motd(c);
}
static void handle_line(Client *c, char *line) {
log_event("message received: %s (%s): %s", c->nickname[0]?c->nickname:"<unreg>", c->host, line);
char *cmd = strtok(line, " ");
if (!cmd) return;
if (strcasecmp(cmd, "NICK") == 0) {
char *nick = strtok(NULL, " ");
if (!nick) { send_reply(c, "431 :No nickname given"); return; }
if (find_client_by_nick(nick)) { send_reply(c, "433 * %s :Nickname is already in use", nick); return; }
strncpy(c->nickname, nick, MAX_NICKLEN);
handle_registration(c);
} else if (strcasecmp(cmd, "USER") == 0) {
char *user = strtok(NULL, " ");
strtok(NULL, " "); strtok(NULL, " ");
char *realname = strtok(NULL, ":");
if (!user || !realname) { send_reply(c, "461 * USER :Not enough parameters"); return; }
strncpy(c->user, user, MAX_USERLEN);
strncpy(c->realname, realname, MAX_USERLEN);
handle_registration(c);
} else if (strcasecmp(cmd, "PASS") == 0) {
char *pass = strtok(NULL, " ");
if (!password) { c->pass_ok = 1; return; }
if (!pass) { send_reply(c, "461 * PASS :Not enough parameters"); return; }
if (strcmp(pass, password) == 0) c->pass_ok = 1;
else send_reply(c, "464 :Password incorrect");
} else if (strcasecmp(cmd, "PING") == 0) {
char *arg = strtok(NULL, " ");
if (!arg) { send_reply(c, "409 %s :No origin specified", c->nickname); return; }
send_reply(c, "PONG %s :%s", SERVER_NAME, arg);
} else if (strcasecmp(cmd, "PONG") == 0) {
c->sent_ping = 0;
} else if (strcasecmp(cmd, "QUIT") == 0) {
char *msg = strtok(NULL, ":");
send_raw(c, "ERROR :%s", msg?msg:"Client quit");
close(c->fd); c->fd = -1;
} else if (strcasecmp(cmd, "JOIN") == 0) {
char *chan = strtok(NULL, " ");
if (!chan) { send_reply(c, "461 %s JOIN :Not enough parameters", c->nickname); return; }
Channel *ch = get_or_create_channel(chan);
add_client_to_channel(c, ch);
send_raw(c, ":%s!%s@%s JOIN %s", c->nickname, c->user, c->host, ch->name);
if (ch->topic[0])
send_reply(c, "332 %s %s :%s", c->nickname, ch->name, ch->topic);
else
send_reply(c, "331 %s %s :No topic is set", c->nickname, ch->name);
send_names(c, ch);
} else if (strcasecmp(cmd, "PART") == 0) {
char *chan = strtok(NULL, " ");
Channel *ch = find_channel(chan);
if (!ch) { send_reply(c, "403 %s %s :No such channel", c->nickname, chan); return; }
remove_client_from_channel(c, ch);
send_raw(c, ":%s!%s@%s PART %s", c->nickname, c->user, c->host, ch->name);
} else if (strcasecmp(cmd, "PRIVMSG") == 0 || strcasecmp(cmd, "NOTICE") == 0) {
char *target = strtok(NULL, " ");
char *msg = strtok(NULL, ":");
if (!target) { send_reply(c, "411 %s :No recipient given (%s)", c->nickname, cmd); return; }
if (!msg) { send_reply(c, "412 %s :No text to send", c->nickname); return; }
Client *dest = find_client_by_nick(target);
if (dest) send_raw(dest, ":%s!%s@%s %s %s :%s", c->nickname, c->user, c->host, cmd, target, msg);
else {
Channel *ch = find_channel(target);
if (ch) broadcast_channel(ch, c, ":%s!%s@%s %s %s :%s", c->nickname, c->user, c->host, cmd, target, msg);
else send_reply(c, "401 %s %s :No such nick/channel", c->nickname, target);
}
} else if (strcasecmp(cmd, "TOPIC") == 0) {
char *chan = strtok(NULL, " ");
Channel *ch = find_channel(chan);
if (!ch) { send_reply(c, "403 %s %s :No such channel", c->nickname, chan); return; }
char *topic = strtok(NULL, ":");
if (topic) {
strncpy(ch->topic, topic, MAX_TOPICLEN);
broadcast_channel(ch, NULL, ":%s!%s@%s TOPIC %s :%s", c->nickname, c->user, c->host, ch->name, ch->topic);
} else {
if (ch->topic[0])
send_reply(c, "332 %s %s :%s", c->nickname, ch->name, ch->topic);
else
send_reply(c, "331 %s %s :No topic is set", c->nickname, ch->name);
}
} else if (strcasecmp(cmd, "LIST") == 0) {
for (Channel *ch = channels; ch; ch = ch->next)
send_list(c, ch);
send_reply(c, "323 %s :End of LIST", c->nickname);
} else if (strcasecmp(cmd, "NAMES") == 0) {
char *chan = strtok(NULL, " ");
if (chan) {
Channel *ch = find_channel(chan);
if (ch) send_names(c, ch);
else send_reply(c, "403 %s %s :No such channel", c->nickname, chan);
} else {
for (int i = 0; i < c->channel_count; ++i)
send_names(c, c->channels[i]);
}
} else if (strcasecmp(cmd, "MODE") == 0) {
char *chan = strtok(NULL, " ");
Channel *ch = find_channel(chan);
if (!ch) { send_reply(c, "403 %s %s :No such channel", c->nickname, chan); return; }
char *flag = strtok(NULL, " ");
if (!flag) {
send_reply(c, "324 %s %s +", c->nickname, ch->name);
} else if (strcmp(flag, "+k") == 0) {
char *key = strtok(NULL, " ");
if (!key) { send_reply(c, "461 %s MODE :Not enough parameters", c->nickname); return; }
strncpy(ch->key, key, MAX_CHANNELLEN);
broadcast_channel(ch, NULL, ":%s!%s@%s MODE %s +k %s", c->nickname, c->user, c->host, ch->name, key);
} else if (strcmp(flag, "-k") == 0) {
ch->key[0] = 0;
broadcast_channel(ch, NULL, ":%s!%s@%s MODE %s -k", c->nickname, c->user, c->host, ch->name);
} else {
send_reply(c, "472 %s %s :Unknown MODE flag", c->nickname, flag);
}
} else if (strcasecmp(cmd, "MOTD") == 0) {
send_motd(c);
} else if (strcasecmp(cmd, "LUSERS") == 0) {
send_lusers(c);
} else if (strcasecmp(cmd, "ISON") == 0) {
char *nick = strtok(NULL, " ");
char buf[MAX_MSG] = "";
while (nick) {
if (find_client_by_nick(nick)) {
if (strlen(buf) + strlen(nick) + 2 >= sizeof(buf)) break;
strncat(buf, nick, sizeof(buf) - strlen(buf) - 1);
strncat(buf, " ", sizeof(buf) - strlen(buf) - 1);
}
nick = strtok(NULL, " ");
}
send_reply(c, "303 %s :%s", c->nickname, buf);
} else if (strcasecmp(cmd, "WHO") == 0) {
char *chan = strtok(NULL, " ");
Channel *ch = find_channel(chan);
if (ch) {
for (int i = 0; i < ch->member_count; ++i) {
Client *m = ch->members[i];
send_reply(c, "352 %s %s %s %s %s %s H :0 %s", c->nickname, ch->name, m->user, m->host, SERVER_NAME, m->nickname, m->realname);
}
send_reply(c, "315 %s %s :End of WHO list", c->nickname, ch->name);
}
} else if (strcasecmp(cmd, "WHOIS") == 0) {
char *nick = strtok(NULL, " ");
Client *u = find_client_by_nick(nick);
if (u) {
send_reply(c, "311 %s %s %s %s * :%s", c->nickname, u->nickname, u->user, u->host, u->realname);
send_reply(c, "312 %s %s %s :%s", c->nickname, u->nickname, SERVER_NAME, SERVER_NAME);
char buf[MAX_MSG] = "";
for (int i = 0; i < u->channel_count; ++i) {
if (strlen(buf) + strlen(u->channels[i]->name) + 2 >= sizeof(buf)) break;
strncat(buf, u->channels[i]->name, sizeof(buf) - strlen(buf) - 1);
strncat(buf, " ", sizeof(buf) - strlen(buf) - 1);
}
send_reply(c, "319 %s %s :%s", c->nickname, u->nickname, buf);
send_reply(c, "318 %s %s :End of WHOIS list", c->nickname, u->nickname);
} else {
send_reply(c, "401 %s %s :No such nick", c->nickname, nick);
}
} else if (strcasecmp(cmd, "AWAY") == 0) {
} else if (strcasecmp(cmd, "WALLOPS") == 0) {
char *msg = strtok(NULL, ":");
for (Client *cl = clients; cl; cl = cl->next)
send_raw(cl, ":%s NOTICE %s :Global notice: %s", c->nickname, cl->nickname, msg?msg:"");
} else if (strcasecmp(cmd, "CAP") == 0) {
char *sub = strtok(NULL, " ");
if (sub && strcasecmp(sub, "LS") == 0) {
send_raw(c, "CAP * LS :");
} else if (sub && strcasecmp(sub, "END") == 0) {
} else if (sub && strcasecmp(sub, "REQ") == 0) {
char *arg = strtok(NULL, " ");
send_raw(c, "CAP * NAK :%s", arg ? arg : "");
}
} else {
send_reply(c, "421 %s %s :Unknown command", c->nickname, cmd);
}
}
static void remove_client(Client *c) {
for (int i = 0; i < c->channel_count; ++i)
remove_client_from_channel(c, c->channels[i]);
if (clients == c) {
clients = c->next;
} else {
Client *prev = clients;
while (prev && prev->next != c) prev = prev->next;
if (prev) prev->next = c->next;
}
log_event("user disconnected: %s (%s)", c->nickname[0]?c->nickname:"<unreg>", c->host);
del_epoll(c->fd);
if (c->fd >= 0) close(c->fd);
c->fd = -1;
free(c->channels);
free(c);
}
static void accept_new_client() {
struct sockaddr_in addr;
socklen_t len = sizeof(addr);
int fd = accept(listen_fd, (struct sockaddr*)&addr, &len);
if (fd < 0) return;
set_nonblocking(fd);
Client *c = calloc(1, sizeof(Client));
c->fd = fd;
snprintf(c->host, sizeof(c->host), "%s", inet_ntoa(addr.sin_addr));
c->next = clients;
clients = c;
add_epoll(fd, EPOLLIN, c);
log_event("user connected: %s", c->host);
}
static int parse_args(int argc, char **argv, int *port, char **pass, int *log_enabled) {
*port = 6667;
*pass = NULL;
*log_enabled = 0;
for (int i = 1; i < argc; ++i) {
if (strcmp(argv[i], "--port") == 0 && i+1 < argc) {
*port = atoi(argv[++i]);
} else if (strcmp(argv[i], "--password") == 0 && i+1 < argc) {
*pass = argv[++i];
} else if (strcmp(argv[i], "--log") == 0) {
*log_enabled = 1;
}
}
return 0;
}
int main(int argc, char **argv) {
int port;
parse_args(argc, argv, &port, &password, &logging_enabled);
listen_fd = socket(AF_INET, SOCK_STREAM, 0);
int opt = 1;
setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
struct sockaddr_in addr = {0};
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = INADDR_ANY;
addr.sin_port = htons(port);
bind(listen_fd, (struct sockaddr*)&addr, sizeof(addr));
listen(listen_fd, 128);
set_nonblocking(listen_fd);
epoll_fd = epoll_create1(0);
if (epoll_fd < 0) fatal("epoll_create1");
add_epoll(listen_fd, EPOLLIN, NULL);
printf("rirc server (epoll) listening on port %d\n", port);
struct epoll_event events[MAX_EVENTS];
while (1) {
int n = epoll_wait(epoll_fd, events, MAX_EVENTS, 1000);
for (int i = 0; i < n; ++i) {
if (events[i].data.ptr == NULL) {
accept_new_client();
continue;
}
Client *c = (Client*)events[i].data.ptr;
int client_removed = 0;
if (events[i].events & EPOLLIN) {
int r = read(c->fd, c->readbuf + c->readbuf_len, sizeof(c->readbuf) - c->readbuf_len - 1);
if (r <= 0) { remove_client(c); client_removed = 1; continue; }
c->readbuf_len += r;
c->readbuf[c->readbuf_len] = 0;
char *line, *saveptr;
line = strtok_r(c->readbuf, "\r\n", &saveptr);
while (line) {
printf("%s\n",line);
handle_line(c, line);
line = strtok_r(NULL, "\r\n", &saveptr);
}
c->readbuf_len = 0;
}
if (!client_removed && (events[i].events & EPOLLOUT) && c->writebuf_len > 0) {
int w = write(c->fd, c->writebuf, c->writebuf_len);
if (w > 0) {
memmove(c->writebuf, c->writebuf+w, c->writebuf_len-w);
c->writebuf_len -= w;
}
}
if (!client_removed && c->fd >= 0) {
uint32_t new_events = EPOLLIN;
if (c->writebuf_len > 0) new_events |= EPOLLOUT;
mod_epoll(c->fd, new_events, c);
}
}
}
return 0;
}