commit 033456e94932a5fbeebfc212402648a55a69f7ae Author: Xnoe Date: Sat Jun 19 14:48:55 2021 +0100 Initial commit of XnoIRCd codebase diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4974e64 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +*.o +xnoircd diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..9b7f09d --- /dev/null +++ b/Makefile @@ -0,0 +1,28 @@ +FLAGS = -pthread + +xnoircd: user.o nameable.o channel.o connection.o ircserver.o irccommand.o userconnection.o main.o + g++ $(FLAGS) user.o nameable.o channel.o connection.o ircserver.o irccommand.o userconnection.o main.o -o $@ + +user.o: user.cpp user.h + g++ -g -c $*.cpp + +connection.o: connection.cpp connection.h + g++ -g -c $*.cpp + +ircserver.o: ircserver.cpp ircserver.h + g++ -g -c $*.cpp + +irccommand.o: irccommand.cpp irccommand.h + g++ -g -c $*.cpp + +userconnection.o: userconnection.cpp userconnection.h + g++ -g -c $*.cpp + +nameable.o: nameable.cpp nameable.h + g++ -g -c $*.cpp + +channel.o: channel.cpp channel.h + g++ -g -c $*.cpp + +main.o: main.cpp + g++ -g -c $*.cpp \ No newline at end of file diff --git a/channel.cpp b/channel.cpp new file mode 100644 index 0000000..f373983 --- /dev/null +++ b/channel.cpp @@ -0,0 +1,28 @@ +#include "channel.h" + +Channel::Channel(IRCServer* server, std::string channel_name): Nameable(server) { + this->channel_name = channel_name; + server->register_addressable_name(channel_name, this); +} + +Channel::~Channel() { + remove_addressable_name(channel_name); +} + +void Channel::send_direct(std::string message) { + send_relay(message); +} + +std::string Channel::get_topic() { + return topic; +} + +void Channel::set_topic(std::string topic) { + this->topic = topic; +} +bool Channel::has_topic() { + return (this->topic!=""); +} +NameableType Channel::what_are_you() { + return NT_Channel; +} \ No newline at end of file diff --git a/channel.h b/channel.h new file mode 100644 index 0000000..7e8a092 --- /dev/null +++ b/channel.h @@ -0,0 +1,26 @@ +#ifndef CHANNEL_H +#define CHANNEL_H + +#include +#include "nameable.h" +#include "ircserver.h" + +class Channel: public Nameable { +private: + std::string channel_name; + std::string topic; + +public: + Channel(IRCServer* server, std::string channel_name); + ~Channel(); + + void send_direct(std::string message); + + std::string get_topic(); + bool has_topic(); + void set_topic(std::string topic); + + NameableType what_are_you(); +}; + +#endif \ No newline at end of file diff --git a/connection.cpp b/connection.cpp new file mode 100644 index 0000000..80f7a65 --- /dev/null +++ b/connection.cpp @@ -0,0 +1,42 @@ +#include "connection.h" +#include + +Connection::Connection(int fd) { + this->fd = fd; + std::thread(&Connection::conn_hdlr, this).detach(); +} + +Connection::~Connection() { + close(this->fd); +} + +void Connection::conn_hdlr() { + buffer = new char[512]; + while (alive) { + if (read(this->fd, buffer, 511) == 0) { + destroy(); + return; + } + std::string message(buffer); + int index; + if ((index = message.find_first_of("\r\n")) == std::string::npos) + continue; + else + message = message.substr(0, index); + process_cmd(message); + } +} + +void Connection::process_cmd(std::string command) {} +void Connection::destroy() {} + +void Connection::kill() { + this->alive = false; +} + +void Connection::send(std::string to_write) { + to_write = to_write + "\r\n"; + const char* buffer = to_write.c_str(); + int len = strlen(buffer); + write(this->fd, buffer, len); +} \ No newline at end of file diff --git a/connection.h b/connection.h new file mode 100644 index 0000000..6bb4173 --- /dev/null +++ b/connection.h @@ -0,0 +1,29 @@ +#ifndef CONNECTION_H +#define CONNECTION_H + +#include +#include +#include +#include + +class Connection { +private: + int fd; + int alive = true; + char* buffer; + +public: + Connection(int fd); + ~Connection(); + + void conn_hdlr(); + + virtual void process_cmd(std::string command); + virtual void destroy(); + + void kill(); + + void send(std::string to_write); +}; + +#endif \ No newline at end of file diff --git a/irccommand.cpp b/irccommand.cpp new file mode 100644 index 0000000..130765b --- /dev/null +++ b/irccommand.cpp @@ -0,0 +1,94 @@ +#include "irccommand.h" + +#include + +IRCCommand::IRCCommand(std::string raw_command) { + if (raw_command[0] == '@') { + int index = raw_command.find_first_of(" "); + + std::string tag_section = raw_command.substr(1, index); + raw_command = raw_command.substr(index+1); + + // Todo: Further process tags down + } + + if (raw_command[0] != 0) + raw_command.substr(1); + else + return; + + if (raw_command[0] == ':') { + int index = raw_command.find_first_of(" "); + source = raw_command.substr(1, index); + raw_command = raw_command.substr(index+1); + } + + if (raw_command[0] != 0) + raw_command.substr(1); + else + return; + + int index = raw_command.find_first_of(" "); + command = raw_command.substr(0, index); + raw_command = raw_command.substr(index+1); + + if (raw_command[0] != 0) + raw_command.substr(1); + else + return; + + std::vector payloads; + index = raw_command.find_first_of(" "); + + while (index != std::string::npos && raw_command[0] != ':') { + payloads.push_back(raw_command.substr(0, index)); + raw_command = raw_command.substr(index + 1); + index = raw_command.find_first_of(" "); + } + + if (raw_command[0] == ':') + payloads.push_back(raw_command.substr(1)); + else + payloads.push_back(raw_command); + + this->payload = payloads; +} + +bool IRCCommand::has_source() { + return (source!=""); +} + +std::string IRCCommand::get_source() { + return source; +} + +bool IRCCommand::has_command() { + return (command!=""); +} + +std::string IRCCommand::get_command() { + return command; +} + +bool IRCCommand::has_payload() { + return payload.size() > 0; +} + +std::string IRCCommand::get_payload(int i) { + return payload.at(i); +} + +int IRCCommand::get_payload_count() { + return payload.size(); +} + +std::string IRCCommand::to_string() { + std::string myself; + if (has_source()) + myself += "Source: " + get_source() + "\n"; + if (has_command()) + myself += "Command: " + get_command() + "\n"; + for (std::string p : payload) + myself += "Parameters: " + p + "\n"; + return myself; +} \ No newline at end of file diff --git a/irccommand.h b/irccommand.h new file mode 100644 index 0000000..7b98d8b --- /dev/null +++ b/irccommand.h @@ -0,0 +1,33 @@ +#ifndef IRCCOMMAND_H +#define IRCCOMMAND_H + +#include +#include +#include + +class IRCCommand { +private: + std::unordered_map tags; + std::string source, command = ""; + std::vector payload; +public: + IRCCommand(std::string raw_command); + + bool has_source(); + + std::string get_source(); + + bool has_command(); + + std::string get_command(); + + bool has_payload(); + + std::string get_payload(int i); + + int get_payload_count(); + + std::string to_string(); +}; + +#endif \ No newline at end of file diff --git a/ircserver.cpp b/ircserver.cpp new file mode 100644 index 0000000..0846764 --- /dev/null +++ b/ircserver.cpp @@ -0,0 +1,30 @@ +#include "ircserver.h" + +IRCServer::IRCServer(std::string hostname) { + this->hostname = hostname; + this->nameable_map = {}; +} + +bool IRCServer::is_addressable_name_free(std::string name) { + return nameable_map.find(name) == nameable_map.end(); +} + +bool IRCServer::has_addressable_name(std::string name) { + return !is_addressable_name_free(name); +} + +void IRCServer::remove_addressable_name(std::string name) { + nameable_map.erase(name); +} + +void IRCServer::register_addressable_name(std::string name, Nameable* bind) { + nameable_map[name] = bind; +} + +Nameable* IRCServer::resolve_addressable_name(std::string name) { + return nameable_map[name]; +} + +std::string IRCServer::get_hostname() { + return hostname; +} \ No newline at end of file diff --git a/ircserver.h b/ircserver.h new file mode 100644 index 0000000..2bf7eba --- /dev/null +++ b/ircserver.h @@ -0,0 +1,24 @@ +#ifndef IRCSERVER_H +#define IRCSERVER_H + +#include +#include + +class Nameable; +class IRCServer { +private: + std::unordered_map nameable_map; + std::string hostname; +public: + + IRCServer(std::string); + + void register_addressable_name(std::string name, Nameable* bind); + bool is_addressable_name_free(std::string name); + bool has_addressable_name(std::string name); + void remove_addressable_name(std::string name); + Nameable* resolve_addressable_name(std::string name); + std::string get_hostname(); +}; + +#endif \ No newline at end of file diff --git a/main.cpp b/main.cpp new file mode 100644 index 0000000..c814949 --- /dev/null +++ b/main.cpp @@ -0,0 +1,42 @@ +#include +#include +#include + +#include "ircserver.h" +#include "userconnection.h" + +int main() { + int socketfd = socket(AF_INET, SOCK_STREAM, 0); + if (socketfd == 0) { + std::cout << "Socket Create Failed!\n" << std::flush; + return 1; + } + int opt = 1; + if (setsockopt(socketfd, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt))) { + std::cout << "Setting Socket Options Failed!\n" << std::flush; + return 1; + } + + struct sockaddr_in addr; + addr.sin_family = AF_INET; + addr.sin_addr.s_addr = INADDR_ANY; + addr.sin_port = htons(6667); + + unsigned int addrsize = sizeof(addr); + if (bind(socketfd, (struct sockaddr*)&addr, addrsize) < 0) { + std::cout << "Socket Bind Failed!\n" << std::flush; + return 1; + } + + if (listen(socketfd, 5) < 0) { + std::cout << "Socket Listen Failed!\n" << std::flush; + return 1; + } + + IRCServer* server = new IRCServer("xnoircd"); + + while (true) { + int sockfd = accept(socketfd, (struct sockaddr*)&addr, &addrsize); + new UserConnection(sockfd, server); + } +} \ No newline at end of file diff --git a/nameable.cpp b/nameable.cpp new file mode 100644 index 0000000..84692ae --- /dev/null +++ b/nameable.cpp @@ -0,0 +1,71 @@ +#include "nameable.h" + +Nameable::Nameable(IRCServer* server) { + this->server = server; +} +Nameable::~Nameable() { + this->associates.clear(); + this->associates.shrink_to_fit(); +} + +std::string Nameable::get_addressable_name() { + return this->addressable_name; +} +bool Nameable::set_addressable_name(std::string name) { + if (this->server->is_addressable_name_free(name)) { + this->addressable_name = name; + server->register_addressable_name(name, this); + return true; + } else { + return false; + } +} +bool Nameable::has_addressable_name() { + return (this->addressable_name!=""); +} +void Nameable::remove_addressable_name(std::string name) { + server->remove_addressable_name(name); +} +bool Nameable::rename(std::string name) { + if (server->is_addressable_name_free(name)) { + server->remove_addressable_name(name); + server->register_addressable_name(name, this); + this->addressable_name = name; + return true; + } else { + return false; + } +} + +std::vector Nameable::get_associates() { + return this->associates; +} +void Nameable::add_associate(Nameable* associate) { + std::cout << "Adding: " << associate->get_addressable_name() << "\n"; + this->associates.push_back(associate); +} +bool Nameable::has_associate(Nameable* associate) { + return !(std::find(associates.begin(), associates.end(), associate) == associates.end()); +} +void Nameable::remove_associate(Nameable* associate) { + auto index = std::find(this->associates.begin(), this->associates.end(), associate); + if (index != this->associates.end()) { + this->associates.erase(index); + } +} +std::string Nameable::associates_as_string() { + std::string associate_list; + for (Nameable* associate : associates) { + associate_list += associate->get_addressable_name() + " "; + } + return associate_list; +} + +void Nameable::send_direct(std::string message) {} +void Nameable::send_relay(std::string message) { + for (Nameable* associate : this->associates) { + associate->send_direct(message); + } +} + +NameableType Nameable::what_are_you() {} \ No newline at end of file diff --git a/nameable.h b/nameable.h new file mode 100644 index 0000000..d01240b --- /dev/null +++ b/nameable.h @@ -0,0 +1,44 @@ +#ifndef NAMEABLE_H +#define NAMEABLE_H + +#include +#include +#include +#include +#include "connection.h" +#include "ircserver.h" + +enum NameableType { + NT_User, + NT_Channel +}; + +class Nameable { +private: + std::string addressable_name = ""; + std::vector associates; + +public: + IRCServer* server; + Nameable(IRCServer* server); + ~Nameable(); + + std::string get_addressable_name(); + bool set_addressable_name(std::string name); + bool has_addressable_name(); + void remove_addressable_name(std::string name); + bool rename(std::string to); + + std::vector get_associates(); + void add_associate(Nameable* associate); + bool has_associate(Nameable* associate); + void remove_associate(Nameable* associate); + std::string associates_as_string(); + + virtual void send_direct(std::string message); + void send_relay(std::string message); + + virtual NameableType what_are_you(); +}; + +#endif \ No newline at end of file diff --git a/user.cpp b/user.cpp new file mode 100644 index 0000000..173ab5c --- /dev/null +++ b/user.cpp @@ -0,0 +1,233 @@ +#include "user.h" +#include + +std::vector split_string(std::string str, std::string delim) { + std::vector to_return; + int delim_len = delim.length(); + int index = str.find_first_of(delim); + while (index != std::string::npos) { + to_return.push_back(str.substr(0, index)); + str = str.substr(index+delim_len); + index = str.find_first_of(delim); + } + to_return.push_back(str); + return to_return; +} + +std::unordered_map>> User::commands = { + {"NICK", {0, &User::nick_cmd}}, + {"PING", {1, &User::ping_cmd}}, + {"WHO", {1, &User::who_cmd}}, + {"WHOIS", {1, &User::whois_cmd}}, + {"JOIN", {1, &User::join_cmd}}, + {"PART", {1, &User::part_cmd}}, + {"QUIT", {0, &User::quit_cmd}}, + {"PRIVMSG", {2, &User::privmsg_cmd}}, + {"TOPIC", {2, &User::topic_cmd}}, + {"NAMES", {1, &User::names_cmd}} +}; + +User::User(IRCServer* server, UserConnection* conn): Nameable(server) { + this->connection = conn; +} +User::~User() { + this->server->remove_addressable_name(this->get_nick()); + this->send_relay(":" + this->get_nick() + " QUIT :Quit: "); + for (Nameable* associate : this->get_associates()) + associate->remove_associate(this); + delete connection; +} + +void User::nick_cmd(IRCCommand* cmd) { + set_nick(cmd->get_payload(0)); +} + +void User::ping_cmd(IRCCommand* cmd) { + send_direct(":" + server->get_hostname() + " PONG " + server->get_hostname() + " " + cmd->get_payload(0)); +} + +void User::join_cmd(IRCCommand* cmd) { + std::string channels_parameter = cmd->get_payload(0); + + std::vector named_channels = split_string(channels_parameter, ","); + std::cout << named_channels.size() << "\n"; + + for (std::string named_channel : named_channels) { + Nameable* channel; + if (server->has_addressable_name(named_channel)) { + channel = server->resolve_addressable_name(named_channel); + } else { + channel = new Channel(server, named_channel); + } + + if (channel->what_are_you() != NT_Channel) + continue; + + Channel* chan = (Channel*)channel; + + this->add_associate(chan); + chan->add_associate(this); + chan->send_direct(":" + this->get_nick() + " JOIN " + named_channel); + std::vector channel_members = chan->get_associates(); + + this->send_direct(":" + server->get_hostname() + " 353 " + this->get_nick() + " " + named_channel + " :" + chan->associates_as_string()); + if (chan->has_topic()) + this->send_direct(":" + server->get_hostname() + " 332 " + this->get_nick() + " " + named_channel + " :" + chan->get_topic()); + } +} + +void User::part_cmd(IRCCommand* cmd) { + std::string channels_parameter = cmd->get_payload(0); + + std::vector named_channels = split_string(channels_parameter, ","); + + for (std::string named_channel : named_channels) { + if (server->has_addressable_name(named_channel)) { + Nameable* channel = server->resolve_addressable_name(named_channel); + this->remove_associate(channel); + channel->remove_associate(this); + channel->send_direct(":" + this->get_nick() + " PART " + named_channel); + } + } +} + +void User::privmsg_cmd(IRCCommand* cmd) { + std::string named_target = cmd->get_payload(0); + + if (server->has_addressable_name(named_target)) { + Nameable* target = server->resolve_addressable_name(named_target); + for (Nameable* associate : target->get_associates()) { + if (associate->get_addressable_name() != this->get_nick()) + associate->send_direct(":" + this->get_nick() + " PRIVMSG " + named_target + " :" + cmd->get_payload(1)); + } + } +} + +void User::quit_cmd(IRCCommand* cmd) { + destroy(); +} + +void User::who_cmd(IRCCommand* cmd) { + if (!server->has_addressable_name(cmd->get_payload(0))) + return; + + Nameable* target = server->resolve_addressable_name(cmd->get_payload(0)); + if (target->what_are_you() != NT_User) + return; + + User* user = (User*)target; + + send_direct(":" + server->get_hostname() + " 352 " + this->get_nick() + " * " + user->get_username() + " " /*+ user->get_hostname()*/ + " " + server->get_hostname() + " H :0 " + user->get_realname()); + send_direct(":" + server->get_hostname() + " 315 " + this->get_nick() + " " + user->get_nick() + " :End of WHO listing."); +} + +void User::whois_cmd(IRCCommand* cmd) { + if (!server->has_addressable_name(cmd->get_payload(0))) + return; + + Nameable* target = server->resolve_addressable_name(cmd->get_payload(0)); + if (target->what_are_you() != NT_User) + return; + + User* user = (User*)target; +} + +void User::topic_cmd(IRCCommand* cmd) { + if (!server->has_addressable_name(cmd->get_payload(0))) + return; + + Nameable* target = server->resolve_addressable_name(cmd->get_payload(0)); + if (target->what_are_you() != NT_Channel) + return; + + ((Channel*)target)->set_topic(cmd->get_payload(1)); +} + +void User::names_cmd(IRCCommand* cmd) { + std::string named_channel = cmd->get_payload(0); + if (!server->has_addressable_name(named_channel)) + return; + Nameable* resolved = server->resolve_addressable_name(named_channel); + if (resolved->what_are_you() != NT_Channel) + return; + Channel* chan = (Channel*)resolved; + this->send_direct(":" + server->get_hostname() + " 353 " + this->get_nick() + " " + named_channel + " :" + chan->associates_as_string()); +} + +void User::process_cmd(IRCCommand* cmd) { + auto command = commands.find(cmd->get_command()); + if (command == commands.end()) { + send_direct(":" + server->get_hostname() + " 421 " + get_nick() + " " + cmd->get_command() + " :Unknown Command."); + return; + } + + if (cmd->get_payload_count() < std::get<0>(command->second)) { + send_direct(":" + server->get_hostname() + " 461 " + get_nick() + " " + cmd->get_command() + " :Too few parameters."); + return; + } + + std::get<1>(command->second)(this, cmd); +} + +void User::set_nick(std::string nick) { + bool success; + std::string old_nick = this->get_nick(); + if (this->has_nick()) + success = this->rename(nick); + else + success = this->set_addressable_name(nick); + + if (success) { + if (old_nick != "") + send_direct(":" + old_nick + " NICK :" + this->get_nick()); + } else { + send_direct(":" + server->get_hostname() + " 433 * " + nick + " Nickname is already in use!"); + } +} + +void User::set_username(std::string username) { + this->username = username; +} + +void User::set_realname(std::string realname) { + this->realname = realname; +} + +bool User::has_nick() { + return has_addressable_name(); +} + +bool User::has_username(){ + return (username!=""); +} + +bool User::has_realname(){ + return (realname!=""); +} + +std::string User::get_nick(){ + return get_addressable_name(); +} + +std::string User::get_username(){ + return username; +} + +std::string User::get_realname(){ + return realname; +} + +void User::send_direct(std::string message) { + std::cout << message << "\n"; + this->connection->send(message); +} + +NameableType User::what_are_you() { + return NT_User; +} + +void User::destroy() { + Connection* conn = connection; + delete this; + conn->kill(); +} \ No newline at end of file diff --git a/user.h b/user.h new file mode 100644 index 0000000..1372cda --- /dev/null +++ b/user.h @@ -0,0 +1,69 @@ +#ifndef USER_H +#define USER_H + +#include +#include +#include +#include + +#include "irccommand.h" +#include "ircserver.h" +#include "userconnection.h" +#include "nameable.h" +#include "channel.h" + +class UserConnection; +class User: public Nameable { +private: + std::string username, realname = ""; + Connection* connection; + + void nick_cmd(IRCCommand* cmd); + void ping_cmd(IRCCommand* cmd); + void who_cmd(IRCCommand* cmd); + void whois_cmd(IRCCommand* cmd); + void join_cmd(IRCCommand* cmd); + void part_cmd(IRCCommand* cmd); + void quit_cmd(IRCCommand* cmd); + void privmsg_cmd(IRCCommand* cmd); + void topic_cmd(IRCCommand* cmd); + void names_cmd(IRCCommand* cmd); + + static std::unordered_map>> commands; + + //std::string construct_string(int count, std::string source, std::string command, std::string parameters...); + template + std::string construct_string(std::string source, std::string command, T... parameters); + +public: + User(IRCServer* server, UserConnection* conn); + ~User(); + + void process_cmd(IRCCommand* cmd); + + void set_nick(std::string nick); + + void set_username(std::string username); + + void set_realname(std::string realname); + + bool has_nick(); + + bool has_username(); + + bool has_realname(); + + std::string get_nick(); + + std::string get_username(); + + std::string get_realname(); + + void send_direct(std::string message); + + NameableType what_are_you(); + + void destroy(); +}; + +#endif \ No newline at end of file diff --git a/userconnection.cpp b/userconnection.cpp new file mode 100644 index 0000000..32bb3c6 --- /dev/null +++ b/userconnection.cpp @@ -0,0 +1,50 @@ +#include "userconnection.h" + +#include + +UserConnection::UserConnection(int fd, IRCServer* server): Connection(fd) { + this->server = server; + this->user = new User(server, this); +} + +UserConnection::~UserConnection() {} + +void UserConnection::process_cmd(std::string cmd_in) { + if (cmd_in == "") + return; + + IRCCommand* cmd = new IRCCommand(cmd_in); + std::cout << cmd_in << std::endl; + + if (state == Registered) { + user->process_cmd(cmd); + delete cmd; + return; + } + + std::string command = cmd->get_command(); + + if (command == "NICK" && cmd->has_payload()) { + user->set_nick(cmd->get_payload(0)); + } + + if (command == "USER" && cmd->has_payload()) { + user->set_username(cmd->get_payload(0)); + user->set_realname(cmd->get_payload(3)); + } + + if (user->has_nick() && user->has_username()) { + state = Registered; + send(":xnoircd 001 " + user->get_nick() + " :Welcome"); + send(":xnoircd 002 " + user->get_nick() + " :Welcome"); + send(":xnoircd 003 " + user->get_nick() + " :Welcome"); + send(":xnoircd 004 " + user->get_nick() + " xnoircd 0 ABCabc ABCabc"); + send(":xnoircd 422 " + user->get_nick() + " :No MOTD"); + } + + delete cmd; +} + +void UserConnection::destroy() { + this->user->destroy(); +} \ No newline at end of file diff --git a/userconnection.h b/userconnection.h new file mode 100644 index 0000000..6b670c9 --- /dev/null +++ b/userconnection.h @@ -0,0 +1,29 @@ +#ifndef USERCONNECTION_H +#define USERCONNECTION_H + +#include + +#include "connection.h" +#include "ircserver.h" +#include "user.h" + +enum UCState { + Initial, + Registered +}; + +class User; +class UserConnection: public Connection { +private: + User* user; + UCState state = Initial; + IRCServer* server; +public: + UserConnection(int fd, IRCServer* server); + ~UserConnection(); + + void process_cmd(std::string cmd_in); + void destroy(); +}; + +#endif \ No newline at end of file