diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..8fe5d69 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,50 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Debug with LLDB", + "type": "lldb", + "request": "launch", + "args": ["6667", "pass"], + "program": "${workspaceFolder}/ircserv_debug", + "cwd": "${workspaceFolder}", + "stopOnEntry": false, + "preLaunchTask": "42 build with Makefile", + "initCommands": [ + "process handle SIGINT -s false -p true" + ] + }, + { + "name": "C/C++ Runner: Debug Session", + "type": "cppdbg", + "request": "launch", + "args": ["6667", "pass"], +// "stopAtEntry": true, + "externalConsole": false, + "cwd": "${workspaceFolder}", + "program": "${workspaceFolder}/ircserv_debug", + "MIMode": "gdb", + "miDebuggerPath": "gdb", + // "miDebuggerArgs": "-ex 'set debuginfod enabled off'", + //"preLaunchTask": "42 C build bonus test.c", + "preLaunchTask": "42 build with Makefile", + "setupCommands": [ + { + "description": "Enable pretty-printing for gdb", + "text": "-enable-pretty-printing", + "ignoreFailures": true + }, + { + "description": "Pass SIGINT to program", + "text": "handle SIGINT pass nostop", + "ignoreFailures": true + }, + { + "description": "Pass SIGQUIT to program", + "text": "handle SIGQUIT pass nostop", + "ignoreFailures": true + } + ] + } + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..4eb0aea --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,103 @@ +{ + "C_Cpp_Runner.cCompilerPath": "gcc", + "C_Cpp_Runner.cppCompilerPath": "g++", + "C_Cpp_Runner.debuggerPath": "gdb", + "C_Cpp_Runner.cStandard": "", + "C_Cpp_Runner.cppStandard": "", + "C_Cpp_Runner.msvcBatchPath": "", + "C_Cpp_Runner.useMsvc": false, + "C_Cpp_Runner.warnings": [ + "-Wall", + "-Wextra", + "-Wpedantic", + "-Wshadow", + "-Wformat=2", + "-Wcast-align", + "-Wconversion", + "-Wsign-conversion", + "-Wnull-dereference" + ], + "C_Cpp_Runner.msvcWarnings": [ + "/W4", + "/permissive-", + "/w14242", + "/w14287", + "/w14296", + "/w14311", + "/w14826", + "/w44062", + "/w44242", + "/w14905", + "/w14906", + "/w14263", + "/w44265", + "/w14928" + ], + "C_Cpp_Runner.enableWarnings": true, + "C_Cpp_Runner.warningsAsError": false, + "C_Cpp_Runner.compilerArgs": [ + "-Wall -Werror -Wextra" + ], + "C_Cpp_Runner.linkerArgs": [], + "C_Cpp_Runner.includePaths": [], + "C_Cpp_Runner.includeSearch": [ + "*", + "**/*" + ], + "C_Cpp_Runner.excludeSearch": [ + "**/build", + "**/build/**", + "**/.*", + "**/.*/**", + "**/.vscode", + "**/.vscode/**" + ], + "C_Cpp_Runner.useAddressSanitizer": false, + "C_Cpp_Runner.useUndefinedSanitizer": false, + "C_Cpp_Runner.useLeakSanitizer": false, + "C_Cpp_Runner.showCompilationTime": false, + "C_Cpp_Runner.useLinkTimeOptimization": false, + "C_Cpp_Runner.msvcSecureNoWarnings": false, + "42header.email": "rhaas@student.42berlin.de", +"42header.username": "rhaas", +"files.associations": { + "rh_fmtconv_std.h": "c", + "ft_printf_helper.h": "c", + "unistd.h": "c", + "rh_fmtspec.h": "c", + "stddef.h": "c", + "rh_write.h": "c", + "libft.h": "c", + "push_swap.h": "c", + "ft_printf.h": "c", + "sort.h": "c", + "utils.h": "c", + "output.h": "c", + "get_next_line.h": "c", + "mlx.h": "c", + "keysymdef.h": "c", + "keysym.h": "c", + "bit": "c", + "move_zoom.h": "c", + "location.h": "c", + "xlib.h": "c", + "fractol.h": "c", + "limits": "c", + "math.h": "c", + "typeinfo": "c", + "limits.h": "c", + "fractals.h": "c", + "stdint.h": "c", + "stat.h": "c" +}, +"editor.defaultFoldingRangeProvider": "ms-vscode.cpptools", +"editor.defaultFormatter": "ms-vscode.cpptools", +"editor.insertSpaces": false, +"editor.detectIndentation": false, +"#endif with guard": { + "prefix": "#endif", + "body": "#endif // ${1:GUARD_NAME}", + "description": "Add #endif with guard comment" +} +} + diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000..d772cd9 --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,22 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "type": "cppbuild", + "label": "42 build with Makefile", + "command": "/usr/bin/make", + "args": [ + // "fclean", + "debug", + ], + "options": { + "cwd": "${workspaceFolder}" + }, + "problemMatcher": [ + "$gcc" + ], + "group": "build", + "detail": "compiler: /usr/bin/c++" + } + ] +} diff --git a/README.md b/README.md index 07d21ed..1071e55 100644 --- a/README.md +++ b/README.md @@ -1,26 +1,21 @@ *This project has been created as part of the 42 curriculum by blnunez-, kmurugan, rhaas.* -# ft_irc - IRC server +# ft_irc - IRC Server ## Description ### 42 Project | Internet Relay Chat in C++98 ### Objective -Build an IRC server in C++98 that implements... - +Build an IRC server in C++98 that handles multiple concurrent clients over TCP, implementing the core IRC protocol (registration, channels, modes, private messaging, and operator commands) over an event-driven sockets layer. ### Overview -... +**ft_irc** is a 42 project that teaches event-driven I/O, the poll multiplexing model, and the IRC protocol. The server listens on a given port and password, accepts multiple clients via non-blocking sockets, registers them with PASS/NICK/USER, and lets them join channels, exchange private and channel messages, and manage channels with KICK/INVITE/TOPIC/MODE and operator privileges. --- ## Instructions -### Requirements - -- - ### Compile & Run ```sh @@ -31,23 +26,339 @@ make && ./ircserv make bonus && ./ircserv_bonus ``` +**NOTE:** The server binds an IPv4 and an IPv6 listening socket (`IPV6_V6ONLY`) and multiplexes them with `poll`. Connect with any IRC client (e.g. `irssi`) or a raw `nc` session. + +```sh +# connect with netcat and register +nc -C # -C sends CRLF line endings (on macOS use -c) +PASS +NICK +USER 0 * : +JOIN #channel +``` + +Port must be in the range `1-65535`, a password must be supplied.\ +Please note that you may need system admin permissions to use privileged ports (usually 1–1023). + --- -## Clang tool commands +## Architecture + +### Overview + +```mermaid +flowchart LR + subgraph CLIENT["client connection"] + IRC["irc client"] + DCCP["DCC peer"] + end + + subgraph CONN["connection"] + LISTENER["IRC listener"] + SOCKET["Socket"] + end + + subgraph CORE["server core"] + SERVER["Server"] + CMD["CommandProcessor"] + CM["ClientManager"] + CHM["ChannelManager"] + end + + IRC --> LISTENER --> SOCKET --> SERVER + SERVER --> CMD + SERVER --> CM + SERVER --> CHM + CM --> CHM + DCCP -. "direct peer transfer" .-> SERVER + + class IRC,DCCP,LISTENER,SOCKET,SERVER,CMD,CM,CHM node; +``` + +### Core Server + +```mermaid +flowchart TB + subgraph SRV["Server"] + direction TB + LSTN["IRC listeners\n (bind + listen)"] + ACCEPT["Accept client"] + LOOP["Event loop\n (poll(), non-blocking)"] + SOCK["Socket\n (buffered reads, overlong-line discard)"] + SIG["SignalHandler"] + LOG["Logger"] + end + + subgraph DRIVES["owned and driven by the loop"] + direction TB + CMD["CommandProcessor"] + CM["ClientManager"] + CHM["ChannelManager"] + TM["TransferManager (bonus)"] + end + + LSTN --> ACCEPT --> SOCK --> LOOP + LOOP --> CMD + LOOP --> CM + LOOP --> CHM + LOOP --> TM + + class LSTN,ACCEPT,LOOP,SOCK,SIG,LOG node; + class CMD,CM,CHM,TM node; +``` + +### Client System +```mermaid +flowchart TB + subgraph CORE["server core"] + SRV["Server"] + CMD["CommandProcessor"] + end + + subgraph CLISYS["client system"] + CLI["Client"] + CLM["ClientManager\n (lookup by nick)"] + REG["Registration\n (PASS NICK USER)"] + QUIT["QUIT/disconnect\n (part all channels)"] + end + + SRV -->|"accepted connection"| CLM --> CLI + CMD -->|"commands by nick"| CLM + CLM --> QUIT -. "leave all channels" .-> CHM["ChannelManager"] + CLI --> REG -->|"send welcome message"| CLIENT["client"] + + class SRV,CMD,CLIENT node; + class CLISYS,CLI,CLM,REG,QUIT node; +``` + +### Channel System + +```mermaid +flowchart TB + subgraph CORE["server core"] + SRV["Server"] + CM["ClientManager"] + end + + subgraph CHSYS["channel system"] + CHM["ChannelManager"] + CH["Channel"] + MEM["MemberSet\n (Client pointers, per-member ModeSet)"] + MS["ModeSet"] + MOD["Mode"] + end + + SRV --> CHM --> CH + CM --> |"provides client objects"| CH + CH -->|contains| MEM + CH -->|has| MS + CH -->|uses| MOD + MEM -->|stores per-member| MS + MS -->|uses| MOD + + class SRV,CM node; + class CHSYS,CHM,CH,MEM,MS,MOD node; +``` + +### Command Processing + +```mermaid +flowchart TB + + CI["client input"] + SOCKIN["socket"] + RB["response buffer"] + SO["socket output"] + + subgraph CP["Command Processing"] + PA["command parser"] + VA["command validation"] + H["command handler"] + INV["invalid"] + end + + CI --> SOCKIN --> PA + PA --> VA + VA --> H + VA -.-> INV + H --> RB + INV --> RB + RB --> SO + + class CI,SOCKIN,RB,SO node; + class CP,PA,VA,H,INV node; +``` + +### Bonus + +```mermaid +flowchart TB + PRIVMSG["PRIVMSG"] + NORMAL["Client"] + BOT["DCCBot"] + + subgraph FT["file transfer"] + VAL["Validate filename in files/"] + LSN["Open listener - OS-picked port"] + BUILD["Build DCC SEND CTCP offer"] + OFFER["Send offer to target client"] + ACC["!accept - only the target"] + STREAM["connect + stream - 1 KiB chunks"] + DONE["transfer complete"] + EXPIRE["rejected or 60 s expiry"] + end + + PARSE["Parse CTCP (VERSION / PING)"] + REPLY["NOTICE reply to client"] + OTHER["!help !version !whoami !accept !reject"] + + PRIVMSG -->|"target is bot"| BOT + PRIVMSG -->|"target is client"| NORMAL + BOT -->|"CTCP"| PARSE --> REPLY + BOT -->|"other command"| OTHER + BOT -->|"!send"| VAL --> LSN --> BUILD --> OFFER + OFFER -->|"client accepts"| ACC --> STREAM --> DONE + OFFER -->|"!reject / timeout"| EXPIRE + + class PRIVMSG,NORMAL,BOT,REPLY,OTHER node; + class FT,VAL,LSN,BUILD,OFFER,ACC,STREAM,DONE,EXPIRE node; +``` +--- + +## IRC Command & Message Reference + +### Registration +- `PASS ` — authenticate during registration. +- `NICK ` — set/change the nick. +- `USER ` — complete registration credentials. + +### Channels +- `JOIN <#channel> []` — join (or create) a channel, optional key for `+k`. +- `PART <#channel> []` — leave a channel (deleted when empty). +- `KICK <#channel> []` — remove a member (the issuer must outrank the target). +- `INVITE <#channel>` — invite a user (may require operator rank). +- `TOPIC <#channel> []` — view/set the topic (may require operator rank). +- `NAMES <#channel>` — list current members. +- `MODE <#channel> [modes]` — view/set channel modes and user modes. + +### Messaging +- `PRIVMSG ` — send a message. The bonus bot answers PRIVMSG addressed to it. +- `NOTICE ` — send a notice (no auto-reply / CTCP handling). + +### Channel Modes +| Mode | Argument | Meaning | +|------|----------|---------| +| `i` | no | Invite-only channel (`JOIN` requires an invite) | +| `t` | no | Only operators may set the topic | +| `k` | yes | Channel key (password) required to join | +| `o` | yes | Grant/revoke operator privilege to a member | +| `l` | yes | User limit — max concurrent members | + +### Channel User Modes +`o` Operator · none + +Modes act on the target `nick` and are shown as `@` for operator or none for regular users. Privileges are checked on every call to a command that needs elevated privileges (this may also depend on the state of a channel, e.g. whether changing the TOPIC is restricted or not). + +### Other +- `PING`/`PONG` — keepalive and latency check. + +--- + +## Bonus: DCC Bot & File Transfer + +Built with `make bonus` → `./ircserv_bonus `. + +### DCCBot + +| Abbr.| Meaning | +|------|---------------------------| +| CTCP | Client-To-Client Protocol | +| DCC | Direct Client-to-Client | + +A bot named `DCCBot` (also reached as `bot` / `BOT` / `#bot`, or by sending a bot command to any channel) that intercepts `PRIVMSG`: bot commands are answered with `PRIVMSG`, CTCP queries with `NOTICE`. + +| Command | Action | +|------------------|--------| +| `!help` | List the available bot commands | +| `!version` | Report the bot/server version | +| `!whoami` | Show the requester's nick | +| `!send ` | Start a DCC SEND offer of `files/` | +| `!accept` | Accept the pending transfer offer | +| `!reject` | Reject the pending transfer offer | + +CTCP queries (`\x01VERSION\x01`, `\x01PING \x01`) sent to the bot via `PRIVMSG` are answered by the bot with `NOTICE`. + +### File transfer (DCC SEND) + +#### Using irssi + +Sender: ```sh -# Run clang-format before PR (use clang-format version 21) -make format +/dcc send ``` +Receiver receives: + +`DCC SEND from [ port ]: ` + +Receiver: +```sh +# set filepath +/set dcc_download_path + +# receive file +/dcc get +``` + +#### Using nc + +Sender: ```sh -# Run clang-tidy on individual file +PRIVMSG :\x01DCC SEND \x01 +``` + +Receiver receives: + +`:sender!sender@127.0.0.1 PRIVMSG receiver :\x01DCC SEND \x01` + +Receiver in another terminal must open nc to receive: +```sh +nc > +``` + +#### Using the BOT + +`!send` opens a listener on an OS-picked port and hands the target a CTCP `DCC SEND 127.0.0.1 ` line. The receiver replies with `!accept`, connects to the advertised address/port and receives the file in 1 KiB chunks. Only one transfer is active at a time (`bot busy` otherwise). Pending offers expire after 60 seconds. Offered files must live in a `files/` directory next to the server; filenames containing `/` or `..` are rejected. + +Sender: +```sh +/msg bot !send +``` + +Receiver: +```sh +/msg bot !accept + +# Receiver in another terminal must open nc to receive: +nc 127.0.0.1 > +``` + +--- + +## Clang tool commands + +```sh +# clang-format all headers and sources +make format + +# clang-tidy one file make tidy FILE=srcs/ -# Run clang-tidy on multiple files (filenames without spaces) +# clang-tidy on multiple files (filenames without spaces) make tidy FILE="srcs/ srcs/" -# Run clang-tidy on all .cpp/.hpp under srcs and includes +# clang-tidy every source and header make tidy-all ``` @@ -57,46 +368,77 @@ NOTE: Review clang-tidy results and fix only what makes sense for this project. ## Key Challenges & Solutions -### Topic +### Protocol-Correct Error Replies + +**Challenge:** Every command can fail in several ways (wrong channel, no such nick, missing params, insufficient privileges) and each failure must produce the correct RFC 2812 numeric to the client. + +**Solution:** A `Reply` module maps each numeric code to its required parameter layout and trailing text, and an `Error` carries a pre-built `Reply::Message`.`CommandProcessor::run` validates the command name, registration state, +operator requirement and parameter count up front. On the first bad condition it sends the error to the offending client via `sendError_` and stops that command line without throwing. Lower-level helpers (`Channel`, `ChannelManager`, `ClientManager`) still throw `Error`, and each handler wraps its calls in a small local `try/catch (Error)` that forwards `e.reply()` to the client. Every failure therefore produces exactly one well-formed `:server :\r\n`, and no exception escapes into the event loop. + +### Usage of `poll` and buffers + +**Challenge:** Apart from setting up sockets correctly for receiving data streams, caching and handling of incomplete or overlong messages needed to be implemented. Additionally, a certain amount of fault tolerance was desired, e.g. for not disconnecting a client on the first too long message. + +**Solution:** Before passing on any data to command parsers and processors, the validity of a full line is checked. No incomplete or overlong data is ever forwarded to the internal handlers. Additionally an error counter is installed separately for each client, which limits the amount of consecutive errors and is reset on every valid input. -**Challenge:** +### Single-Author Event Loop -**Solution:** +**Challenge:** One person owned the whole loop, so every change touched it all at once and nothing could be changed in isolation. `Server` and `Socket` were not separated into distinct jobs, and the work arrived as one PR with 30-50 commits, making every review unsafe. + +**Solution:** If redone: separate `Socket` (fd, buffer, limits) from `Server` (pollfd vector, close decisions) from day one, and ship changes as small, one-behaviour PRs. + +### Channel Lifetime & Ownership + +**Challenge:** Channels are created on join, destroyed when the last member parts, and must never hold a pointer to a freed `Client`. + +**Solution:** `ChannelManager` owns `Channel` objects in a case-folded map and cleans up empty channels after PART/KICK/leaveAll. `MemberSet` stores `Client*` + `ModeSet`. `ClientManager::remove` first parts the client from every channel (via `ChannelManager::leaveAll`) before freeing the `Client`, so channel member vectors never dangle. + +### Over-Length Input Handling + +**Challenge:** Not disconnecting on the first over-length message forced a discard-mode state machine, a per-client error counter and deferred-close markers into the loop — where the hard-to-find bugs lived. + +**Solution:** If redone: close on the first over-length line. That deletes the discard state, the error counter and the close markers. The loop becomes just "lines in, close on error". + +### Bonus: CTCP & DCC SEND through the Bot + +**Challenge:** The CTCP mechanic used by DCC is barely documented anywhere. You can forward a CTCP by stripping the wrapping `\x01` bytes and re-sending what is inside. But to actually run a file transfer the server needs to hand the receiver a `\x01DCC SEND \x01` line, open a listener the receiver can connect to, and stream the file. Wiring the two bonuses together (bot + transfer manager) with the bot as the middleman got complicated as the design evolved, and real clients silently abstract all of this away so there was no reference. + +**Solution:** Make the bot the DCC sender. `!send` asks the `TransferManager` to validate the filename, open a listener on an OS-picked port, and return the offer port/size. The bot then builds the `DCC SEND ... 127.0.0.1 ` CTCP and delivers it wrapped in `\x01` to the target over `PRIVMSG`. The receiver just replies `!accept` and connects to the advertised address/port — which means the whole flow works over a plain `nc` session, no IRC client needed. Using the bot to construct and send the CTCP also makes the query path symmetric: the same `PRIVMSG` interception that answers `!help`/`!version` carries the DCC offer, and CTCP responses go back out as `NOTICE`. Transfer state (offer → accept/reject → stream → expire after 60 s) stayed in `TransferManager`, while the bot is a dumb messenger. --- ## Resources ### IRC protocol -[RFC 2810 - IRC: Architecture](https://rfc-editor.org/rfc/rfc2810) -[RFC 2811 - IRC: Channel Management](https://rfc-editor.org/rfc/rfc2811) -[RFC 2812 - IRC: Client Protocol](https://rfc-editor.org/rfc/rfc2812) -[RFC 2813 - IRC: Server Protocol](https://rfc-editor.org/rfc/rfc2813) -[ircgod](https://ircgod.com) -[Model IRC client protocol](https://modern.ircdocs.horse) +- [RFC 2810 - IRC: Architecture](https://rfc-editor.org/rfc/rfc2810) +- [RFC 2811 - IRC: Channel Management](https://rfc-editor.org/rfc/rfc2811) +- [RFC 2812 - IRC: Client Protocol](https://rfc-editor.org/rfc/rfc2812) +- [RFC 2813 - IRC: Server Protocol](https://rfc-editor.org/rfc/rfc2813) +- [ircgod](https://ircgod.com) +- [Model IRC client protocol](https://modern.ircdocs.horse) ### rfc reading tips -[Tips for Reading RFCs](https://www.rfc-editor.org/series/rfc-tips/) +- [Tips for Reading RFCs](https://www.rfc-editor.org/series/rfc-tips/) ### Reference IRC client and server -[Server: ergo chat (formerly Oragono)](https://github.com/ergochat/ergo) -[Client: irssi](https://github.com/irssi/irssi) +- [Server: ergo chat (formerly Oragono)](https://github.com/ergochat/ergo) +- [Client: irssi](https://github.com/irssi/irssi) ### Sockets -[Sockets & Networking](https://web.mit.edu/6.031/www/sp19/classes/23-sockets-networking/) -[All about sockets](https://docs.oracle.com/javase/tutorial/networking/sockets/index.html) - -[CS107 Sockets Programming](https://web.stanford.edu/class/archive/cs/cs107/cs107.1238/lectures/20/) -[Beej's Guide to Network Programming](https://beej.us/guide/bgnet/) - What is a +- [Sockets & Networking](https://web.mit.edu/6.031/www/sp19/classes/23-sockets-networking/) +- [All about sockets](https://docs.oracle.com/javase/tutorial/networking/sockets/index.html) +- [CS107 Sockets Programming](https://web.stanford.edu/class/archive/cs/cs107/cs107.1238/lectures/20/) +- [Beej's Guide to Network Programming](https://beej.us/guide/bgnet/) - What is a socket?, IP Addresses, structs, and Data Munging, Jumping from IPv4 to IPv6, System Calls or Bust ### DCC protocol -[Direct Client-to-Client protocol (DCC)](https://modern.ircdocs.horse/dcc.html) +- [Direct Client-to-Client protocol (DCC)](https://modern.ircdocs.horse/dcc.html) ### Use of AI -- -- +- Learning and verification: Clarifying IRC numeric replies, and the RFC 2812 grammar. +- Documentation: Proofreading and refining the README and project descriptions. +- Testing and debugging: Generating protocol-level test scenarios and reviewing edge cases. --- diff --git a/includes/Server.hpp b/includes/Server.hpp index 931ed4c..2630c66 100644 --- a/includes/Server.hpp +++ b/includes/Server.hpp @@ -59,7 +59,8 @@ class Server { void processServerSocketData_(); void acceptClient_(int const serverFd); void processClientSockets_(); - void removeClientSocketPfd_(int const fd, std::string const &msg); + bool removeClientSocketPfd_(int const fd, std::string const &msg = "", + bool force = false) throw(); void storePollFd_(int const socketFd, bool srvSocket); void removePollFd_(int const fd); void log_(std::string const &msg, Logger::logMode const lm); diff --git a/includes/Socket.hpp b/includes/Socket.hpp index 380cb02..6cb3c6a 100644 --- a/includes/Socket.hpp +++ b/includes/Socket.hpp @@ -23,6 +23,7 @@ class Socket { bool discardMode; bool isSrvSocket; bool closeFd; + std::string closeMsg; SocketIOData(bool srvSocket) : bytesRead(0), recvErrorCount(0), sendErrorCount(0), @@ -38,15 +39,15 @@ class Socket { public: Socket(Logger const &logger) : logger_(logger) {} - void addSocket(int const fd, bool srvSocket); - bool checkSocketData(int const fd); - std::string getCRLFLineFromBuffer(int const fd, bool &buffHasMoreCmds, - bool &msgTooLong); - void sendMsg(int const fd, std::string const &msg); - bool socketCloseMarker(int const fd); - void closeSocket(int const fd); - + void addSocket(int const fd, bool srvSocket); + std::string getCRLFLineFromBuffer(int const fd, bool &buffHasMoreCmds, + bool &msgTooLong); + void sendMsg(int const fd, std::string const &msg); + bool socketCloseMarker(int const fd); + void setSocketCloseState(int const fd, bool doClose, + std::string const &closeMsg = ""); bool updateBuffer_(struct pollfd &pfd); - SocketIOData *getSockIOData(int const fd); - bool isServerSocket(int const fd); + SocketIOData &getSockIOData(int const fd); + void closeSocket(int const fd); + bool removeSockIOData(int const fd) throw(); }; diff --git a/memo/bind_socket.md b/memo/bind_socket.md new file mode 100644 index 0000000..a037bf6 --- /dev/null +++ b/memo/bind_socket.md @@ -0,0 +1,59 @@ +# bind a socket to a single address, multiple addresses and/or sockets on linux - what are the implications? + +Binding a socket on Linux involves critical trade-offs between **accessibility**, **security**, and **concurrency**. The implications depend heavily on the specific IP address chosen and the socket options used. + +### 1. Binding to a Single Specific Address (e.g., `192.168.1.5`) +Binding to a specific interface IP restricts the socket to only accept connections arriving on that specific network interface. + +* **Security Implication**: This significantly **reduces the attack surface**. If the machine has a public interface (eth0) and a private management interface (eth1), binding only to `eth1` ensures the service is unreachable from the internet, even if firewall rules are misconfigured. +* **Routing Dependency**: The service will fail to start if the specific IP is not currently assigned to an interface (e.g., DHCP failure or interface down). +* **Isolation**: Useful for multi-tenant servers where different services must listen on the same port but on different IPs (e.g., Virtual Hosting). + +### 2. Binding to Multiple Addresses (Wildcard `0.0.0.0` or `::`) +Binding to `0.0.0.0` (IPv4) or `::` (IPv6) instructs the kernel to listen on **all available network interfaces**. + +* **Security Implication**: This **maximizes the attack surface**. The service becomes accessible from every interface: loopback (`127.0.0.1`), LAN, and potentially the public internet. + * **Risk**: Accidental exposure of internal-only services (like databases or debug ports) to the public network is a common vulnerability. + * **Mitigation**: Requires strict firewall rules (`iptables`/`nftables`) to block unwanted interfaces, as the application itself accepts traffic from anywhere. +* **Convenience**: Ideal for services intended to be universally accessible (e.g., a public web server) or in dynamic IP environments (DHCP) where the specific IP might change. +* **IPv6 Dual-Stack**: On Linux, binding to `::` often listens on both IPv4 and IPv6 (depending on `net.ipv6.bindv6only`), which can lead to unexpected behavior if not managed. + +### 3. Binding Multiple Sockets to the Same Port +By default, Linux enforces exclusive binding: only one socket can bind to a specific `IP:Port` tuple. Attempting to bind a second socket results in `EADDRINUSE`. However, specific socket options alter this behavior. + +#### A. `SO_REUSEADDR` (Address Reuse) +* **Function**: Allows a new socket to bind to a port that is in `TIME_WAIT` state (recently closed). It also allows binding to the same port on **different** specific IP addresses (e.g., `192.168.1.5:80` and `192.168.1.6:80`). +* **Implication**: It does **not** allow two processes to bind to the exact same `IP:Port` combination simultaneously for load balancing (with the exception of UDP multicast). +* **Use Case**: Essential for high-availability servers that need to restart instantly without waiting for the TCP `TIME_WAIT` period (typically 60-120 seconds) to expire. + +#### B. `SO_REUSEPORT` (Port Reuse - Linux 3.9+) +* **Function**: Allows **multiple sockets** (in different processes or threads) to bind to the **exact same** `IP:Port` combination, provided **all** of them set this option before binding. +* **Load Balancing**: The kernel performs load balancing, distributing incoming connections across the bound sockets. This avoids the "thundering herd" problem where one process accepts and distributes work. +* **Security Implication**: + * **Port Hijacking**: Without proper checks, a malicious process could bind to a privileged port and steal connections. Linux mitigates this by requiring all processes in the `SO_REUSEPORT` group to have the **same Effective User ID (UID)**. + * **Risk**: If an attacker gains access to the same user account, they can inject a socket into the group and intercept traffic (e.g., capturing unencrypted TLS handshakes). + + + +### Summary of Implications + +| Binding Strategy | Accessibility | Security Posture | Concurrency | Primary Use Case | +| :--- | :--- | :--- | :--- | :--- | +| **Specific IP** | Restricted to one interface | **High** (Minimal exposure) | Single Socket | Internal services, multi-IP hosting | +| **Wildcard (`0.0.0.0`)** | All interfaces (Public + Private) | **Low** (Requires firewall) | Single Socket | Public-facing services | +| **`SO_REUSEADDR`** | Depends on IP bound | Neutral | Sequential (Time-wait bypass) | Fast server restarts | +| **`SO_REUSEPORT`** | Depends on IP bound | **Moderate** (UID check required) | **Parallel** (Kernel LB) | Multi-process servers (Nginx, Go) | + +### Best Practices +1. **Default to Specific IPs**: Unless the service must be public, bind to `127.0.0.1` or a specific internal LAN IP. +2. **Avoid `0.0.0.0` for Databases**: Never bind databases (Redis, Postgres, MySQL) to `0.0.0.0` unless explicitly intended for remote access; this is a leading cause of ransomware attacks. +3. **Use `SO_REUSEPORT` for Scale**: For high-performance TCP servers needing multiple worker processes, use `SO_REUSEPORT` (Linux 3.9+) instead of a single acceptor thread passing file descriptors. +4. **Verify UID**: When using `SO_REUSEPORT`, ensure strict user permissions to prevent local privilege escalation attacks via port hijacking. + + + + ``` + int optval = 1; + if (setsockopt(sock_fd, SOL_SOCKET, SO_REUSEADDR, &optval, sizeof(optval))) + throw std::runtime_error("Error while setting socket options!"); +``` \ No newline at end of file diff --git a/memo/ft_irc_en.subject.pdf b/memo/ft_irc_en.subject.pdf new file mode 100644 index 0000000..69d287b Binary files /dev/null and b/memo/ft_irc_en.subject.pdf differ diff --git a/memo/memo.md b/memo/memo.md new file mode 100644 index 0000000..8d6e31a --- /dev/null +++ b/memo/memo.md @@ -0,0 +1,81 @@ +# Questions +- what is a spanning tree exactly?\ +maybe not relevant, bc we have a single lonely server only + + > In graph theory, a spanning tree is a subgraph of an undirected, connected graph that includes all vertices of the original graph using the minimum possible number of edges. For a graph with $n$ vertices, a spanning tree always contains exactly $n-1$ edges, ensuring the structure is connected without forming any cycles or loops. + > + > Spanning trees are critical for network routing protocols (like the Spanning Tree Protocol or STP) to prevent data loops in mesh topologies, and they serve as intermediate steps in pathfinding algorithms such as Dijkstra’s algorithm and A*. When edges have weights, the goal is often to find a minimum spanning tree using greedy algorithms like Prim’s or Kruskal’s to minimize total connection costs. + > +# RFCs +https://datatracker.ietf.org/doc/html/rfc2810 +- contains links to implementations at the end + +https://datatracker.ietf.org/doc/html/rfc2811 +https://datatracker.ietf.org/doc/html/rfc2812 +https://datatracker.ietf.org/doc/html/rfc2813 + +# Reqs +## Subject + +### However, you only have to implement the following features: +You must be able to ++ authenticate ++ set a nickname ++ a username ++ join a channel ++ send and receive private messages using your reference client ++ All the messages sent from one client to a channel have to be forwarded to every other client that joined the channel. ++ You must have operators and regular users. ++ Then, you have to implement the commands that are specific to **channel operators**: + + KICK - Eject a client from the channel + + INVITE - Invite a client to a channel + + TOPIC - Change or view the channel topic + + MODE - Change the channel’s mode: + + i: Set/remove Invite-only channel + + t: Set/remove the restrictions of the TOPIC command to channel operators + + k: Set/remove the channel key (password) + + o: Give/take channel operator privilege + + l: Set/remove the user limit to channel + +### Verify every possible error and issue, such as receiving partial data, low bandwidth, etc. + +### Client Commands basic + ++ With both nc and the reference IRC client, check that you can authenticate, set a nickname, a username, join a channel. This should be fine (you should have already done this previously). ++ Verify that private messages (PRIVMSG) are fully functional with different parameters. + + +## Eval +https://wormav.github.io/42_eval/Cursus/Ft_irc/index.html ++ Ask and check how many poll() (or equivalent) are present in the code. **There must be only one.** ++ **MacOS only!** ~~Verify that each call to fcntl() is done as follows: fcntl(fd, F_SETFL, O_NONBLOCK); Any other use of fcntl() is forbidden.~~ + +# difference poll() select() kqueue() epoll() + +**Performance and Scalability**: The primary difference lies in computational complexity and file descriptor handling. **`select()`** and **`poll()`** both operate in **O(n)** time, requiring the kernel to scan all monitored file descriptors on every call, whereas **`epoll()`** and **`kqueue()`** operate in **O(1)** time by maintaining kernel-side state, making them suitable for high-concurrency servers. **`select()`** is further limited by a default **1024 file descriptor cap** and carries risks of stack corruption if exceeded, while **`poll()`** removes this hard limit but retains the linear scanning performance penalty. + +**Portability and Use Cases**: **`select()`** and **`poll()`** are **POSIX-standard** and available across all Unix-like systems, making them ideal for **cross-platform applications** or those with **low concurrency** (<1000 FDs). **`epoll()`** is **Linux-only** (available since kernel 2.5.44) and is the standard for **high-performance Linux servers** handling thousands of connections. **`kqueue()`** is the equivalent high-performance alternative for **BSD-derived systems** (macOS, FreeBSD), offering richer event filtering capabilities than epoll. + +**API Design**: **`select()`** uses bitmask structures which are inefficient for sparse file descriptors, while **`poll()`** uses a sparse array of structures for better handling of large FD ranges. **`epoll()`** separates the registration of file descriptors (`epoll_ctl`) from waiting for events (`epoll_wait`), supporting both **edge-triggered** and **level-triggered** modes. **`kqueue()`** integrates event registration and retrieval into a single **`kevent()`** system call, using a more abstracted structure that supports various event types beyond just I/O. + +| API | Complexity | FD Limit | Portability | Best For | +| :--- | :--- | :--- | :--- | :--- | +| **select()** | O(n) | 1024 (default) | Universal | Legacy code, few FDs (<10) | +| **poll()** | O(n) | No hard limit | POSIX (Universal) | Cross-platform, moderate concurrency | +| **epoll()** | O(1) | System limit | Linux only | High-performance Linux servers | +| **kqueue()** | O(1) | System limit | BSD/macOS | High-performance BSD/macOS servers | + +# std conforming clients + +*we can choose one ref client that will also be used in evaluation* + +> **From the subject pdf:**\ +> Several IRC clients exist. You have to choose one of them as a reference. Your reference client will be used during the evaluation process. + ++ HexChat: Open-source, supports multi-server, UTF-8, TLS, SASL, and various scripting languages. + ++ Irssi: A terminal-based client supporting multi-server, UTF-8, TLS, and SASL, often used for always-on connectivity. + +# Reference implementation +https://github.com/42YerevanProjects/ft_irc/blob/master/src/network/Server.cpp#L263 +https://deepwiki.com/42YerevanProjects/ft_irc/4-protocol-implementation \ No newline at end of file diff --git a/memo/poll.md b/memo/poll.md new file mode 100644 index 0000000..bc1b6ca --- /dev/null +++ b/memo/poll.md @@ -0,0 +1,158 @@ +The **`poll()`** function in C++ (via ``) is a POSIX system call used for **I/O multiplexing**, allowing a program to monitor multiple file descriptors to see if I/O is possible on any of them. It is similar to `select()` but uses an array of `struct pollfd` instead of bitmasks, avoiding the `FD_SETSIZE` limit and offering better performance for large numbers of descriptors. + +### Syntax and Structure + +```cpp +#include + +int poll(struct pollfd *fds, nfds_t nfds, int timeout); +``` + +* **`fds`**: An array of `struct pollfd` specifying the file descriptors and events to monitor. +* **`nfds`**: The number of elements in the `fds` array. +* **`timeout`**: Time to wait in milliseconds. Use **`0`** for non-blocking, **`-1`** for infinite blocking, or a positive integer for a specific duration. + +### Key Event Flags + +* **`POLLIN`**: Data is available for reading. +* **`POLLOUT`**: Data can be written without blocking. +* **`POLLERR`**: An error occurred. +* **`POLLHUP`**: Connection hung up (disconnected). +* **`POLLNVAL`**: Invalid file descriptor. + +### Basic Example + +```cpp +#include +#include +#include + +int main() { + struct pollfd fds[1]; + + // Monitor stdin (file descriptor 0) for input + fds[0].fd = STDIN_FILENO; + fds[0].events = POLLIN; + + // Wait for 5 seconds (5000 ms) + int ret = poll(fds, 1, 5000); + + if (ret < 0) { + std::cerr << "poll error" << std::endl; + } else if (ret == 0) { + std::cout << "Timeout: No data received." << std::endl; + } else { + // Check if the specific event occurred + if (fds[0].revents & POLLIN) { + std::cout << "Data is available on stdin." << std::endl; + } + if (fds[0].revents & POLLHUP) { + std::cout << "Connection closed." << std::endl; + } + } + return 0; +} +``` + +### Windows Support +Standard `poll()` is not natively supported on Windows. For Winsock, use **`WSAPoll()`** or implement a compatibility layer using `select()`. Linux and macOS support `poll()` natively. + + + +### Performance and Scalability +While `poll()` improves upon `select()` by removing the 1024 file descriptor limit and using a flexible array instead of bitmasks, it still suffers from **O(n) performance complexity**. Every call requires the kernel to copy the entire file descriptor array from user space, linearly scan every descriptor to check for events, and copy the results back. This makes `poll()` inefficient for applications monitoring thousands of connections, as the CPU cost grows linearly with the number of descriptors regardless of how many are actually active. For high-concurrency scenarios, **`epoll`** (Linux) or **`kqueue`** (BSD/macOS) are preferred as they use event-driven callbacks to achieve O(1) or O(active) complexity. + +### Timeout Behavior and Granularity +> must be 0 (non-blocking) for ft_irc! + +The `timeout` argument specifies the wait duration in **milliseconds**. +* **`0`**: Returns immediately (non-blocking poll), useful for checking status without waiting. +* **`-1`**: Blocks indefinitely until an event occurs or a signal interrupts the call. +* **Positive Integer**: Blocks for at least the specified time. Note that system timer granularity may round up the actual wait time slightly. +A return value of **`0`** specifically indicates a timeout occurred with no events, whereas **`-1`** indicates an error (check `errno`). + +### Critical Pitfalls and Event Handling +* **Regular Files**: `poll()` always reports regular files as ready for reading/writing immediately. It cannot be used to wait for file content changes; using it on regular files in a loop will cause a "busy spin" consuming 100% CPU. +* **Race Conditions**: A positive result in `revents` (e.g., `POLLIN`) guarantees that a read *would not block at that exact instant*, but it does not guarantee future state. Data could be consumed by another thread or the connection could close before your `read()` call. Always handle `EAGAIN`/`EWOULDBLOCK` even after `poll()` returns ready. +* **Automatic Flags**: The kernel sets `POLLERR`, `POLLHUP`, and `POLLNVAL` in `revents` **even if you did not request them** in `events`. Robust code must always check for these flags to detect connection resets, errors, or invalid descriptors. +* **Array Limits**: Passing an `nfds` value larger than the process's `RLIMIT_NOFILE` (often 1024 by default) will cause `poll()` to fail immediately with `EINVAL`. + +#### note on how to detect whether client closed the connection on their side + POLLHUP isn't useful, because: + When you kill netcat with Ctrl+C: + Netcat's OS kernel sent a FIN packet to your server + Your server's poll() detected this with POLLIN + Your read() returned 0 (zero bytes) == "I got the FIN, connection closed" + FIN is the TCP mechanism for orderly shutdown. + That's why POLLIN + read() return 0—the FIN, + and the zero read is how the kernel tells you "the peer closed." + + +### Windows Compatibility +Standard `poll()` is part of POSIX and is not natively available in the standard Winsock API. Windows provides **`WSAPoll()`** in newer versions (Vista/Server 2008+), but it has historically had bugs and performance issues. Production C++ applications targeting Windows often use `select()`, Overlapped I/O (IOCP), or cross-platform libraries like **`libuv`** or **`Boost.Asio`** to abstract these differences. + +## --- Pitfalls --- + +Common pitfalls when using `poll()` in C++ often stem from misunderstanding how the kernel updates the `pollfd` structure or mismanaging the event loop logic. + +### 1. Misinterpreting `revents` vs `events` +A frequent error is checking the `events` field after the call instead of `revents`. +* **`events`**: Input only. Specifies what you are interested in. It is **not** modified by `poll()`. +* **`revents`**: Output only. Filled by the kernel with actual events that occurred. +* **Pitfall**: Checking `fds[i].events & POLLIN` after the call will always return true if you requested it, regardless of whether data is actually available. You must check `fds[i].revents & POLLIN`. + +### 2. Failing to Consume Data (The "Busy Loop") +`poll()` is level-triggered. If `POLLIN` is set, it means data is available *now*. +* **Pitfall**: If `poll()` returns `POLLIN` but you do not call `read()` (or `recv()`) to consume the data, the next call to `poll()` will immediately return `POLLIN` again. +* **Result**: This creates a tight, 100% CPU usage loop. Always ensure you read all available data or switch to non-blocking I/O with proper handling. + +### 3. Ignoring `POLLHUP`, `POLLERR`, and `POLLNVAL` +These flags are special because they are **always** reported in `revents` if the condition exists, even if you did not request them in `events`. +* **Pitfall**: Only checking for `POLLIN` or `POLLOUT`. If a socket disconnects (`POLLHUP`) or an error occurs (`POLLERR`), `poll()` returns successfully, but `POLLIN` might not be set (or might be set alongside `POLLHUP` depending on the OS). +* **Consequence**: If you don't explicitly check for `POLLHUP` or `POLLERR`, you might keep a dead socket in your array forever, or worse, attempt to write to a closed connection. +* **Best Practice**: Always check `if (revents & (POLLHUP | POLLERR | POLLNVAL))` and close/remove the descriptor immediately. + +### 4. Not Resetting or Managing the `pollfd` Array +Unlike `select()`, `poll()` does not modify the `events` field, but the logic for managing the array can be tricky. +* **Pitfall**: Passing invalid or closed file descriptors in the array without marking them. If an FD is closed but left in the array, `poll()` will set `POLLNVAL` for that entry. +* **Strategy**: When removing a socket, it is common to either compact the array (shift elements) or set the `fd` to a negative value (e.g., `-1`). The standard dictates that if `fd < 0`, `poll()` ignores the entry and sets `revents` to 0. + +### 5. Performance Scalability (O(N) Complexity) +While `poll()` fixes the 1024 FD limit of `select()`, it retains the **O(N)** performance characteristic. +* **Pitfall**: Using `poll()` for thousands of concurrent connections. The kernel must scan the *entire* array passed by the user on every call, regardless of how many FDs are actually active. +* **Impact**: As the number of connections grows, CPU usage increases linearly, even if traffic is low. For high-concurrency servers (10k+ connections), **`epoll`** (Linux) or **`kqueue`** (BSD/macOS) is required. + +### 6. Signal Interruption (`EINTR`) +* probably n/a for ft_irc, since poll is used non-blocking and hence isn't waiting +* BUT: the server process is maybe stopped by a signal, so this must be handled? in the main for loop?\ +-> Maybe it's only Ctrl+C / SIGINT, that is handled "automagically"?\ +-> NO, see **SIGNALS** below +* **Pitfall**: Not handling the return value `-1` with `errno == EINTR`. +* **Context**: If a signal is caught while `poll()` is waiting, it returns early with an error. +* **Fix**: Always wrap `poll()` in a loop that retries if `errno == EINTR`. + +#### SIGNALS +Yes, handling signals is a mandatory requirement for the ft_irc project. + +According to the project specifications, you must handle specific signals to ensure robust client-server interaction: + ++ SIGINT (CTRL+C): To properly terminate the server or client process. ++ CTRL+D: To handle partial messages or end-of-file conditions. ++ CTRL+Z: To handle suspended processes. + +The guidelines also specify that you should use no memory leaks (including still reachable ones) and implement send buffers for each client to efficiently manage these signal interruptions, particularly for CTRL+Z. + +### 7. ERRNO handling +* subject requires to not implement any logic based on ERRNO +* Key Characteristics\ +EAGAIN and EWOULDBLOCK are POSIX error codes (stored in errno) indicating that an operation cannot be completed immediately without blocking, but may succeed if attempted later.\ +Not a Fatal Error: Receiving these codes is normal behavior in non-blocking I/O. It simply means "try again later." It does not indicate a broken connection or a bug. + +### Summary Checklist for Robust Usage +1. Check return value: `-1` (error), `0` (timeout), `>0` (events). +2. Iterate only if return `> 0`. +3. Check `revents` (never `events`) for status. +4. Prioritize checking `POLLERR`, `POLLHUP`, and `POLLNVAL` before data flags. +5. Consume data immediately upon `POLLIN`. +6. Handle `EINTR` by retrying. + diff --git a/memo/socket.md b/memo/socket.md new file mode 100644 index 0000000..ef38369 --- /dev/null +++ b/memo/socket.md @@ -0,0 +1,32 @@ +### sockets +man 2 socket +man 7 socket +man setsockopt +man listen + +> The accept() system call is used with connection-based socket types (SOCK_STREAM, SOCK_SEQPACKET) + + +## for `int listen(int sockfd, int backlog);` can the backlog parameter be set to unlimited? + +**tl;dr: set `backlog` to `SOMAXCONN`** + +No, the `backlog` parameter in `int listen(int sockfd, int backlog);` **cannot be set to unlimited**. + +### Key Constraints + +1. **Silent Truncation**: The `backlog` argument is a **hint**, not a strict command. If you pass a value larger than the system's maximum limit, the kernel **silently truncates** it to that maximum. It does not error out, nor does it create an infinite queue. +2. **System Limit (`somaxconn`)**: On Linux, the effective maximum is defined by the kernel parameter **`net.core.somaxconn`**. + * **Default**: Historically **128**, but increased to **4096** in Linux kernel 5.4+. + * **Behavior**: If you call `listen(sockfd, 10000)` but `somaxconn` is 128, the queue size becomes 128. +3. **Negative Values**: Passing a negative value (e.g., `-1` intending "unlimited") typically results in the backlog being set to **0** (minimum) or a small default, depending on the OS implementation. It does not trigger unlimited behavior. +4. **`SOMAXCONN` Constant**: The constant `SOMAXCONN` (defined in ``) represents the maximum useful backlog value for the platform at compile time. + * **Linux**: Often defined as 4096 or 128 in headers, but the *runtime* limit is what matters (`/proc/sys/net/core/somaxconn`). + * **Windows**: `SOMAXCONN` is defined as `0x7fffffff` (max int), acting as a special flag to tell the OS to use the "maximum reasonable value," but this is still finite and managed by the OS, not truly unlimited. + +### Best Practice +To maximize the backlog: +1. Pass a large integer (e.g., `4096` or higher) to `listen()`. +2. Ensure the system's `net.core.somaxconn` is tuned accordingly (e.g., via `sysctl -w net.core.somaxconn=65535`). +3. The effective backlog will be `min(requested_backlog, somaxconn)`. + diff --git a/playground/main.cpp b/playground/main.cpp new file mode 100644 index 0000000..709adf2 --- /dev/null +++ b/playground/main.cpp @@ -0,0 +1,68 @@ +#include +#include +#include +//#include +#include +#include +#include +#include + +// Global flag to indicate when to exit +// std::atomic should_exit(false); +volatile static std::sig_atomic_t should_exit = 0; + +class A { +private: +std::string s; +public: +A() {s = "Hello";} +~A() { + std::cout << "dtor: " << s << std::endl; +} +}; + +// Signal handler - NO I/O operations here! +extern "C" +void handle_sigint(int sig) { + should_exit = sig; // Only set the flag +} + +// Function to release resources +void cleanup_resources(A *pa) { + std::cout << "[Cleanup] Releasing resources...\n"; + // Close files, free memory, close database connections, etc. + delete pa; + std::cout << "[Cleanup] Resources released successfully.\n"; +} + +const std::pairp(SIGINT, "SIGINT"); +static const std::map sigs(&p, &p+1); + +int main() { + // Install the signal handler for SIGINT + if (std::signal(SIGINT, handle_sigint) == SIG_ERR) { + std::cerr << "std::signal failed\n"; + return 1; + } + + std::cout << "Signal handler installed. Press Ctrl+C to exit gracefully.\n"; + + A *pa = new(A); + A b; + + // Main program loop + while (!should_exit) { + std::cout << "Running... (PID: " << getpid() << ")\n"; + sleep(1); // Simulate work + } + + // Release resources before exiting + std::cout << "\n[Signal Handler] Caught signal: " << + /* sigs.at(should_exit) */ + strsignal(should_exit) + << ". Shutting down gracefully...\n"; + cleanup_resources(pa); + + std::cout << "Program terminated cleanly.\n"; + return 0; +} diff --git a/srcs/diff-bonus b/srcs/diff-bonus new file mode 100644 index 0000000..a07a171 --- /dev/null +++ b/srcs/diff-bonus @@ -0,0 +1,1256 @@ +diff --git a/Makefile b/Makefile +index d8246cb..57acd0f 100644 +--- a/Makefile ++++ b/Makefile +@@ -1,8 +1,8 @@ + # ---- Library and Project names ----------------------------------------------- + NAME = ircserv + DEBUG_NAME = ircserv_debug +-# BONUS = ircserv_bonus +-# DEBUG_BONUS = ircserv_bonus_debug ++BONUS = ircserv_bonus ++DEBUG_BONUS = ircserv_bonus_debug + + # ---- Sources & Objects ------------------------------------------------------- + SRC_DIR = srcs +@@ -29,26 +29,27 @@ SRCS_COMMANDS = CommandProcessor \ + + SRCS_UTILS = MemberSet \ + ModeSet +-# Parser \ +-# Reply + +-# SRCS_BONUS = DCC \ +-# Bot ++SRCS_BONUS = Bot \ ++ TransferManager + + SRCS = $(SRCS_ROOT) \ + $(addprefix server/, $(SRCS_SERVER)) \ + $(addprefix commands/, $(SRCS_COMMANDS)) \ + $(addprefix utils/, $(SRCS_UTILS)) + ++SRCS_ALL_BONUS = $(SRCS) \ ++ $(addprefix bonus/, $(SRCS_BONUS)) ++ + OBJS = $(addprefix $(BUILD_DIR)/$(SRC_DIR)/, $(addsuffix .o, $(SRCS))) + DEBUG_OBJS = $(addprefix $(DEBUG_BUILD_DIR)/$(SRC_DIR)/, $(addsuffix .o, $(SRCS))) +-# BONUS_OBJS = $(OBJS:.o=.bonus.o) +-# DEBUG_BONUS_OBJS = $(DEBUG_OBJS:.o=.bonus.o) ++BONUS_OBJS = $(addprefix $(BUILD_DIR)/$(SRC_DIR)/, $(addsuffix .bonus.o, $(SRCS_ALL_BONUS))) ++DEBUG_BONUS_OBJS= $(addprefix $(DEBUG_BUILD_DIR)/$(SRC_DIR)/, $(addsuffix .bonus.o, $(SRCS_ALL_BONUS))) + + # ---- Auto-generated header deps ---------------------------------------------- + + DEPS = $(OBJS:.o=.d) $(DEBUG_OBJS:.o=.d) +-# BONUS_DEPS = $(BONUS_OBJS:.o=.d) $(DEBUG_BONUS_OBJS:.o=.d) ++BONUS_DEPS = $(BONUS_OBJS:.o=.d) $(DEBUG_BONUS_OBJS:.o=.d) + + # ---- Compiler and Flags ------------------------------------------------------ + +@@ -62,11 +63,11 @@ DEBUG_CXXFLAGS = $(CXXFLAGS) $(DFLAGS) + + all: $(NAME) + +-# bonus: $(BONUS) ++bonus: $(BONUS) + + debug: $(DEBUG_NAME) + +-# debug_bonus: $(DEBUG_BONUS) ++debug_bonus: $(DEBUG_BONUS) + + # clang-format + format: +@@ -91,11 +92,11 @@ $(NAME): $(OBJS) + $(DEBUG_NAME): $(DEBUG_OBJS) + $(CXX) $(DEBUG_CXXFLAGS) $(DEBUG_OBJS) -o $@ + +-# $(BONUS): $(BONUS_OBJS) +-# $(CXX) $(CXXFLAGS) $(BONUS_OBJS) -o $@ ++$(BONUS): $(BONUS_OBJS) ++ $(CXX) $(CXXFLAGS) $(BONUS_OBJS) -o $@ + +-# $(DEBUG_BONUS): $(DEBUG_BONUS_OBJS) +-# $(CXX) $(DEBUG_CXXFLAGS) $(DEBUG_BONUS_OBJS) -o $@ ++$(DEBUG_BONUS): $(DEBUG_BONUS_OBJS) ++ $(CXX) $(DEBUG_CXXFLAGS) $(DEBUG_BONUS_OBJS) -o $@ + + $(BUILD_DIR)/$(SRC_DIR)/%.o: $(SRC_DIR)/%.cpp + @mkdir -p $(@D) +@@ -105,13 +106,13 @@ $(DEBUG_BUILD_DIR)/$(SRC_DIR)/%.o: $(SRC_DIR)/%.cpp + @mkdir -p $(@D) + $(CXX) $(DEBUG_CXXFLAGS) $(IFLAGS) -MMD -MP -c $< -o $@ + +-# $(BUILD_DIR)/$(SRC_DIR)/%.bonus.o: $(SRC_DIR)/%.cpp +-# @mkdir -p $(@D) +-# $(CXX) $(CXXFLAGS) $(IFLAGS) -MMD -MP -c -DBONUS $< -o $@ ++$(BUILD_DIR)/$(SRC_DIR)/%.bonus.o: $(SRC_DIR)/%.cpp ++ @mkdir -p $(@D) ++ $(CXX) $(CXXFLAGS) $(IFLAGS) -MMD -MP -c -DBONUS $< -o $@ + +-# $(DEBUG_BUILD_DIR)/$(SRC_DIR)/%.bonus.o: $(SRC_DIR)/%.cpp +-# @mkdir -p $(@D) +-# $(CXX) $(DEBUG_CXXFLAGS) $(IFLAGS) -MMD -MP -c -DBONUS $< -o $@ ++$(DEBUG_BUILD_DIR)/$(SRC_DIR)/%.bonus.o: $(SRC_DIR)/%.cpp ++ @mkdir -p $(@D) ++ $(CXX) $(DEBUG_CXXFLAGS) $(IFLAGS) -MMD -MP -c -DBONUS $< -o $@ + + # ---- Cleanup ----------------------------------------------------------------- + clean: +@@ -122,7 +123,6 @@ fclean: clean + + re: fclean all + +--include $(DEPS) +-# -include $(DEPS) $(BONUS_DEPS) ++-include $(DEPS) $(BONUS_DEPS) + + .PHONY: all bonus clean fclean re debug debug_bonus format tidy tidy-all +diff --git a/includes/Bot.hpp b/includes/Bot.hpp +new file mode 100644 +index 0000000..d603397 +--- /dev/null ++++ b/includes/Bot.hpp +@@ -0,0 +1,34 @@ ++#ifdef BONUS ++ ++#pragma once ++ ++#include ++#include ++ ++class Client; ++class Server; ++ ++class Bot { ++ std::string name_; ++ bool isBotCommand_(const std::string ¶m) const; ++ ++ public: ++ Bot(); ++ ++ std::string getPrefix(const std::string &serverName) const; ++ std::string getVersion(const std::string &serverName) const; ++ ++ bool isForBot(Server &server, const std::vector ¶ms) const; ++ ++ void handleMessage(Server &server, Client &client, ++ const std::string &body) const; ++ void handleSend(Server &server, Client &client, const std::string &nick, ++ const std::string &filename) const; ++ void handleCTCP(const std::string &serverName, Client &client, ++ const std::string &body) const; ++ ++ std::string unknown() const; ++ std::string help() const; ++}; ++ ++#endif +diff --git a/includes/CTCP.hpp b/includes/CTCP.hpp +new file mode 100644 +index 0000000..f060cdd +--- /dev/null ++++ b/includes/CTCP.hpp +@@ -0,0 +1,31 @@ ++#ifdef BONUS ++ ++#pragma once ++ ++#include ++ ++namespace CTCP { ++static const char SOH = '\x01'; ++static const std::string strSOH = "\x01"; ++ ++static inline bool isCTCP(const std::string &body) { ++ return !body.empty() && body[0] == SOH; ++} ++ ++// strip starting SOF (\x01), strip ending SOH if present ++static inline std::string extractCTCP(const std::string &body) { ++ std::string out = body.substr(1); ++ if (!out.empty() && out[out.size() - 1] == SOH) ++ out.erase(out.size() - 1); ++ return out; ++} ++ ++// Builds "\x01\x01". Used by bot ++static inline std::string buildCTCP(const std::string payload) { ++ std::string out = strSOH + payload + SOH; ++ return out; ++} ++ ++} // namespace CTCP ++ ++#endif +diff --git a/includes/Server.hpp b/includes/Server.hpp +index ce66f81..ac886a8 100644 +--- a/includes/Server.hpp ++++ b/includes/Server.hpp +@@ -6,6 +6,11 @@ + #include "Logger.hpp" // for Logger + #include "Socket.hpp" // for Socket + ++#ifdef BONUS ++#include "Bot.hpp" // for Bot ++#include "TransferManager.hpp" ++#endif ++ + #include // for pollfd + #include // for size_t + #include // for string, basic_string +@@ -30,6 +35,22 @@ class Server { + ClientManager clients_; + CommandProcessor cmds_; + ++#ifdef BONUS ++ struct PollFd { ++ struct pollfd pfd; ++ ++ PollFd(int fd, short events) { ++ pfd.fd = fd; ++ pfd.events = events; ++ pfd.revents = 0; ++ } ++ }; ++ ++ TransferManager tm_; ++ Bot bot_; ++ void processTransferSocket_(); ++#endif ++ + size_t numServerSockets_; + std::vector pollFds_; + +@@ -60,4 +81,9 @@ class Server { + ClientManager &getClientManager(); + + void sendMsg(int const fd, std::string const &msg); ++ ++#ifdef BONUS ++ TransferManager &getTransferManager(); ++ Bot &getBot(); ++#endif + }; +diff --git a/includes/TransferManager.hpp b/includes/TransferManager.hpp +new file mode 100644 +index 0000000..406f41f +--- /dev/null ++++ b/includes/TransferManager.hpp +@@ -0,0 +1,70 @@ ++#ifdef BONUS ++ ++#pragma once ++ ++#include ++#include ++#include ++ ++class Client; ++ ++enum Status { Pending, Accepted, Rejected }; ++ ++struct Transfer { ++ int clientFd; ++ int targetFd; ++ std::string filename; ++ std::string filepath; // "files/" + filename ++ off_t filesize; ++ int listenerFd; ++ int dataFd; ++ int port; ++ off_t offset; ++ Status status; ++ time_t lastActivity; ++ ++ Transfer() ++ : clientFd(-1), targetFd(-1), filesize(0), listenerFd(-1), dataFd(-1), ++ port(0), offset(0), status(Pending), lastActivity(std::time(0)) {}; ++ ++ Transfer(int client, int target, std::string name, std::string path, ++ off_t size) ++ : clientFd(client), targetFd(target), filename(name), filepath(path), ++ filesize(size), listenerFd(-1), dataFd(-1), port(0), offset(0), ++ status(Pending), lastActivity(std::time(0)) {} ++}; ++ ++class TransferManager { ++ bool active_; // one transfer at a time. true = busy. ++ Transfer current_; ++ ++ bool openListener_(); ++ void closeFds_(); ++ bool fileSize_(const std::string &path, off_t &size) const; ++ static void setNonBlocking_(int fd); ++ ++ TransferManager(const TransferManager &); ++ TransferManager &operator=(const TransferManager &); ++ ++ public: ++ TransferManager(); ++ ~TransferManager(); ++ ++ std::string openOffer(int clientFd, int targetFd, ++ const std::string &filename); ++ bool setOfferStatus(int targetFd, bool accept); ++ void closeOffer(); ++ bool expired() const; ++ ++ int getDataFd(); ++ ++ ssize_t sendChunk(); ++ ++ int getListenerFd() const; ++ bool isActive() const; ++ Transfer *current(); ++ bool ownsFd(int fd) const; ++ bool transferDone() const; ++}; ++ ++#endif +diff --git a/includes/Utils.hpp b/includes/Utils.hpp +index 3f84ea1..904456a 100644 +--- a/includes/Utils.hpp ++++ b/includes/Utils.hpp +@@ -57,4 +57,15 @@ static inline std::string join3(const std::string &a, const std::string &b, + return a + " " + b + " " + (c.empty() ? "" : " " + c); + } + ++#ifdef BONUS ++ ++static inline std::string trimPrefix(const std::string &s, ++ const std::string &prefix) { ++ if (s.compare(0, prefix.size(), prefix) == 0) ++ return s.substr(prefix.size()); ++ return s; ++} ++ ++#endif ++ + } // namespace Utils +diff --git a/srcs/bonus/Bot.cpp b/srcs/bonus/Bot.cpp +new file mode 100644 +index 0000000..05f9213 +--- /dev/null ++++ b/srcs/bonus/Bot.cpp +@@ -0,0 +1,167 @@ ++#ifdef BONUS ++ ++#include "Bot.hpp" ++ ++#include "CTCP.hpp" ++#include "Client.hpp" ++#include "Errors.hpp" ++#include "Reply.hpp" ++#include "Server.hpp" ++#include "TransferManager.hpp" ++#include "Utils.hpp" ++ ++#include ++ ++Bot::Bot() : name_("DCCBot") {} ++ ++// ---- Identifiers ------------------------------------------------------------ ++ ++std::string Bot::getPrefix(const std::string &serverName) const { ++ return name_ + "!bot@" + serverName; ++} ++ ++std::string Bot::getVersion(const std::string &serverName) const { ++ return "I am " + name_ + " on " + serverName; ++} ++ ++// ---- Command recognition ---------------------------------------------------- ++ ++/* ++PRIVMSG ++ ++Targeted to bot (bot command or ctcp) ++ bot / #bot / dccbot ++Bot command ++ #chan ++ ++NOTE: Should be called only when params > 1 && !params[0].empty() && !params[1].empty(). ++This will intercept any user with nickname bot / BOT / #bot -> but OK for bonus. ++*/ ++bool Bot::isForBot(Server &server, ++ const std::vector ¶ms) const { ++ const std::string &target = Utils::foldCase(params[0]); ++ if (target == Utils::foldCase(name_) || target == "bot" || target == "#bot") ++ return true; ++ return (server.getChannelManager().isChannelName(params[0]) && ++ isBotCommand_(params[1])); ++} ++ ++bool Bot::isBotCommand_(const std::string &body) const { ++ if (body.empty()) ++ return false; ++ std::vector words = Utils::split(body, ' '); ++ std::string cmd = Utils::trimPrefix(Utils::foldCase(words[0]), "!"); ++ if (cmd.empty()) ++ return false; ++ ++ static const std::string botCmds[] = {"help", "version", "whoami", ++ "send", "accept", "reject"}; ++ static const size_t totalbotCmds = sizeof(botCmds) / sizeof(*botCmds); ++ for (size_t i = 0; i < totalbotCmds; i++) { ++ if (botCmds[i] == cmd) ++ return true; ++ } ++ return false; ++} ++ ++// ---- Command handlers ------------------------------------------------------- ++ ++// bot ++// #chan ++void Bot::handleMessage(Server &server, Client &client, ++ const std::string &body) const { ++ std::vector words = Utils::split(body, ' '); ++ std::string cmd = Utils::foldCase(words.empty() ? "" : words[0]); ++ cmd = Utils::trimPrefix(cmd, "!"); ++ std::string msg; ++ if (cmd.empty() || cmd == "help") ++ msg = help(); ++ else if (cmd == "version") ++ msg = getVersion(server.getServerName()); ++ else if (cmd == "whoami") ++ msg = "You are " + client.getNick(); ++ else if (cmd == "send") { ++ if (words.size() < 3 || words[1].empty() || words[2].empty()) ++ client.sendReply(Reply::reply(Reply::ERR_NEEDMOREPARAMS, "SEND")); ++ else ++ handleSend(server, client, words[1], words[2]); ++ return; ++ } else if (cmd == "accept" || cmd == "reject") { ++ bool ok = server.getTransferManager().setOfferStatus(client.getFd(), ++ cmd == "accept"); ++ msg = ok ? (cmd == "accept" ? "transfer accepted - connect to receive" ++ : "transfer rejected") ++ : "no pending transfers for you"; ++ } else ++ msg = unknown(); ++ client.send(Reply::fromClient(getPrefix(server.getServerName()), "PRIVMSG", ++ client.getNick(), msg)); ++} ++ ++/* ++bot !send ++ bot is initiator, opens a listener, ++ replies buildCTCP("DCC SEND ...") in PRIVMSG to requester ++ ++NOTE: DCC receiver connects to the offer IP. ++127.0.0.1 macOS returns broken accepted socket (SO_ERROR=9 / POLLHUP) when a ++process connects to its own LAN IP, which breaks the transfers for same-host ++receivers ++*/ ++void Bot::handleSend(Server &server, Client &client, const std::string &nick, ++ const std::string &filename) const { ++ try { ++ Client &target = server.getClientManager().at(nick); ++ TransferManager &tm = server.getTransferManager(); ++ std::string err = ++ tm.openOffer(client.getFd(), target.getFd(), filename); ++ if (!err.empty()) { ++ client.send(Reply::fromClient(getPrefix(server.getServerName()), ++ "PRIVMSG", client.getNick(), err)); ++ return; ++ } ++ Transfer *t = tm.current(); ++ std::ostringstream dcc; ++ dcc << "DCC SEND " << t->filename << " 127.0.0.1 " << t->port << " " ++ << t->filesize; ++ std::string msg = CTCP::buildCTCP(dcc.str()); ++ target.send(Reply::fromClient(getPrefix(server.getServerName()), ++ "PRIVMSG", target.getNick(), msg)); ++ client.send(Reply::fromClient(getPrefix(server.getServerName()), ++ "PRIVMSG", client.getNick(), ++ "offer sent to " + target.getNick())); ++ } catch (Error &e) { ++ if (e.hasReply()) ++ client.sendReply(e.reply()); ++ } ++} ++ ++void Bot::handleCTCP(const std::string &serverName, Client &client, ++ const std::string &body) const { ++ std::string ctcp = CTCP::extractCTCP(body); ++ std::vector words = Utils::split(ctcp, ' '); ++ const std::string &text = Utils::foldCase(words.empty() ? "" : words[0]); ++ std::string msg; ++ if (text == "version") ++ msg = getVersion(serverName); ++ else if (text.compare(0, 4, "ping") == 0) ++ msg = ctcp; ++ else ++ msg = unknown(); ++ client.send(Reply::fromClient(getPrefix(serverName), "NOTICE", ++ client.getNick(), CTCP::buildCTCP(msg))); ++} ++ ++// ---- Response text ---------------------------------------------------------- ++ ++// should reply "Do you mean xxx" -> like a compiler. ++// make this a bit smarter? ++std::string Bot::unknown() const { ++ return "Unknown command. Try !help"; ++} ++ ++std::string Bot::help() const { ++ return "Commands: !help, !version, !whoami, !send, !accept, !reject"; ++} ++ ++#endif +diff --git a/srcs/bonus/TransferManager.cpp b/srcs/bonus/TransferManager.cpp +new file mode 100644 +index 0000000..5fc35cf +--- /dev/null ++++ b/srcs/bonus/TransferManager.cpp +@@ -0,0 +1,209 @@ ++#ifdef BONUS ++ ++#include "TransferManager.hpp" ++ ++#include "Client.hpp" ++ ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++ ++static const int CHUNK_SIZE = 1024; ++static const time_t STALE_TIMEOUT = 60; ++static const int LISTENER_BACKLOG = 8; ++static const char *FILES_DIR = "files/"; ++ ++TransferManager::TransferManager() : active_(false) {} ++ ++TransferManager::~TransferManager() { ++ closeFds_(); ++} ++ ++// ---- Offer lifecycle -------------------------------------------------------- ++ ++// returns error msg or "" ++std::string TransferManager::openOffer(int clientFd, int targetFd, ++ const std::string &filename) { ++ if (active_) { ++ if (!expired()) ++ return "bot busy"; ++ closeOffer(); ++ } ++ if (filename.find('/') != std::string::npos || ++ filename.find("..") != std::string::npos) ++ return "invalid filename"; ++ std::string filepath = std::string(FILES_DIR) + filename; ++ off_t size = 0; ++ if (!fileSize_(filepath, size)) ++ return std::string("file not found under ") + FILES_DIR; ++ current_ = Transfer(clientFd, targetFd, filename, filepath, size); ++ if (!openListener_()) { ++ closeFds_(); ++ return "socket error, try again later"; ++ } ++ active_ = true; ++ return ""; ++} ++ ++bool TransferManager::setOfferStatus(int targetFd, bool accept) { ++ if (!active_ || current_.status != Pending || current_.targetFd != targetFd) ++ return false; ++ current_.status = accept ? Accepted : Rejected; ++ if (!accept) ++ closeOffer(); ++ return true; ++} ++ ++void TransferManager::closeOffer() { ++ closeFds_(); ++ active_ = false; ++} ++ ++bool TransferManager::expired() const { ++ return active_ && std::time(0) - current_.lastActivity > STALE_TIMEOUT; ++} ++ ++// ---- Listener & connection -------------------------------------------------- ++ ++bool TransferManager::openListener_() { ++ struct sockaddr_in addr; ++ std::memset(&addr, 0, sizeof(addr)); ++ addr.sin_family = AF_INET; ++ addr.sin_addr.s_addr = htonl(INADDR_ANY); ++ addr.sin_port = htons(0); // let OS pick a port ++ ++ int fd = ::socket(AF_INET, SOCK_STREAM, 0); ++ if (fd < 0) ++ return false; ++ ++ int opt = 1; ++ if (::setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)) < 0) { ++ ::close(fd); ++ return false; ++ } ++ ++ if (::bind(fd, reinterpret_cast(&addr), sizeof(addr)) < ++ 0) { ++ ::close(fd); ++ return false; ++ } ++ if (::listen(fd, LISTENER_BACKLOG) < 0) { ++ ::close(fd); ++ return false; ++ } ++ setNonBlocking_(fd); ++ ++ socklen_t len = sizeof(addr); ++ if (::getsockname(fd, reinterpret_cast(&addr), &len) < ++ 0) { ++ ::close(fd); ++ return false; ++ } ++ ++ current_.listenerFd = fd; ++ current_.port = ntohs(addr.sin_port); ++ return true; ++} ++ ++// accept() receiver's connection ++int TransferManager::getDataFd() { ++ if (!active_ || current_.status != Accepted || current_.listenerFd == -1) ++ return -1; ++ if (current_.dataFd != -1) ++ return current_.dataFd; ++ ++ struct sockaddr_in addr; ++ socklen_t len = sizeof(addr); ++ int fd = ::accept(current_.listenerFd, ++ reinterpret_cast(&addr), &len); ++ if (fd < 0) ++ return -1; ++ setNonBlocking_(fd); ++ current_.dataFd = fd; ++ return fd; ++} ++ ++// ---- Transfer IO ------------------------------------------------------------ ++ ++// send 1KB ++ssize_t TransferManager::sendChunk() { ++ if (!active_ || current_.status != Accepted || current_.dataFd == -1) ++ return -1; ++ ++ std::ifstream f(current_.filepath.c_str(), std::ios::in | std::ios::binary); ++ if (!f.is_open()) ++ return -1; ++ f.seekg(current_.offset); ++ char buf[CHUNK_SIZE]; ++ f.read(buf, CHUNK_SIZE); ++ std::streamsize n = f.gcount(); ++ if (n <= 0) ++ return 0; // EOF ++ ssize_t sent = ::send(current_.dataFd, buf, n, MSG_NOSIGNAL); ++ if (sent <= 0) ++ return 0; ++ current_.lastActivity = std::time(0); ++ current_.offset += sent; ++ return sent; ++} ++ ++// ---- State ------------------------------------------------------------------ ++ ++int TransferManager::getListenerFd() const { ++ return active_ ? current_.listenerFd : -1; ++} ++ ++bool TransferManager::isActive() const { ++ return active_; ++} ++ ++Transfer *TransferManager::current() { ++ return active_ ? ¤t_ : 0; ++} ++ ++bool TransferManager::ownsFd(int fd) const { ++ return active_ && (fd == current_.listenerFd || fd == current_.dataFd); ++} ++ ++bool TransferManager::transferDone() const { ++ return active_ && (current_.status == Accepted && ++ current_.offset >= current_.filesize); ++} ++ ++// ---- Helpers ---------------------------------------------------------------- ++ ++void TransferManager::closeFds_() { ++ if (current_.listenerFd != -1) { ++ ::close(current_.listenerFd); ++ current_.listenerFd = -1; ++ } ++ if (current_.dataFd != -1) { ++ ::close(current_.dataFd); ++ current_.dataFd = -1; ++ } ++} ++ ++bool TransferManager::fileSize_(const std::string &path, off_t &size) const { ++ std::ifstream f(path.c_str(), std::ios::in | std::ios::binary); ++ if (!f.is_open()) ++ return false; ++ f.seekg(0, std::ios::end); ++ std::streamoff p = f.tellg(); ++ if (p < 0) ++ return false; ++ size = static_cast(p); ++ return true; ++} ++ ++void TransferManager::setNonBlocking_(int fd) { ++ int flags = ::fcntl(fd, F_GETFL, 0); ++ if (flags != -1) ++ ::fcntl(fd, F_SETFL, flags | O_NONBLOCK); ++} ++ ++#endif +diff --git a/srcs/commands/Messaging.cpp b/srcs/commands/Messaging.cpp +index cd1a4ba..0866804 100644 +--- a/srcs/commands/Messaging.cpp ++++ b/srcs/commands/Messaging.cpp +@@ -27,6 +27,11 @@ + #include "Server.hpp" + #include "Utils.hpp" + ++#ifdef BONUS ++#include "Bot.hpp" ++#include "CTCP.hpp" ++#endif ++ + #include + #include + +@@ -46,20 +51,10 @@ void deliverTo(Server &server, Client &from, const std::string &target, + server.getClientManager().at(target).send(line); // throws 401 + } + ++// call checkMinParam() before calling deliver() + bool deliver(Server &server, Client &client, + const std::vector ¶ms, const std::string &cmd, + bool silent) { +- if (params.empty() || params[0].empty()) { +- if (!silent) +- client.sendReply(Reply::reply(Reply::ERR_NORECIPIENT, "", +- "No recipient given (" + cmd + ")")); +- return false; +- } +- if (params.size() < 2 || params[1].empty()) { +- if (!silent) +- client.sendReply(Reply::reply(Reply::ERR_NOTEXTTOSEND)); +- return false; +- } + const std::vector targets = Utils::split(params[0], ','); + for (size_t i = 0; i < targets.size(); i++) { + if (targets[i].empty()) +@@ -76,14 +71,45 @@ bool deliver(Server &server, Client &client, + return false; + } + ++// Checks min parameters. Sends error message and returns false on error. ++bool checkMinParam(Client &client, const std::vector ¶ms, ++ bool silent) { ++ if (params.empty() || params[0].empty()) { ++ if (!silent) ++ client.sendReply(Reply::reply(Reply::ERR_NORECIPIENT, "", ++ "No recipient given (PRIVMSG)")); ++ return false; ++ } ++ if (params.size() < 2 || params[1].empty()) { ++ if (!silent) ++ client.sendReply(Reply::reply(Reply::ERR_NOTEXTTOSEND, "")); ++ return false; ++ } ++ return true; ++} ++ + } // namespace + + bool Handlers::privmsg(Server &server, Client &client, + const std::vector ¶ms) { ++ if (!checkMinParam(client, params, false)) ++ return false; ++#ifdef BONUS ++ Bot &bot = server.getBot(); ++ if (bot.isForBot(server, params)) { ++ if (CTCP::isCTCP(params[1])) ++ bot.handleCTCP(server.getServerName(), client, params[1]); ++ else ++ bot.handleMessage(server, client, params[1]); ++ return false; ++ } ++#endif + return deliver(server, client, params, "PRIVMSG", false); + } + + bool Handlers::notice(Server &server, Client &client, + const std::vector ¶ms) { ++ if (!checkMinParam(client, params, true)) ++ return false; + return deliver(server, client, params, "NOTICE", true); + } +diff --git a/srcs/server/Channel.cpp b/srcs/server/Channel.cpp +index 39e312b..7aef362 100644 +--- a/srcs/server/Channel.cpp ++++ b/srcs/server/Channel.cpp +@@ -255,7 +255,8 @@ bool Channel::applyModeToMember_(Server &server, Client &client, + client.sendReply(Reply::reply(Reply::ERR_USERNOTINCHANNEL, + change.arg + " " + name_)); + } catch (Error &e) { +- client.sendReply(e.reply()); ++ if (e.hasReply()) ++ client.sendReply(e.reply()); + } + return false; + } +diff --git a/srcs/server/Server.cpp b/srcs/server/Server.cpp +index 055ef86..2e8bb84 100644 +--- a/srcs/server/Server.cpp ++++ b/srcs/server/Server.cpp +@@ -4,6 +4,10 @@ + #include "ServerHelper.hpp" // for getAdressFamilyStr, composeAddrfamAddrP... + #include "SignalHandler.hpp" // for SignalHandler + ++#ifdef BONUS ++#include "Bot.hpp" // for Bot ++#endif ++ + #include // for find_if + #include // for exception + #include // for IPPROTO_IPV6, IPV6_V6ONLY +@@ -120,6 +124,10 @@ void Server::processClientSockets_() { + !SignalHandler::stopLoopSignalReceived() && idx < pollFds_.size(); + ++idx) { + pollfd &pfd = pollFds_.at(idx); ++#ifdef BONUS ++ if (tm_.ownsFd(pfd.fd)) ++ continue; ++#endif + + if (!(sockets_.checkSocket4Recv(pfd) && (pfd.revents & POLLIN))) + continue; +@@ -145,6 +153,10 @@ void Server::processClientSockets_() { + void Server::removeClosedSockets_() { + for (size_t idx = numServerSockets_; idx < pollFds_.size(); ++idx) { + pollfd &pfd = pollFds_.at(idx); ++#ifdef BONUS ++ if (tm_.ownsFd(pfd.fd)) ++ continue; ++#endif + if (sockets_.socketClosed(pfd.fd)) { + clients_.remove(pfd.fd, channels_); + removePollFd_(pfd.fd); +@@ -243,7 +255,12 @@ void Server::run() { + return; + + while (!SignalHandler::stopLoopSignalReceived()) { +- int pollResult = poll(pollFds_.data(), pollFds_.size(), -1); ++ int timeout = -1; ++#ifdef BONUS ++ if (tm_.isActive()) ++ timeout = 100; ++#endif ++ int pollResult = poll(pollFds_.data(), pollFds_.size(), timeout); + if (pollResult == -1) { + if (SignalHandler::stopLoopSignalReceived()) + break; +@@ -253,6 +270,9 @@ void Server::run() { + + processServerSocketData_(); + processClientSockets_(); ++#ifdef BONUS ++ processTransferSocket_(); ++#endif + } + } + +@@ -269,3 +289,73 @@ void Server::sendMsg(int const fd, std::string const &msg) { + #endif + } + } ++ ++#ifdef BONUS ++TransferManager &Server::getTransferManager() { ++ return tm_; ++} ++ ++Bot &Server::getBot() { ++ return bot_; ++} ++ ++void Server::processTransferSocket_() { ++ if (!tm_.isActive()) ++ return; ++ if (tm_.expired()) { ++ tm_.closeOffer(); ++ return; ++ } ++ int listener = tm_.getListenerFd(); ++ if (listener == -1) ++ return; ++ int dataFd = tm_.getDataFd(); ++ struct pollfd pfds[2]; ++ int n = 1; ++ pfds[0] = PollFd(listener, POLLIN).pfd; ++ if (dataFd != -1) { ++ pfds[1] = PollFd(dataFd, POLLOUT).pfd; ++ n = 2; ++ } ++ if (poll(pfds, n, 0) <= 0) ++ return; ++ if (pfds[0].revents & (POLLERR | POLLNVAL)) { ++ tm_.closeOffer(); ++ return; ++ } ++ if (pfds[0].revents & POLLIN) { ++ dataFd = tm_.getDataFd(); ++ if (dataFd < 0) ++ return; ++ pfds[1] = PollFd(dataFd, POLLOUT).pfd; ++ n = 2; ++ if (poll(pfds, n, 0) <= 0) ++ return; ++ } ++ if (n != 2) ++ return; ++ if (pfds[1].revents & (POLLERR | POLLNVAL)) { ++ tm_.closeOffer(); ++ return; ++ } ++ if (!(pfds[1].revents & (POLLOUT | POLLHUP))) ++ return; ++ ssize_t sent = 0; ++ do { ++ sent = tm_.sendChunk(); ++ } while (sent > 0); ++ std::string msg; ++ if (tm_.transferDone()) { ++ msg = "transfer complete"; ++ } else if (sent < 0) ++ msg = "transfer failed"; ++ else ++ return; ++ Client *client = clients_.find(tm_.current()->clientFd); ++ if (client) ++ client->send(Reply::fromClient(bot_.getPrefix(serverName_), "PRIVMSG", ++ client->getNick(), msg)); ++ tm_.closeOffer(); ++} ++ ++#endif +diff --git a/srcs/server/Socket.cpp b/srcs/server/Socket.cpp +index 198558c..85f71b0 100644 +--- a/srcs/server/Socket.cpp ++++ b/srcs/server/Socket.cpp +@@ -48,6 +48,9 @@ bool Socket::updateBuffer_(int const fd) { + return true; + } + ++/* ++Tolerate lines ending with '\n' and "\r\n". ++*/ + std::string Socket::getCRLFLineFromBuffer(int const fd, bool &buffHasMoreCmds) { + if (!buffHasMoreCmds) { + if (!updateBuffer_(fd)) +@@ -55,30 +58,31 @@ std::string Socket::getCRLFLineFromBuffer(int const fd, bool &buffHasMoreCmds) { + } + + bool msgTooLong = false; +- size_t lfcrPos = bufferLengthAndSanityCheck_(fd, msgTooLong); ++ size_t eol = bufferLengthAndSanityCheck_(fd, msgTooLong); + if (msgTooLong) + return std::string(MAXMSGLEN + 1, '\0'); +- if (lfcrPos == std::string::npos) ++ if (eol == std::string::npos) + return ""; + + Socket::SocketIOData &sIOD = sockIODataStore_.at(fd); +- std::string cmdLine; +- cmdLine = sIOD.readBuffer.substr(0, lfcrPos); ++ std::string cmdLine = sIOD.readBuffer.substr(0, eol); ++ if (!cmdLine.empty() && *cmdLine.rbegin() == '\r') ++ cmdLine.erase(cmdLine.size() - 1); + sIOD.recvErrorCount = 0; +- sIOD.readBuffer = sIOD.readBuffer.substr(lfcrPos + 2); +- buffHasMoreCmds = (sIOD.readBuffer.find("\r\n") != std::string::npos); ++ sIOD.readBuffer = sIOD.readBuffer.substr(eol + 1); ++ buffHasMoreCmds = (sIOD.readBuffer.find('\n') != std::string::npos); + return cmdLine; + } + + size_t Socket::bufferLengthAndSanityCheck_(int const fd, bool &msgTooLong) { + Socket::SocketIOData &sIOD = sockIODataStore_.at(fd); + std::string &rdBuffer = sIOD.readBuffer; +- size_t lfcrPos = rdBuffer.find("\r\n"); ++ size_t eol = rdBuffer.find('\n'); + + if (!sIOD.discardMode) { + if ((rdBuffer.length() < MAXMSGLEN) || +- (lfcrPos != std::string::npos && lfcrPos <= MAXMSGLEN - 2)) +- return lfcrPos; ++ (eol != std::string::npos && eol <= MAXMSGLEN - 2)) ++ return eol; + sIOD.discardMode = true; + msgTooLong = true; + } +@@ -87,18 +91,18 @@ size_t Socket::bufferLengthAndSanityCheck_(int const fd, bool &msgTooLong) { + closeSocket(fd); + return std::string::npos; + } +- if (lfcrPos != std::string::npos) { +- rdBuffer.erase(0, lfcrPos + 2); ++ if (eol != std::string::npos) { ++ rdBuffer.erase(0, eol + 1); + sIOD.discardMode = false; + sIOD.recvErrorCount = 0; +- return rdBuffer.find("\r\n"); ++ return rdBuffer.find('\n'); + } +- // lfcrPos == std::string::npos ++ // eol == std::string::npos + if (!rdBuffer.empty() && '\r' == *rdBuffer.rbegin()) + rdBuffer = "\r"; + else + rdBuffer.clear(); +- return lfcrPos; ++ return eol; + } + + /* +diff --git a/tests/test_bot.sh b/tests/test_bot.sh +new file mode 100755 +index 0000000..80fe3b5 +--- /dev/null ++++ b/tests/test_bot.sh +@@ -0,0 +1,103 @@ ++#!/bin/bash ++ ++# DCC bot lifecycle test — no irssi/netcat needed. ++# Uses bash /dev/tcp: one fd per IRC session, kept open for the whole test. ++ ++# Usage: ./test_bot.sh [host] [port] [password] [testfile] ++# Requires: server running (./ircserv_bonus PORT PASS, files/ present) ++ ++set -u ++ ++HOST=${1:-localhost} ++PORT=${2:-6667} ++PASS=${3:-pass} ++FILE=${4:-test.txt} ++BOT=bot ++ ++PASSED=0 ++FAILED=0 ++ ++exec 3<>"/dev/tcp/$HOST/$PORT" 2>/dev/null || { echo "cannot connect"; exit 1; } # alice ++exec 4<>"/dev/tcp/$HOST/$PORT" 2>/dev/null || { echo "cannot connect"; exit 1; } # bob ++ ++send() { # fd rest... ++ local fd=$1; shift ++ printf '%s\r\n' "$@" >&"$fd" ++} ++ ++# wait4