From f1a7e3e8f78e753834da3fe0bc2139e388fa2a4b Mon Sep 17 00:00:00 2001 From: root Date: Thu, 28 May 2026 12:43:25 +0000 Subject: [PATCH] push --- README.md | 101 +++++++++ en.subject.pdf => en.subject (2).pdf | Bin includes/Server.hpp | 3 + srcs/Server.cpp | 316 +++++++++++++++++++++++++++ 4 files changed, 420 insertions(+) create mode 100644 README.md rename en.subject.pdf => en.subject (2).pdf (100%) diff --git a/README.md b/README.md new file mode 100644 index 0000000..51089ed --- /dev/null +++ b/README.md @@ -0,0 +1,101 @@ +*This project has been created as part of the 42 curriculum by lfirmin, jle-neze.* + +## Description + +ft_irc is an IRC server written in C++ 98. It allows multiple clients to connect simultaneously, authenticate, join channels, and communicate in real time using the IRC protocol over TCP/IP. The server is built around a single `poll()` call with non-blocking I/O, handling all connections without forking. + +## Instructions + +### Compilation + +``` +make +``` + +### Execution + +``` +./ircserv +``` + +- `port` : the port the server listens on (e.g. 6667) +- `password` : the connection password required by IRC clients + +### Connecting with irssi + +``` +irssi +/connect localhost +``` + +### Connecting with netcat (for testing) + +``` +nc -C localhost +``` + +Then send commands manually: +``` +PASS +NICK +USER 0 * : +JOIN # +PRIVMSG # : +``` + +## Available Commands + +### Registration + +| Command | Usage | Description | +|---------|-------|-------------| +| `PASS` | `PASS ` | Authenticate with the server password | +| `NICK` | `NICK ` | Set or change your nickname | +| `USER` | `USER 0 * :` | Set your username and realname | +| `QUIT` | `QUIT` | Disconnect from the server | +| `PING` | `PING ` | Ping the server, replies with PONG | + +### Channels + +| Command | Usage | Description | +|---------|-------|-------------| +| `JOIN` | `JOIN # [key]` | Join or create a channel | +| `PART` | `PART #` | Leave a channel | +| `PRIVMSG` | `PRIVMSG # :` | Send a message to a channel | +| `PRIVMSG` | `PRIVMSG :` | Send a private message to a user | + +### Operator Commands + +| Command | Usage | Description | +|---------|-------|-------------| +| `KICK` | `KICK # ` | Eject a user from the channel | +| `INVITE` | `INVITE #` | Invite a user to the channel | +| `TOPIC` | `TOPIC #` | View the channel topic | +| `TOPIC` | `TOPIC # :` | Change the channel topic | +| `MODE` | `MODE # +i` | Set channel invite-only | +| `MODE` | `MODE # -i` | Remove invite-only | +| `MODE` | `MODE # +t` | Restrict TOPIC to operators | +| `MODE` | `MODE # -t` | Remove TOPIC restriction | +| `MODE` | `MODE # +k ` | Set a channel password | +| `MODE` | `MODE # -k` | Remove the channel password | +| `MODE` | `MODE # +o ` | Give operator privilege to a user | +| `MODE` | `MODE # -o ` | Remove operator privilege from a user | +| `MODE` | `MODE # +l ` | Set a user limit on the channel | +| `MODE` | `MODE # -l` | Remove the user limit | + +## Resources + +- [RFC 1459 - IRC Protocol](https://datatracker.ietf.org/doc/html/rfc1459) +- [RFC 2812 - IRC Client Protocol](https://datatracker.ietf.org/doc/html/rfc2812) +- [Beej's Guide to Network Programming](https://beej.us/guide/bgnet/) +- [irssi documentation](https://irssi.org/documentation/) + +### AI Usage + +Claude (claude.ai) was used during this project to: +- Guide the step-by-step implementation of the Server class (socket setup, poll loop, client handling) +- Explain IRC protocol concepts and numeric error codes +- Help structure the Channel class and its methods +- Debug compilation errors and logic issues + +All generated content was reviewed, understood and typed manually by the authors before being integrated into the project. diff --git a/en.subject.pdf b/en.subject (2).pdf similarity index 100% rename from en.subject.pdf rename to en.subject (2).pdf diff --git a/includes/Server.hpp b/includes/Server.hpp index bab68b8..8da1954 100644 --- a/includes/Server.hpp +++ b/includes/Server.hpp @@ -6,16 +6,19 @@ # include # include # include +# include # include # include # include # include # include "ParseBuffer.hpp" +# include # include # include # include # include "CommandValidator.hpp" # include "Channel.hpp" +# include struct ConnectedUser { diff --git a/srcs/Server.cpp b/srcs/Server.cpp index 47056d8..4db80fc 100644 --- a/srcs/Server.cpp +++ b/srcs/Server.cpp @@ -36,6 +36,7 @@ void Server::start(void) pollfd.events = POLLIN; pollfd.revents = 0; _fds.push_back(pollfd); + std::cout << "FT_IRC server startded on " << _port << " Port." << std::endl << "The password is \"" << _password << "\"." << std::endl; } ConnectedUser::ConnectedUser(void) : fd(-1), passOk(false), nickOk(false), userOk(false) @@ -109,6 +110,8 @@ void Server::acceptNew(void) if (newFd == -1) throw std::runtime_error(strerror(errno)); fcntl(newFd, F_SETFL, O_NONBLOCK); + int flag = 1; + setsockopt(newFd, IPPROTO_TCP, TCP_NODELAY, &flag, sizeof(flag)); struct pollfd pfd; pfd.fd = newFd; pfd.events = POLLIN; @@ -191,6 +194,20 @@ void Server::dispatch(int fd, const IrcMessage &msg) handlePing(fd, msg); else if (cmd == "QUIT") handleQuit(fd, msg); + else if (cmd == "JOIN") + handleJoin(fd, msg); + else if (cmd == "PART") + handlePart(fd, msg); + else if (cmd == "PRIVMSG") + handlePrivmsg(fd, msg); + else if (cmd == "KICK") + handleKick(fd, msg); + else if (cmd == "INVITE") + handleInvite(fd, msg); + else if (cmd == "TOPIC") + handleTopic(fd, msg); + else if (cmd == "MODE") + handleMode(fd, msg); } void Server::handlePass(int fd, const IrcMessage &msg) @@ -243,4 +260,303 @@ void Server::handleQuit(int fd, const IrcMessage &msg) { removeUser(fd); (void)msg; +} + +void Server::broadcastToChannel(const std::string &chanName, const std::string &msg, int exceptFd) +{ + Channel &chan = _channels[chanName]; + const std::vector &members = chan.getMembers(); + for (std::vector::const_iterator it = members.begin(); it != members.end(); ++it) + { + if (*it != exceptFd) + sendReply(*it, msg); + } +} + +void Server::handleJoin(int fd, const IrcMessage &msg) +{ + std::string chanName = msg.param(0); + if (!chanName.empty() && chanName[0] != '#') + { + sendReply(fd, "403 " + chanName + " :No such channel\r\n"); + return ; + } + if (_channels.find(chanName) == _channels.end()) + _channels.insert(std::make_pair(chanName, Channel(chanName))); + Channel &chan = _channels[chanName]; + if (chan.isInviteOnly() && !chan.isInvited(fd)) + { + sendReply(fd, "473 " + chanName + " :Cannot join channel (+i)\r\n"); + return ; + } + if (chan.getKey() != "" && (msg.paramCount() < 2 || msg.param(1) != chan.getKey())) + { + sendReply(fd, "475 " + chanName + " :Cannot join channel (+k)\r\n"); + return ; + } + if (chan.getUserLimit() > 0 && chan.memberCount() >= chan.getUserLimit()) + { + sendReply(fd, "471 " + chanName + " :Cannot join channel (+l)\r\n"); + return ; + } + if (chan.hasMember(fd)) + return ; + chan.addMember(fd); + if (chan.memberCount() == 1) + chan.addOperator(fd); + std::string msg_add = ":" + _users[fd].nick + "!" + _users[fd].username + "@localhost JOIN " + chanName + "\r\n"; + broadcastToChannel(chanName, msg_add, -1); + if (chan.getTopic() != "") + sendReply(fd, "332 " + _users[fd].nick + " " + chanName + " :" + chan.getTopic() + "\r\n"); +} + +void Server::handlePart(int fd, const IrcMessage &msg) +{ + std::string chanName = msg.param(0); + if (_channels.find(chanName) == _channels.end()) + { + sendReply(fd, "403 " + chanName + " :No such channel\r\n"); + return ; + } + Channel &chan = _channels[chanName]; + if (!chan.hasMember(fd)) + { + sendReply(fd, "442 " + chanName + " :You're not on that channel\r\n"); + return ; + } + std::string msg_rm = ":" + _users[fd].nick + "!" + _users[fd].username + "@localhost PART " + chanName + "\r\n"; + broadcastToChannel(chanName, msg_rm, -1); + chan.removeMember(fd); + if (chan.memberCount() == 0) + _channels.erase(chanName); +} + +void Server::handlePrivmsg(int fd, const IrcMessage &msg) +{ + std::string target = msg.param(0); + std::string text = msg.param(1); + std::string mess = ":" + _users[fd].nick + "!" + _users[fd].username + "@localhost PRIVMSG " + target + " :" + text + "\r\n"; + if (target[0] == '#') + { + if (_channels.find(target) == _channels.end()) + { + sendReply(fd, "403 " + target + " :No such channel\r\n"); + return ; + } + Channel &chan = _channels[target]; + if (!chan.hasMember(fd)) + { + sendReply(fd, "404 " + target + " :Cannot send to channel\r\n"); + return ; + } + broadcastToChannel(target, mess, fd); + } + else + { + for (std::map::iterator it = _users.begin(); it != _users.end(); ++it) + { + if (it->second.nick == target) + { + sendReply(it->first, mess); + return; + } + } + sendReply(fd, "401 " + target + " :No such nick\r\n"); + } +} + +void Server::handleKick(int fd, const IrcMessage &msg) +{ + std::string chanName = msg.param(0); + std::string targetNick = msg.param(1); + if (_channels.find(chanName) == _channels.end()) + { + sendReply(fd, "403 " + chanName + " :No such channel\r\n"); + return ; + } + Channel &chan = _channels[chanName]; + if (!chan.hasMember(fd)) + { + sendReply(fd, "442 " + chanName + " :You're not on that channel\r\n"); + return ; + } + if (!chan.isOperator(fd)) + { + sendReply(fd, "482 " + chanName + " :You're not channel operator\r\n"); + return ; + } + std::map::iterator it = _users.begin(); + int targetFd = -1; + for (; it != _users.end(); it++) + { + if (it->second.nick == targetNick) + { + targetFd = it->first; + break ; + } + } + if (targetFd == -1) + { + sendReply(fd, "401 " + targetNick + " :No such nick\r\n"); + return ; + } + if (!chan.hasMember(targetFd)) + { + sendReply(fd, "441 " + targetNick + " " + chanName + " :They aren't on that channel\r\n"); + return ; + } + std::string kick_msg = ":" + _users[fd].nick + "!" + _users[fd].username + "@localhost KICK " + chanName + " " + targetNick + "\r\n"; + broadcastToChannel(chanName, kick_msg, -1); + chan.removeMember(targetFd); +} + +void Server::handleInvite(int fd, const IrcMessage &msg) +{ + std::string targetNick = msg.param(0); + std::string chanName = msg.param(1); + if (_channels.find(chanName) == _channels.end()) + { + sendReply(fd, "403 " + chanName + " :No such channel\r\n"); + return ; + } + Channel &chan = _channels[chanName]; + if (!chan.hasMember(fd)) + { + sendReply(fd, "442 " + chanName + " :You're not on that channel\r\n"); + return ; + } + if (!chan.isOperator(fd)) + { + sendReply(fd, "482 " + chanName + " :You're not channel operator\r\n"); + return ; + } + std::map::iterator it = _users.begin(); + int targetFd = -1; + for (; it != _users.end(); it++) + { + if (it->second.nick == targetNick) + { + targetFd = it->first; + break ; + } + } + if (targetFd == -1) + { + sendReply(fd, "401 " + targetNick + " :No such nick\r\n"); + return ; + } + chan.addInvited(targetFd); + std::string invite_msg = ":" + _users[fd].nick + "!" + _users[fd].username + "@localhost INVITE " + targetNick + " " + chanName + "\r\n"; + sendReply(targetFd, invite_msg); +} + +void Server::handleTopic(int fd, const IrcMessage &msg) +{ + std::string chanName = msg.param(0); + if (_channels.find(chanName) == _channels.end()) + { + sendReply(fd, "403 " + chanName + " :No such channel\r\n"); + return ; + } + Channel &chan = _channels[chanName]; + if (!chan.hasMember(fd)) + { + sendReply(fd, "442 " + chanName + " :You're not on that channel\r\n"); + return ; + } + if (msg.paramCount() == 1) + { + if (chan.getTopic() != "") + sendReply(fd, "332 " + _users[fd].nick + " " + chanName + " :" + chan.getTopic() + "\r\n"); + else + sendReply(fd, "331 " + _users[fd].nick + " " + chanName + " :No topic is set\r\n"); + return ; + } + if (chan.isTopicRestricted() && !chan.isOperator(fd)) + { + sendReply(fd, "482 " + chanName + " :You're not channel operator\r\n"); + return ; + } + chan.setTopic(msg.param(1)); + std::string bd_msg = ":" + _users[fd].nick + "!" + _users[fd].username + "@localhost TOPIC " + chanName + " :" + chan.getTopic() + "\r\n"; + broadcastToChannel(chanName, bd_msg, -1); +} + +void Server::handleMode(int fd, const IrcMessage &msg) +{ + std::string chanName = msg.param(0); + std::string modeStr = msg.param(1); + if (chanName.empty() || chanName[0] != '#') + return ; + if (_channels.find(chanName) == _channels.end()) + { + sendReply(fd, "403 " + chanName + " :No such channel\r\n"); + return ; + } + Channel &chan = _channels[chanName]; + if (!chan.hasMember(fd)) + { + sendReply(fd, "442 " + chanName + " :You're not on that channel\r\n"); + return ; + } + if (!chan.isOperator(fd)) + { + sendReply(fd, "482 " + chanName + " :You're not channel operator\r\n"); + return ; + } + bool adding = (modeStr[0] == '+'); + char mode = modeStr[1]; + if (mode == 'i') + chan.setInviteOnly(adding); + else if (mode == 't') + chan.setTopicRestricted(adding); + else if (mode == 'k') + if (adding) + if (msg.paramCount() >= 3) + chan.setKey(msg.param(2)); + else + return ; + else + chan.setKey(""); + else if (mode == 'o') + { + if (msg.paramCount() >= 3) + { + std::map::iterator it = _users.begin(); + int targetFd = -1; + for (; it != _users.end(); it++) + { + if (it->second.nick == msg.param(2)) + { + targetFd = it->first; + break ; + } + } + if (targetFd == -1) + { + sendReply(fd, "401 " + msg.param(2) + " :No such nick\r\n"); + return ; + } + if (adding) + chan.addOperator(targetFd); + else + chan.removeOperator(targetFd); + } + else + return ; + } + else if (mode == 'l') + { + if (adding) + { + if (msg.paramCount() >= 3) + chan.setUserLimit((size_t)atoi(msg.param(2).c_str())); + else + return ; + } + else + chan.setUserLimit(0); + } + std::string mod_msg = ":" + _users[fd].nick + "!" + _users[fd].username + "@localhost MODE " + chanName + " " + modeStr + "\r\n"; + broadcastToChannel(chanName, mod_msg, -1); } \ No newline at end of file