// A standalone IRC like chat // Configured via commmand line arguments // Based of of https://modern.ircdocs.horse/ as of 2026 // // TODO Allow admin created persisant channels // TODO Implement IRCv6 style chat history #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include // Linux only i'm afraid #define ERROR(name) {if (err == -1) {perror(name); exit(1);}} #define DBG if (DEBUG_MODE) #define VERSION "0.1" // Set by command line parser at program start. // These are default settings char* HOSTNAME = "i.have.no.name!"; int PORT = 6667; int SPY_MODE = 0; int DEBUG_MODE = 0; char* MOTD = "+------------------------------------------------------------------------------+\n" "| #### ## ## ### #### |\n" "| #### ## ## # # ## [A standalone IRC-Like chat server] |\n" "| ## ##### ##### ## ### [Version 0.1 ] |\n" "| #### ## ## ## ## ## ## # [(c) maurycy 2026 ] |\n" "| #### ## ## ## ## ## ## ### |\n" "| |\n" "| |\n" "| Rules: |\n" "| 1. Don't be a dick |\n" "| 2. Don't post anything illegal (or about your crimes) |\n" "| |\n" "| Basic information: |\n" "| - Nicknames are assigned on a first come basis. |\n" "| |\n" "| - Channels are created when joined. The first user to join a channel will |\n" "| be granted 'operator' status, and can set the topic, moderation settings |\n" "| and forcibly remove other users. Operators can also grant these privileges |\n" "| to other users. To avoid lockouts, if no users have operator, the oldest |\n" "| member is appointed to the position. |\n" "| |\n" "| - Messages can be directed towards a user by nickname, or to a channel |\n" "| |\n" "| Mode flags ... |\n" "| ... for users: |\n" "| +i Hidden from user listings unless you share channel with the caller. |\n" "| |\n" "| ... for channels: |\n" "| +s Hidden from channel listings |\n" "| +m Moderated. Users will need +v to talk. |\n" "| +t Protected topic. Requires +o to change |\n" "| |\n" "| ... channel member flags: |\n" "| +o ('@' prefix) Channel operator/founder. |\n" "| +v ('+' prefix) Allowed to talk when moderated |\n" "| |\n" "| Note: messages and chats are NOT end-to-end encrypted. |\n" "| |\n" "| (for admins: this message can be changed with the --motd flag) |\n" "+------------------------------------------------------------------------------+\n"; // Address formatting function. The string space is satically // allocated. Don't free. Future calls will overwrite it. char* format_address(struct sockaddr_in6 addr); /////////////////////////////////////////////////////////////////////////////// // // // Data storage // // // /////////////////////////////////////////////////////////////////////////////// struct timespec start, now; uint64_t uptime; #define N_CAP 5 char* cap_list[N_CAP] = { "multi-prefix", // Implemented "message-tags", // Not yet "server-time", // Not yet "draft/chathistory", "draft/event-playback", }; #define CAP_MULTI_PREFIX 0 struct IRC_User { int connection; struct sockaddr_in6 addr; // Read buffer int line_len; char linebuffer[512]; // Nickname int has_nick; char nick[10]; // Set when the user issues a quit command char* quit_reason; struct IRC_User_modes { uint8_t invis; } modes; // Temporary value used to implement invisible users and message propagation. // Set by calc_known(nick) if the user is known by the nickname, otherwise leared int known_flag; // If set, the registration banner will be derered until after the CAP END // message is recieved, as per // https://ircv3.net/specs/extensions/capability-negotiation int register_defer; // Outcome of CAP negotation uint8_t support_flags[N_CAP]; }; struct IRC_Chan { char name[10]; int member_count; char* topic; struct IRC_Chan_modes { uint8_t secret; uint8_t topic; uint8_t moderated; } modes; struct IRC_Chan_member { struct IRC_Member_modes { uint8_t oper; uint8_t voice; } modes; struct IRC_User* user; } members[]; }; int n_users = 0; struct IRC_User** users = NULL; int n_chan = 0; struct IRC_Chan** chans = NULL; struct IRC_Chan* get_channel(char* name) { for (int i = 0; i < n_chan; i++) { if (strcmp(chans[i]->name, name) == 0) return chans[i]; } return NULL; } struct IRC_User* get_user(char* name) { for (int i = 0; i < n_users; i++) { if (!users[i]->has_nick) continue; if (strcmp(users[i]->nick, name) == 0) return users[i]; } return NULL; } struct IRC_Chan_member* get_user_entry(struct IRC_Chan* chan, char* name) { for (int i = 0; i < chan->member_count; i++) { assert(chan->members[i].user->has_nick); if (strcmp(chan->members[i].user->nick, name) == 0) return &chan->members[i]; } return NULL; } char prefix_buffer[10]; // Returns a pointer into a static buffer char* get_prefix(struct IRC_Member_modes modes, int multi_prefix) { int i = 0; if (modes.oper) prefix_buffer[i++] = '@'; if (modes.voice) prefix_buffer[i++] = '+'; prefix_buffer[i++] = 0; // If the client doesn't support multi-prefix, truncate to one charater. if (!multi_prefix) prefix_buffer[1] = 0; return prefix_buffer; } /////////////////////////////////////////////////////////////////////////////// // // // Sending messages // // // /////////////////////////////////////////////////////////////////////////////// void message(struct IRC_User* dest, char* source, char* command, char* params[], int count, char* trailing) { int fd = dest->connection; dprintf(fd, ":%s %s ", source, command); for (int i = 0; i < count; i++) { if ((i < count - 1) || trailing) { dprintf(fd, "%s ", params[i]); } else { dprintf(fd, "%s", params[i]); } } if (trailing) dprintf(fd, ":%s", trailing); dprintf(fd, "\r\n"); } void send_reply(struct IRC_User* user, char* code, char* data) { int fd = user->connection; dprintf(fd, ":%s %s %s", HOSTNAME, code, user->has_nick?user->nick:"*"); if (data) dprintf(fd, " %s", data); dprintf(fd, "\r\n"); } void broadcast_to_channel(char* nickname, char* chan, char* command, char* target, char* data) { struct IRC_Chan* c = get_channel(chan); assert(c); for (int i = 0; i < c->member_count; i++) { int fd = c->members[i].user->connection; dprintf(fd, ":%s %s", nickname, command); if (target) dprintf(fd, " %s", target); if (data) dprintf(fd, " :%s", data); dprintf(fd, "\r\n"); } } void calc_known(char* nick1) { struct IRC_User* c = get_user(nick1); assert(c); // Initialize knowlage data to zeros for (int i = 0; i < n_users; i++) users[i]->known_flag = 0; c->known_flag = 1; // Someone always knows themselves // Find all channels that contain the interogating user for (int i = 0; i < n_chan; i++) { if (!get_user_entry(chans[i], nick1)) continue; // All the users in said channel are known to them. for (int e = 0; e < chans[i]->member_count; e++) { chans[i]->members[e].user->known_flag = 1; } } } void broadcast_to_known(char* nickname, char* command, char* target, char* data) { // Precompute knowlage data to save time in the loop calc_known(nickname); for (int i = 0; i < n_users; i++) { if (users[i]->known_flag) { int fd = users[i]->connection; dprintf(fd, ":%s %s", nickname, command); if (target) dprintf(fd, " %s", target); if (data) dprintf(fd, " :%s", data); dprintf(fd, "\r\n"); } } } void dump_state() { printf("Channel list:\n"); for (int i = 0; i < n_chan; i++) { printf("\t%s (%d users):\n", chans[i]->name, chans[i]->member_count); printf("\t\tSecret? %d\n", chans[i]->modes.secret); for (int e = 0; e < chans[i]->member_count; e++) { struct IRC_Chan_member user = chans[i]->members[i]; printf("\t\tMember %s (oper %d)\n", user.user->nick, user.modes.oper); } } printf("User list:\n"); for (int i = 0; i < n_users; i++) { char* nick = users[i]->has_nick?users[i]->nick:"(anon)"; printf("\t%s (invis %d) (from %s)\n", nick, users[i]->modes.invis, format_address(users[i]->addr)); } } // General housekeeping void channel_cleanup() { DBG printf("Chan cleanup before:\n"); DBG dump_state(); // Disband empty channels for (int i = 0; i < n_chan; i++) { if (chans[i]->member_count == 0) { if (chans[i]->topic) free(chans[i]->topic); free(chans[i]); memmove(&chans[i], &chans[i + 1], sizeof(struct IRC_Chan*) * (n_chan - 1 - i)); n_chan--; } } // Ensure all channels have an operator for (int i = 0; i < n_chan; i++) { int op_count = 0; for (int e = 0; e < chans[i]->member_count; e++) { op_count += chans[i]->members[e].modes.oper; } DBG printf("OPCOUNT: %d\n", op_count); if (op_count == 0) { // Appoint the oldest user as operator chans[i]->members[0].modes.oper = 1; char msg[512]; snprintf(msg, 512, "%s +o %s", chans[i]->name, chans[i]->members[0].user->nick); broadcast_to_channel(HOSTNAME, chans[i]->name, "MODE", msg, NULL); } } DBG printf("Chan cleanup after:\n"); DBG dump_state(); } /////////////////////////////////////////////////////////////////////////////// // // // Command implementation // // // /////////////////////////////////////////////////////////////////////////////// char* help_string[] = { ":- This is a simple real-time chat service based on IRC. ", ":- Unlike most social media, no persistant state is stored. This", ":- includes message history. (actually deleted, not big-tech \"deleted\")", ":- ", ":- NOTE: communication is NOT ENCRYPTED in transit. Anyone on your", ":- network may eveasdropping on it. ", ":- ", ":- You may either participate using a dedicated IRC client, or", ":- by directly sending commands via telnet/netcat/etc. For the second", ":- option, send \"HELP2\\r\\n\" for details on the protocol.", ":- " }; char* help_string2[] = { ":- Message format:", ":- ", ":- Each message sent by the client should be prefixed by a command,", ":- followed by a space delimited list of parameters. If a parameter", ":- is prefixed with a colon ':', it's assumed to continue until the", ":- end of the line. This is useful for freeform text (like this)", ":- ", ":- Messages sent by the server use the same format, but are prefixed", ":- with \": \", which indicates their origin. The server will", ":- also use three digit numeric responses in place of a command. These", ":- indicate that the message was internally generated in reply to something", ":- you sent.", ":- ", ":- Quickstart guide:", ":- ", ":- NICK ", ":- JOIN #chat", ":- PRIVMSG #chat :Hello there!", ":- ", ":- Permission system:", ":- ", ":- Channel operator (+o, @ prefix): Can change the channel's modes,", ":- and set the topic. Automatically assigned to the first user to JOIN", ":- a channel, or to the most senior user if no operators are left.", ":- ", ":- Operators can also grant/revoke operator status to other users.", ":- ", ":- Commands:", ":- ", ":- HELP/HELP2", ":- Display this message. ", ":- ", ":- NICK ", ":- Set your username/display name. Must be 9 bytes or less,", ":- must be alphanumeric, and must not begin with a digit", ":- Setting a nickname is required for most commands.", ":- ", ":- Nickname changes will be forwarded to any user you share", ":- a channel with/", ":- ", ":- USER 0 * :", ":- Set extra user data. Currently unused.", ":- ", ":- PING ", ":- Generate a PONG message for testing", ":- ", ":- QUIT :", ":- Close your connection. Any channels that drop down to zero users will", ":- be deleted.", ":- ", ":- Reason will be forwarded to any user you share a channel with.", ":- ", ":- JOIN #", ":- Join (or create) a channel. Messages sent to a channel will be sent", ":- to all users that have joined it. If you are the first user to join an", ":- empty channel, you will be given operator permissions.", ":- ", ":- Reason will be forwarded to any members of the channel.", ":- ", ":- PART # [:]", ":- ", ":- Leaves a channel. Reason will be broadcast to other users in the channel. ", ":- ", ":- PRIVMSG [,[,]] :", ":- Send a message to users or channels (name begins with #).", ":- If sending to a channel, it will not be sent to you.", ":- ", ":- LIST", ":- List all channels and and number of currently JOIN-ed users", ":- ", ":- NAMES [#]", ":- List users, optionally in a channel. Channel listings include", ":- membership prefixes (@ = channel operator). ", ":- ", ":- WHO [#]", ":- Same as NAMES, but with different (worse) syntax", ":- ", ":- MODE [ []]", ":- Set modes for a user or channel. Mode string must start with a.", ":- '+' (set) or '-' (clear) followed by a number of flags. Channel", ":- membership modes must have a param argument specifing what user ", ":- they apply to.", ":- ", ":- If no mode is specified, the server will return the target's mode.", ":- ", ":- Reason will be forwarded to any members of the channel.", ":- ", ":- TOPIC :", ":- Set a channel's topic. Require operator if the +t mode is set.", }; // Returns null if it's ok, and a error message otherwise char* validate_name(char* name) { if (strlen(name) == 0) { return ": Come on, at least give me one byte!"; } if (strlen(name) > 9) { return ": Name is too long. Maxium is 9 bytes."; } int bad = 0; for (int i = 0; name[i]; i++) { uint8_t c = name[i]; // Removes control charaters, spaces and most symbols if (c < 48) bad = 1; // Removes digits for the first charater if (i == 0 && c < 65) bad = 1; // Delete bytes if (c == 0x7f) bad = 1; } if (bad) { char* msg = ": Nickname contains invalid charaters."; return msg; } return 0; } // Read the command or a param from the message, updating the pointer to the end. // End of string is parsed as an empty parameter. // Returns a heap allocated value. char* message_split(char** msg) { if (**msg == ':') { // Trailing parameter, copy from after the colon until end of string. char* param = malloc(strlen(*msg) + 1); // This is just a copy, but I hate strncpy! snprintf(param, strlen(*msg), "%s", *msg + 1); // Move the head pointer to the end of string *msg += strlen(*msg); return param; } else { // Space delimited parameter. Ends at either a space or end of line. char* space = strchr(*msg, ' '); if (!space) space = *msg + strlen(*msg); // Extract the parameter int len = space - *msg + 1; char* param = malloc(len); snprintf(param, len, "%s", *msg); // Move the parsing head to the delimiter, or past it if it was a space; *msg += len - 1; if (**msg == ' ') { (*msg)++; } return param; } } void register_banner(struct IRC_User* user) { char buffer[512]; // Used for sending replys #define PRINTF(...) snprintf(buffer, 512, __VA_ARGS__) PRINTF(": Wecome to the %s. See MOTD for more details.", HOSTNAME); send_reply(user, "001", buffer); PRINTF(": Your host is chat.c, version %s", VERSION); send_reply(user, "002", buffer); uint64_t seconds = uptime % 60; uint64_t minutes = (uptime/60) % 60; uint64_t hours = (uptime/60/60) % 60; uint64_t days = (uptime/60/60/24) % 60; PRINTF(": The server has been running for a %ld days, %ldh %ldm %lds", days, hours, minutes, seconds); send_reply(user, "003", buffer); PRINTF("CHANNELLEN=9 NICKLEN=9 TOPICLEN=100 LINELEN=512 :Supported features"); send_reply(user, "005", buffer); PRINTF("CHANMODES=tsm CHANTYPES=# TARGMAX=WHO:1,NAMES:1 SAFELIST"); send_reply(user, "005", buffer); PRINTF("PREFIX=@+ "); send_reply(user, "005", buffer); } // Returns 0 normally, 1 if the user is quitting int handle_command(char* line, struct IRC_User* user) { // Update timing data clock_gettime(CLOCK_MONOTONIC, &now); uptime = (now.tv_sec - start.tv_sec); // Debug logging if (SPY_MODE) { printf("[%ld] %s\t%s\n", uptime, format_address(user->addr), line); } int is_quit = 0; // Set if the user wishes to disconect char* head = line; // Next parameter to parse char buffer[512]; // Used for sending replys #define PRINTF(...) snprintf(buffer, 512, __VA_ARGS__) char* command = message_split(&head); // Case insensitivty: Uppercase command before comparing for (int i = 0; command[i]; i++) { if (command[i] >= 'a' && command[i] <= 'z') { command[i] -= 'a' - 'A'; } } // Require a nickname for most commands, except NICK (to avoid deadlock), // HELP (for user friendlyness) and QUIT (because that would be silly). if ( strcmp(command, "NICK") != 0 && strncmp(command, "HELP", 4) != 0 && strcmp(command, "QUIT") != 0 && strcmp(command, "CAP") != 0 ) { if (!user->has_nick) { send_reply(user, "451", ": What should I call you? Hint: NICK "); free(command); return 0; } } if (strcmp(command, "CAP") == 0) { // IRCv3 capability list char* subcommand = message_split(&head); if (strcmp(subcommand, "LS") == 0) { // List user->register_defer = 1; // Clear the user's nickname so send_reply must use the placeholder for (int i = 0; i < N_CAP; i++) { PRINTF(":%s", cap_list[i]); send_reply(user, "CAP * LS", buffer); } } else if (strcmp(subcommand, "REQ") == 0) { // Request user->register_defer = 1; // Split by spaces char* arg = message_split(&head); char* head2 = arg; char* chunk = message_split(&head2); while (strlen(chunk)) { int support = 0; for (int i = 0; i < N_CAP; i++) { if (strcmp(chunk, cap_list[i]) == 0) { support = 1; user->support_flags[i] = 1; } } send_reply(user, support?"CAP * ACK":"CAP * NAK", chunk); // (split cont) free(chunk); chunk = message_split(&head2); } free(arg); } else if (strcmp(subcommand, "END") == 0) { // End of negotation, time to send banners. // ... but only if the user has registered in the meantime if (user->register_defer && user->has_nick) { register_banner(user); user->register_defer = 0; } } free(subcommand); } else if (strcmp(command, "PING") == 0) { // // ######################################### // # BASELINE IRC # // ######################################### // char* token = message_split(&head); message(user, HOSTNAME, "PONG", &HOSTNAME, 1, token); free(token); } else if (strcmp(command, "QUIT") == 0) { char* reason = message_split(&head); is_quit = 1; user->quit_reason = reason; } else if (strcmp(command, "MOTD") == 0) { char* motd_head = MOTD; send_reply(user, "375", ": "); // Read lines from MOTD file while (strchr(motd_head, '\n')) { int len = strchr(motd_head, '\n') - motd_head; char line[len + 3]; snprintf(line, len + 3, ": %s", motd_head); // prefix with a colon motd_head = strchr(motd_head, '\n') + 1; // ... and send! send_reply(user, "372", line); } send_reply(user, "376", ": "); } else if (strcmp(command, "HELP") == 0) { char* msg; send_reply(user, "375", ":- General Help:"); for (int i = 0; i < sizeof(help_string)/sizeof(help_string[0]); i++) { msg = help_string[i]; send_reply(user, "372", msg); } send_reply(user, "376", ": That's enough kindness for today."); } else if (strcmp(command, "HELP2") == 0) { char* msg; send_reply(user, "375", ":- Protocol help:"); for (int i = 0; i < sizeof(help_string2)/sizeof(help_string2[0]); i++) { msg = help_string2[i]; send_reply(user, "372", msg); } send_reply(user, "376", ": "); } else if (strcmp(command, "NICK") == 0) { char* nick = message_split(&head); // Check length and charset char* error_message = validate_name(nick); if (error_message) { PRINTF("%s %s", nick, error_message); send_reply(user, "432", buffer); goto nick_done; } // Check for collisions if (get_user(nick)) { PRINTF("%s : There's already a %s on this server. Please pick a different name.", nick, nick); send_reply(user, "436", buffer); goto nick_done; } // If we are still here, the nickname is ok. if (user->has_nick) { // Nickname change broadcast_to_known(user->nick, "NICK", nick, NULL); snprintf(user->nick, sizeof(user->nick), "%s", nick); } else { snprintf(user->nick, sizeof(user->nick), "%s", nick); user->has_nick = 1; if (!user->register_defer) register_banner(user); } nick_done: free(nick); } else if (strcmp(command, "USER") == 0) { // Do nothing. } else if (strcmp(command, "JOIN") == 0) { char* chan = message_split(&head); // Validate if (chan[0] != '#') { PRINTF("%s : Channel names must start with a '#'", chan); send_reply(user, "403", buffer); goto join_done; } else { char* error_message = validate_name(chan + 1); if (error_message) { PRINTF("%s %s", chan, error_message); send_reply(user, "403", buffer); goto join_done; } } // Find or create channel int index = -1; for (int i = 0; i < n_chan; i++) { if (strcmp(chan, chans[i]->name) == 0) index = i; } if (index == -1) { // Does not exist, yet... struct IRC_Chan* new = malloc(sizeof(struct IRC_Chan)); snprintf(new->name, 10, "%s", chan); new->member_count = 0; // Zero modes memset(&new->modes, 0, sizeof(struct IRC_Chan_modes)); new->topic = NULL; // ... and add to list index = n_chan; n_chan += 1; chans = realloc(chans, sizeof(struct IRC_Chan) * n_chan); chans[index] = new; } // Handle join chans[index]->member_count++; int count = chans[index]->member_count; chans[index] = realloc(chans[index], sizeof(struct IRC_Chan) + sizeof(struct IRC_Chan_member)*count); struct IRC_Chan_member* us = &chans[index]->members[count - 1]; us->user = user; us->modes.oper = count == 1; // Grant op if this is the only user in the channel. us->modes.voice = 0; broadcast_to_known(user->nick, "JOIN", chan, NULL); // Send listing. // No need to check for invisible users here because the originating user // will always share a channel with them (the one that is being JOINed) for (int i = 0; i < count; i++) { char* prefix = get_prefix(chans[index]->members[i].modes, user->support_flags[CAP_MULTI_PREFIX]); char sym = chans[index]->modes.secret?'@':'='; PRINTF("%c %s :%s%s", sym, chan, prefix, chans[index]->members[i].user->nick); send_reply(user, "353", buffer); } PRINTF("%s : End of names list", chan); send_reply(user, "366", buffer); if (get_channel(chan)->topic) { PRINTF("%s :%s", chan, get_channel(chan)->topic); send_reply(user, "332", buffer); } join_done: free(chan); } else if (strcmp(command, "PART") == 0) { char* chan = message_split(&head); char* reason = message_split(&head); // Error checking struct IRC_Chan* channel = get_channel(chan); if (!channel) { PRINTF("%s : That channel does not exist", chan); send_reply(user, "403", buffer); goto part_done; } struct IRC_Chan_member* entry = get_user_entry(channel, user->nick); if (!entry) { PRINTF("%s : There's no need", chan); send_reply(user, "442", buffer); goto part_done; } // Send notification to other users broadcast_to_channel(user->nick, chan, "PART", chan, reason); // Actually remove them ... for (int i = 0; i < channel->member_count; i ++) { if (channel->members[i].user == user) { memmove( &channel->members[i], &channel->members[i + 1], sizeof(struct IRC_Chan_member) * (channel->member_count - 1 - i) ); channel->member_count--; } } // ... which may lead to empty or operator-less channels channel_cleanup(); part_done: free(chan); free(reason); } else if (strcmp(command, "LIST") == 0) { send_reply(user, "321", ": Channel name, #users, topic"); for (int i = 0; i < n_chan; i++) { // Omit secret channels that the user isn't in. if (chans[i]->modes.secret) { if (!get_user_entry(chans[i], user->nick)) continue; } PRINTF("%s %d :%s", chans[i]->name, chans[i]->member_count, chans[i]->topic); send_reply(user, "322", buffer); } send_reply(user, "323", ": End of LIST"); } else if (strcmp(command, "WHO") == 0 || strcmp(command, "NAMES") == 0) { char* arg = message_split(&head); // Precompute user knowlage data calc_known(user->nick); if (strlen(arg)) { // Channel listing struct IRC_Chan* c = get_channel(arg); // Fail if the channel doesn't exist if (!c) { PRINTF("%s : Channel does not exist", arg); send_reply(user, "404", buffer); goto who_done; } for (int i = 0; i < c->member_count; i++) { // Don't have to check has_nick because users can't join channels without registering char* nick = c->members[i].user->nick; char* flags = get_prefix(c->members[i].modes, user->support_flags[CAP_MULTI_PREFIX]); // Exclude unregistered users if (!users[i]->has_nick) continue; // Exclude unknown invisible users if (c->members[i].user->modes.invis) { if (!c->members[i].user->known_flag) continue; } if (command[0] == 'W') { // WHO format PRINTF("%s %s * %s %s H%s :0 %s", arg, nick, HOSTNAME, nick, flags, nick); send_reply(user, "352", buffer); } else { // NAMES format PRINTF("= %s :%s%s", arg, flags, nick); send_reply(user, "353", buffer); } } } else { // Gobal who for (int i = 0; i < n_users; i++) { if (!users[i]->has_nick) continue; // Exclude unkown invisible users if (users[i]->modes.invis) { if (!users[i]->known_flag) continue; } if (command[0] == 'W') { PRINTF("* %s * %s %s H :0 %s", users[i]->nick, HOSTNAME, users[i]->nick, users[i]->nick); send_reply(user, "352", buffer); } else { // NAMES format PRINTF("= %s :%s%s", "*", "", users[i]->nick); send_reply(user, "353", buffer); } } } if (command[0] == 'W') { send_reply(user, "315", ":End of WHO list"); } else { if (strlen(arg)) { PRINTF("%s : End of names list", arg); } else { PRINTF("* : End of names list"); } send_reply(user, "366", buffer); } who_done: free(arg); } else if (strcmp(command, "PRIVMSG") == 0) { char* target = message_split(&head); char* message_text = message_split(&head); if (!strlen(target)) { send_reply(user, "461", "PRIVMSG : No target specified."); goto privmsg_done; } if (!strlen(message_text)) { send_reply(user, "412", ": Empty message."); goto privmsg_done; } // Handle csv list. For now, truncate at comma char* comma; csv_split: comma = strchr(target, ','); if (comma) *comma = 0; if (target[0] == 0) { send_reply(user, "411", ": To whom?"); goto privmsg_done; } if (target[0] == '#') { // Find matching channel struct IRC_Chan* c = get_channel(target); // Fail if the channel doesn't exist if (!c) { PRINTF("%s : Channel does not exist. Hint: JOIN %s", target, target); send_reply(user, "404", buffer); goto privmsg_done; } if (c->modes.moderated) { struct IRC_Chan_member* us = get_user_entry(c, user->nick); // If moderated, the user either needs +o or +v to chat if (!us->modes.oper && !us->modes.voice) { PRINTF("%s : Channel is moderated", target); send_reply(user, "404", buffer); goto privmsg_done; } } // Send to all users for (int i = 0; i < c->member_count; i++) { // We are not supposed to send it back to the originating user if (c->members[i].user == user) continue; message(c->members[i].user, user->nick, "PRIVMSG", &target, 1, message_text); } } else { // Send to user for (int i = 0; i < n_users; i++) { if (!users[i]->has_nick) continue; if (strcmp(users[i]->nick, target) == 0) { message(users[i], user->nick, "PRIVMSG", &target, 1, message_text); } } } // If there's extra targerts, advance past the comma to send to them. if (comma) { target = comma + 1; goto csv_split; } privmsg_done: free(target); free(message_text); } else if (strcmp(command, "MODE") == 0) { char* target = message_split(&head); char* modes = message_split(&head); char* param = message_split(&head); if (*modes == 0) { // Read operation if (*target == '#') { // Read channel modes struct IRC_Chan* c = get_channel(target); char* secret = c->modes.secret?"s":""; char* mod = c->modes.moderated?"m":""; PRINTF("%s +%s%s", target, secret, mod); send_reply(user, "324", buffer); } else { // Read user mode if (strcmp(target, user->nick)) { send_reply(user, "502", ": Can't get the modes of other users"); goto modes_done; } char* invis = user->modes.invis?"i":""; PRINTF("%s +%s", target, invis); send_reply(user, "221", buffer); } } else { // Change modes if (*modes != '-' && *modes != '+') { send_reply(user, "501", ": Mode strings must start with a + or -"); goto modes_done; } uint8_t set = 0; uint8_t clear = 0; if (*modes == '+') set = 1; if (*modes == '-') clear = 1; if (*target == '#') { struct IRC_Chan* c = get_channel(target); if (!c) { PRINTF("%s : No such channel", target); send_reply(user, "403", target); goto modes_done; } // Permissions check for channel operations struct IRC_Chan_member* actor = get_user_entry(c, user->nick); if (!actor->modes.oper) { PRINTF("%s : You are not allowed to change the mode of that channel", target); send_reply(user, "482", buffer); goto modes_done; } if (strlen(param)) { // Set channel membership modes struct IRC_Chan_member* u = get_user_entry(c, param); if (!u) { PRINTF("%s : That user is not in the channel", param); send_reply(user, "401", buffer); goto modes_done; } for (int i = 1; modes[i]; i++) { char mode = modes[i]; if (mode == 'o') { u->modes.oper &= !clear; u->modes.oper = set; } if (mode == 'v') { u->modes.voice &= !clear; u->modes.voice = set; } } // Propagate membership changes to everyone in that channel if (strlen(modes)) { PRINTF("%s %s %s", target, modes, param); broadcast_to_channel(user->nick, target, "MODE", buffer, NULL); } } else { // Set channel modes for (int i = 1; modes[i]; i++) { char mode = modes[i]; if (mode == 's') { c->modes.secret &= !clear; c->modes.secret = set; } if (mode == 't') { c->modes.topic &= !clear; c->modes.topic = set; } if (mode == 'm') { c->modes.moderated &= !clear; c->modes.moderated = set; } } // Propagate mode changes to everyone in that channel if (strlen(modes)) { PRINTF("%s %s %s", target, modes, param); broadcast_to_channel(user->nick, target, "MODE", buffer, NULL); } } } else { // Set user modes struct IRC_User* u = get_user(target); if (strcmp(target, user->nick)) { send_reply(user, "502", ": Can't change the mode of other users"); goto modes_done; } for (int i = 1; modes[i]; i++) { char mode = modes[i]; if (mode == 'i') { u->modes.invis &= !clear; u->modes.invis = set; } } // Propagate user mode changes to known users if (strlen(modes)) { broadcast_to_known(user->nick, "MODE", line + 5, NULL); } } } // Mode operations may lead to operator-less channels channel_cleanup(); modes_done: free(target); free(modes); free(param); } else if (strcmp(command, "TOPIC") == 0) { char* target = message_split(&head); char* topic = message_split(&head); struct IRC_Chan* c = get_channel(target); if (!c) { PRINTF("%s : Non-existant channels cannot have topics", target); send_reply(user, "403", buffer); goto topic_done; } if (!strlen(topic)) { // Read topic if (!c->topic) { PRINTF("%s : No topic set", target); send_reply(user, "331", buffer); } else { PRINTF("%s : %s", target, topic); send_reply(user, "332", buffer); } } else { // Set topic int allowed = 1; if (c->modes.topic) { struct IRC_Chan_member* us = get_user_entry(c, user->nick); if (!us->modes.oper) allowed = 0; } if (!allowed) { PRINTF("%s : You aren't allowed to change the topic", target); send_reply(user, "482", buffer); goto topic_done; } if (strlen(topic) > 100) { PRINTF("%s : Topic is too long", target); send_reply(user, "417", buffer); goto topic_done; } c->topic = topic; broadcast_to_channel(user->nick, target, "TOPIC", target, topic); topic = NULL; } topic_done: free(target); if (topic) free(topic); } else { PRINTF("%s : Unknown command. Hint: 'HELP\\r\\n'", command); send_reply(user, "421", buffer); } free(command); return is_quit; } void handle_disconnect(struct IRC_User* user) { DBG printf("USER QUIT\n"); // Called from the main loop, right before a user // is removed from the user list. if (user->has_nick) { if (user->quit_reason) { broadcast_to_known(user->nick, "QUIT", NULL, user->quit_reason); } else { broadcast_to_known(user->nick, "QUIT", NULL, "disconnected"); } } // Remove from channels for (int i = 0; i < n_chan; i++) { struct IRC_Chan* c = chans[i]; for (int e = 0; e < c->member_count; e++) { if (c->members[e].user == user) { c->member_count --; memmove(&c->members[e], &c->members[e + 1], sizeof(struct IRC_Chan_member) * (c->member_count - e)); } } } channel_cleanup(); } void handle_new(struct IRC_User* user) { user->has_nick = 0; user->register_defer = 0; user->quit_reason = 0; user->modes.invis = 0; // Called from the main loop, after a user is added to the // user list. char* msg; msg = ": If you are new here, type HELP (and a newline). Otherwise, carry on."; send_reply(user, "001", msg); } /////////////////////////////////////////////////////////////////////////////// // // // Networking code // // // /////////////////////////////////////////////////////////////////////////////// void handle_connection(int fd, struct sockaddr_storage addr) { // Initialize user struct struct IRC_User* user = malloc(sizeof(struct IRC_User)); user->connection = fd; memcpy(&user->addr, &addr, sizeof(struct sockaddr_in6)); user->line_len = 0; DBG printf("New connection from %s\n", format_address(user->addr)); // Add to users list n_users++; users = realloc(users, sizeof(struct IRC_User*) * n_users); users[n_users - 1] = user; handle_new(user); } void handle_data(int fd) { // Find the user's data structure struct IRC_User* user = NULL; int user_i = 0; for (int i = 0; i < n_users; i++) { if (users[i]->connection == fd) { user_i = i; user = users[i]; } } assert(user); int success_count = 0; while (1) { int size = 256; uint8_t buffer[size]; int err; err = read(fd, &buffer, size); if (err > 0) { success_count++; // Got bytes from recv. Add to the user's line buffer, flushing out // to handle_command on a newline. for (int i = 0; i < err; i++) { if (buffer[i] == '\r' || buffer[i] == '\n') { if (user->line_len) { user->linebuffer[user->line_len] = 0; int quit = handle_command(user->linebuffer, user); // Disconnect if requested if (quit) goto quit; } user->line_len = 0; } else { user->linebuffer[user->line_len] = buffer[i]; if (user->line_len < 512 - 1) { // Always leave enough space for a null byte user->line_len++; } else { // TODO warn about truncated messages } } } } else { // Got an error. This could either mean that we are out of data // and should hand back control to the main loop, or that the user // has disconnected. // // Also, if we got an event with no data, that means the user has hung up. if ((errno == EAGAIN || errno == EWOULDBLOCK) && success_count != 0) { // Done with data return; } else { quit: // Problem with connection. // Tell everyone else that they are leaving: handle_disconnect(user); // Ensure connection is closed close(user->connection); // ... and remove from users list assert(n_users - 1 - user_i >= 0); memmove(&users[user_i], &users[user_i + 1], sizeof(struct IRC_User*) * (n_users - 1 - user_i)); n_users--; users = realloc(users, sizeof(struct IRC_User*) * n_users); // Done. return; } } } } char address_string[4*8+7+2+1]; char* format_address(struct sockaddr_in6 addr) { uint8_t* bytes = addr.sin6_addr.s6_addr; // IPv4 addresses are packed as 10 bytes of zeros, followed by 2x 0xff and // real address: int is_ipv4 = 1; for (int i = 0; i < 10; i++) { if (bytes[i]) is_ipv4 = 0; } if (bytes[10] != 0xff) is_ipv4 = 0; if (bytes[11] != 0xff) is_ipv4 = 0; if (is_ipv4) { snprintf(address_string, sizeof(address_string), "%d.%d.%d.%d", bytes[12], bytes[13], bytes[14], bytes[15]); return address_string; } int len = 0; for (int i = 0; i < 8; i ++) { len += snprintf( address_string + len, sizeof(address_string) - len, i<7?"%x:":"%x", bytes[i*2]*256 + bytes[i*2+1] ); } return address_string; } #define GET_ARG \ if (i + 1 >= argc) { \ printf("Flag %s requires an argument.\n", argv[i]); \ goto help; \ } \ i++; int main(int argc, char** argv) { // Parse command line arguments int err; int i = 1; while (i < argc) { char* flag = argv[i]; if (strcmp(flag, "--port") == 0) { GET_ARG; PORT = atoi(argv[i]); } else if (strcmp(flag, "--hostname") == 0) { GET_ARG; HOSTNAME = argv[i]; } else if (strcmp(flag, "--spy") == 0) { SPY_MODE = 1; } else if (strcmp(flag, "--verbose") == 0) { DEBUG_MODE = 1; } else if (strcmp(flag, "--motd") == 0) { GET_ARG; // Open file FILE* motd = fopen(argv[i], "r"); if (!motd) { perror("Opening motd file"); exit(1); } err = fseek(motd, 0L, SEEK_END); ERROR("Reading length of motd file") int len = ftell(motd) + 1; fseek(motd, 0L, SEEK_SET); // Read file MOTD = malloc(len); err = fread(MOTD, sizeof(char), len - 1, motd); ERROR("Reading motd file") // Null terminate MOTD[len - 1] = 0; } else if (strcmp(flag, "--help") == 0) { goto help; } else { printf("Unknown flag '%s'\n", argv[i]); printf("\n"); help: printf("Usage: %s [flags]\n", argv[0]); printf("\n"); printf("Flags:\n"); printf("\t--hostname Origin used for numeric replies and banner. Default: \"i.have.no.name\"\n"); printf("\t--port Port number used for listening. Default: 6667\n"); printf("\t--motd File containing the message of the day. \n"); printf("\t--spy Send recieved message to stdout.\n"); printf("\t--verbose Send debug data stdout.\n"); printf("\t--help Display this message\n"); exit(1); } i++; } // Don't exit on sigpipe. // The default behavoir would kill the server once a client disconnects. // We handle errors by QUIT-ing the user. signal(SIGPIPE, SIG_IGN); // On linux, IPv4 is transparently mapped into IPv6 addresses, so I don't // have to wory about dual stacking here. // See https://www.rfc-editor.org/info/rfc4291/#section-2.5.5.2 struct sockaddr_in6 servaddr = { .sin6_family = AF_INET6, .sin6_addr = in6addr_any, .sin6_port = htons(PORT) }; // Setup socket int sockfd = socket(AF_INET6, SOCK_STREAM, 0); int enable = 1; setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &enable, sizeof(int)); err = bind(sockfd, (struct sockaddr*)&servaddr, sizeof(struct sockaddr_in6)); ERROR("bind"); err = listen(sockfd, 5); ERROR("listen"); err = fcntl(sockfd, F_SETFL, O_NONBLOCK); ERROR("fcntl to set socket as nonblocking") // Setup epoll struct epoll_event ev, event; int epoll_fd = epoll_create1(0); // Add socket to epoll ev.events = EPOLLIN; ev.data.fd = sockfd; epoll_ctl(epoll_fd, EPOLL_CTL_ADD, sockfd, &ev); // Server's running, start counting uptime clock_gettime(CLOCK_MONOTONIC, &start); // Wait for events while (1) { epoll_wait: err = epoll_wait(epoll_fd, &event, 1, -1); // Exit on error, except for interupted wait. if (err == -1) { if (errno == EINTR) goto epoll_wait; ERROR("epoll_wait") } if (event.data.fd == sockfd) { // New connection struct sockaddr_storage their_addr; unsigned int sin_size; sin_size = sizeof(struct sockaddr_storage); int conn = accept(event.data.fd, (struct sockaddr *) &their_addr, &sin_size); err = fcntl(conn, F_SETFL, O_NONBLOCK); ERROR("fcntl to set connection as nonblocking") // Add to epoll ev.events = EPOLLIN | EPOLLET | EPOLLRDHUP | EPOLLERR; ev.data.fd = conn; err = epoll_ctl(epoll_fd, EPOLL_CTL_ADD, conn, &ev); ERROR("epoll_ctl for incomming connection."); // Register user handle_connection(conn, their_addr); } else { // New data, hand off handle_data(event.data.fd); } } }