1 — Introduction to IRC Servers:
An Internet Relay Chat (IRC) server is a crucial component in the infrastructure of IRC networks, facilitating real-time communication among users globally. IRC servers act as hubs where users connect to exchange messages in chat rooms (channels) or directly with each other. This decentralized model allows for a robust and flexible communication platform that has persisted since its inception in the late 1980s.
How IRC Servers Work
Connection Establishment: Users connect to an IRC server using a client software capable of handling IRC protocols. These clients initiate a connection to the server, typically on a specific port (usually 6667 or 6697 for SSL/TLS).
Authentication and Identification: Upon connecting, users may need to authenticate themselves using a nickname (nick) and optional credentials (like passwords). This process ensures that users can be identified within the network.
Channel and Private Messaging: Users can join various channels based on topics or interests. Channels are virtual spaces where multiple users can exchange messages simultaneously. Additionally, users can communicate privately with each other via direct messaging.
Server Communication: IRC servers communicate among themselves to synchronize channels and user information across the network. This synchronization ensures that users can see the same list of channels and users, regardless of which server they are connected to within the network.
Commands and Services: IRC networks often provide additional services through bots and automated systems. These services can include channel management (like creating or maintaining channels), user authentication, and network-wide messaging.
Examples of IRC Servers
Several software implementations serve as IRC servers, each with its own features and configurations:
- ircd-hybrid: A popular open-source IRC server known for its stability and scalability.
- InspIRCd: Another widely used open-source IRC server with extensive customization options.
- ircd-seven: Part of the IRCD-Hybrid family, focusing on improvements and added features.
These servers, along with others, form the backbone of various IRC networks, catering to different communities and needs.
2— Process of creating an IRC:
Creating an IRC server involves setting up a platform that facilitates real-time communication between clients in a chat-based environment. Unlike traditional IRC setups that include server-to-server connections for network federation, my IRC project focuses solely on client-server interactions. This chapter will guide you through the process of setting up and configuring your IRC server from scratch.
1-Planning and Requirements
Before diving into implementation, it’s crucial to define your server’s requirements:
- Functionality: Determine the core features your IRC server will support, such as user authentication, channel management, and message broadcasting.
- Scalability: Consider how your server will handle multiple concurrent connections and optimize for performance.
- Security: Plan for user authentication mechanisms and ensure data transmission is secure, especially if handling sensitive information.
2-Setting Up the Server Infrastructure
Begin by setting up the foundational infrastructure for your IRC server:
- Network Socket: Implement socket programming to handle incoming client connections.
- Protocol Implementation: Develop support for IRC protocol commands such as PASS, NICK, USER, JOIN, PRIVMSG, etc.
- Data Persistence: Consider how user data (nicknames, channels) will be stored and accessed, either through in-memory data structures or a database.
3-Implementing IRC Server Features
Focus on implementing essential IRC server features:
- User Authentication: Implement mechanisms for users to register nicknames (NICK) and authenticate (PASS) to the server.
- Channel Management: Allow users to create (JOIN) and manage channels (PART, MODE, TOPIC).
- Message Handling: Support for broadcasting messages (PRIVMSG), including private messages and channel communication.
3 — Starting the server socket:
To begin, the program expects user input specifying the socket port and password in the following format:
./ircserv <port> <password>
- port: The port number on which your IRC server will be listening to for incoming IRC connections.
- password: The connection password. It will be needed by any IRC client that tries to connect to your server.
For official operation, ensure the port number falls within specific ranges that are not reserved:
+-------------------------+-------------------------------------------------------------+
| Range | Use Cases |
+-------------------------+-------------------------------------------------------------+
| Well-Known Ports | Reserved for standard services (HTTP, FTP, SSH) |
| | Range: 0-1023 |
+-------------------------+-------------------------------------------------------------+
| Registered Ports | Used by registered applications and services |
| | Range: 1024-49151 |
+-------------------------+-------------------------------------------------------------+
| Dynamic or Private Ports| Ephemeral ports for temporary use |
| | Range: 49152-65535 |
+-------------------------+-------------------------------------------------------------+
Next, we will proceed to create the server class and initialize the server socket. Refer to the project structure outlined in the image below for guidance.
1-Creating Server socket:
To establish communication with IRC clients, the server initializes a socket using the following code snippet:
void Server::createSocket(std::string port)
{
// Create a TCP socket
this->sock_fd = socket(AF_INET, SOCK_STREAM, 0);
if (this->sock_fd == -1)
{
close(this->sock_fd);
throw std::runtime_error("Can't create socket");
}
// Define server address structure
sockaddr_in ServerAddress = {};
ServerAddress.sin_family = AF_INET;
ServerAddress.sin_addr.s_addr = INADDR_ANY;
ServerAddress.sin_port = htons(std::atoi(port.c_str()));
// Set socket to non-blocking mode
if (fcntl(sock_fd, F_SETFL, O_NONBLOCK) == -1)
{
close(sock_fd);
throw std::runtime_error("Can't set non-blocking");
}
// Bind socket to the specified port
if (bind(sock_fd, (struct sockaddr*)&ServerAddress, sizeof(ServerAddress)) == -1){
close(this->sock_fd);
throw std::runtime_error("Can't bind socket");
}
// Listen for incoming connections with a queue size of 10
if (listen(sock_fd, 10) == -1)
{
close(sock_fd);
throw std::runtime_error("Error listening");
}
// Server socket created successfully
std::cout << "Server socket created and listening on port " << port << std::endl;
}
2-Starting the server:
initiate the IRC server and handle incoming client connections and messages, the following Server::start() function is utilized:
void Server::start()
{
// Add server socket to pollfds array for monitoring
pollfd serverfd = {sock_fd, POLLIN, 0};
_pollfds.push_back(serverfd);
// Inform that the server is running on the specified port
std::cout << "Server is running on port " << this->port << std::endl;
// Main server loop for handling events
while(true)
{
// Wait indefinitely for events on monitored file descriptors
if (poll(_pollfds.data(), _pollfds.size(), -1) == -1) {
throw std::runtime_error("Poll error");
}
// Iterate through all monitored file descriptors
for(auto it = _pollfds.begin(); it != _pollfds.end(); ++it)
{
// Check if the file descriptor has events to process
if (it->revents == 0)
continue;
// Handle incoming connection request on the server socket
if (it->revents & POLLIN)
{
if(it->fd == sock_fd) {
addClient(sock_fd); // Accept new client connection
break;
}
else {
handleMessage(it->fd); // Handle message from client
}
}
}
}
}
Explanation:
Adding Server Socket to Pollfds:
-
pollfd serverfd = {sock_fd, POLLIN, 0};
initializes apollfd
structure for the server socket (sock_fd
) to monitor for input readiness (POLLIN
). -
_pollfds.push_back(serverfd);
adds the server socket to the_pollfds
vector for polling.
Polling for Events:
-
if (poll(_pollfds.data(), _pollfds.size(), -1) == -1)
polls all file descriptors in_pollfds
indefinitely (-1
timeout) for events. - Throws an exception if
poll
encounters an error.
Processing Events:
- Iterates through each file descriptor in
_pollfds
to check for events (revents
).
Handling Server Socket Event:
-
if (it->fd == sock_fd)
checks if the event is on the server socket: - Calls
addClient(sock_fd);
to accept and add a new client connection.
Handling Client Messages:
-
else if (it->revents & POLLIN)
processes incoming messages from connected clients: - Calls
handleMessage(it->fd);
to handle the message from the client.
3-Adding a client:
To integrate a new client into the IRC server, the Server::addClient(int sock_fd)
function is employed:
void Server::addClient(int sock_fd)
{
// Accept incoming client connection
int client_fd;
sockaddr_in clientAddress = {};
socklen_t clientAddressSize = sizeof(clientAddress);
client_fd = accept(sock_fd, (struct sockaddr*)&clientAddress, &clientAddressSize);
if (client_fd == -1)
throw std::runtime_error("Can't accept client");
// Add client socket to pollfds array for monitoring
pollfd client_poll = {client_fd, POLLIN, 0};
_pollfds.push_back(client_poll);
// Retrieve client hostname
char hostname[NI_MAXHOST];
int result = getnameinfo((struct sockaddr*)&clientAddress, sizeof(clientAddress), hostname, NI_MAXHOST, NULL, 0, NI_NUMERICSERV);
// Handle error if unable to retrieve hostname
if (result != 0) {
close(client_fd);
throw std::runtime_error("Can't get hostname");
}
// Create a new Client object and store in clients map
Client *client = new Client(hostname, ntohs(clientAddress.sin_port), client_fd);
_clients.insert(std::make_pair(client_fd, client));
// TODO: Log client connection
}
Explanation:
- Accepting Client Connection:
accept()
is used to accept a new client connection on the server socket (sock_fd
). If unsuccessful (client_fd == -1
), an exception is thrown. - Adding Client to Pollfds: A
pollfd
structure is initialized for the client socket (client_fd
) withPOLLIN
flag indicating readiness for reading. - Retrieving Client Hostname:
getnameinfo()
retrieves the hostname of the connecting client fromclientAddress
. If unsuccessful (result != 0
), the client socket is closed and an exception is thrown. - Creating Client Object: A new
Client
object is instantiated with the retrieved hostname, client port (converted from network to host byte order), and client socket file descriptor (client_fd
). This client object is stored in_clients
map for future management.
4-Handling message and commands:
To manage incoming messages and execute commands from clients within the IRC server, the following functions are employed:
void Server::handleMessage(int fd)
{
try
{
// Retrieve client associated with the file descriptor
Client* client = _clients.at(fd);
// Read incoming message from client
std::string message = readMessage(fd);
// Pass message to command handler for processing
_commandHandler->handleCommand(message, client);
}
catch(const std::exception& e)
{
std::cout << e.what() << std::endl;
throw std::runtime_error("Error handling message");
}
}
Explanation:
-
Retrieve Client:
_clients.at(fd)
retrieves the client object associated with the provided file descriptor (fd
). -
Read Message:
readMessage(fd)
reads the incoming message from the client socket (fd
). -
Handle Command:
_commandHandler->handleCommand(message, client)
delegates message handling and command execution to_commandHandler
, passing the received message and client object.
std::string Server::readMessage(int fd)
{
char buffer[1024];
std::string message;
// Receive message from client
int bytes = recv(fd, buffer, 1024, 0);
// Handle receive errors
if (bytes == -1)
throw std::runtime_error("Can't read message");
else
message = std::string(buffer, bytes);
return message;
}
Explanation:
-
Receive Message:
recv(fd, buffer, 1024, 0)
reads up to 1024 bytes from the client socket (fd
) intobuffer
. -
Handle Receive Errors: Throws an exception if
recv
returns-1
, indicating an error occurred during message reception.
void CommandHandler::handleCommand(std::string command, Client *client)
{
// Remove leading colon from command if present
if (command[0] == ':')
command = command.substr(1);
// Process each command line by line
std::stringstream ss(command);
std::string cmd;
while(std::getline(ss, cmd))
{
// Trim any trailing carriage return characters
if (cmd.back() == '\r')
cmd.pop_back();
// Retrieve command name
std::string commandName = cmd.substr(0, cmd.find(" "));
try
{
// Retrieve corresponding command object
Command *c = _commands.at(commandName);
// Extract command arguments
std::string argsBuffer = cmd.substr(cmd.find(" ") + 1);
std::istringstream argsStream(argsBuffer);
std::string arg;
std::list<std::string> args;
while(std::getline(argsStream, arg, ' '))
{
arg.erase(std::remove_if(arg.begin(), arg.end(), ::isspace), arg.end());
args.push_back(arg);
}
// Execute command with client and arguments
c->run(client, args);
}
catch (const std::out_of_range &e)
{
// Handle unknown command error
client-reply(Replies::ERR_UNKNOWNCOMMAND(commandName));
}
}
}
- Command Processing: Splits the incoming command into individual lines (
cmd
), trims any trailing carriage return characters, and retrieves the command name. - Command Execution: Attempts to locate and execute the corresponding command handler (
Command *c = _commands.at(commandName);
) using_commands
map. - Argument Parsing: Extracts command arguments from
cmd
, parses them, and executes the command with the client and parsed arguments (c->run(client, args);
). - Error Handling: Catches
std::out_of_range
exception to handle unknown command scenarios (clientreply(Replies::ERR_UNKNOWNCOMMAND(commandName));
).
the diagram of the algorithm followed is below
4 — Creating a bot:
In this section, you have the discretion to select the type of bot to develop, ensuring its relevance within the project framework. For this purpose, I have chosen to develop a weather bot, which functions as a client connecting to the server. Other clients interact with this bot using the command format: WEATHER . When the server receives such a command, it relays it to the bot and awaits the bot's response. Upon receiving the weather information from the bot, the server then directs this response back to the specific client who initiated the query.
Below is an overview of the logical structure underlying this process:
1.Clients send a request in the form WEATHER to the server.
2.The server receives the request and forwards it to the weather bot.
3.The bot processes the request, querying the relevant weather data.
4.The bot sends the weather information back to the server.
5.The server, upon receiving the bot’s response, forwards this information to the client who made the initial request.
1 — inside weather bot brain:
The code blow check args and weather_api_key and attempts to connect and register with the server then in case of success start listening for incoming calls
int main(int argc, char **argv) {
if (argc != 3) {
std::cerr << GREEN << "Usage: " << argv[0] << " <port> <password>" << RESET << std::endl;
return 1;
}
std::string server = "localhost";
int port = std::stoi(argv[1]);
std::string password = argv[2];
int sockfd;
char *apikey = getenv("WEATHER_API_KEY");
std::string apiKey;
if (apikey)
{
apiKey = apikey;
std::cout << GREEN << "WEATHER_API_KEY environment variable set ✅" << RESET << std::endl;
}
else
{
std::cerr << RED << "WEATHER_API_KEY environment variable not set ❌" << RESET << std::endl;
exit(1);
}
if (connectToServer(server, port, sockfd)) {
std::cout << GREEN << "Connected to server ✅" << RESET << std::endl;
// Send registration messages
if (!sendToServer(sockfd, "PASS " + password + "\r\n") ||
!sendToServer(sockfd, "NICK bot\r\n") ||
!sendToServer(sockfd, "USER botname 0 * :bot\r\n")) {
std::cerr << RED << "Failed to register with the server ❌" << RESET << std::endl;
close(sockfd);
return 1;
}
std::cout << GREEN << "Registered with the server ✅" << RESET << std::endl;
std::cout << GREEN << "Listening for messages..." << RESET << std::endl;
listenForMessages(sockfd, apiKey);
close(sockfd);
} else {
std::cerr << RED << "Failed to connect to server ❌" << RESET << std::endl;
}
return 0;
}
now we check the connectToServer function that attempts to connect with the server using a socket
bool connectToServer(const std::string &server, int port, int &sockfd) {
struct sockaddr_in server_addr;
struct hostent *host;
if ((host = gethostbyname(server.c_str())) == NULL) {
std::cerr << RED << "Failed to get host by name ❌" << RESET << std::endl;
return false;
}
if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
std::cerr << RED << "Failed to create socket ❌" << RESET << std::endl;
return false;
}
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(port);
server_addr.sin_addr = *((struct in_addr *)host->h_addr);
memset(&(server_addr.sin_zero), '\0', 8);
if (connect(sockfd, (struct sockaddr *)&server_addr, sizeof(struct sockaddr)) == -1) {
std::cerr << RED << "Failed to connect to server ❌" << RESET << std::endl;
close(sockfd);
return false;
}
return true;
}
Hostname Resolution (gethostbyname
):
- The function first attempts to resolve the hostname (
server
) to an IP address usinggethostbyname
. If unsuccessful (returnsNULL
), it prints an error message and returnsfalse
, indicating failure to resolve the host.
Socket Creation (socket
):
- If hostname resolution succeeds, the function creates a TCP socket (
SOCK_STREAM
) usingsocket(AF_INET, SOCK_STREAM, 0)
. If socket creation fails (returns-1
), it prints an error message and returnsfalse
.
Server Address Configuration (server_addr
):
- The function initializes a
sockaddr_in
structure (server_addr
) to hold the server's address information: -
sin_family
is set toAF_INET
indicating IPv4. -
sin_port
is set to the specified port number (port
) in network byte order (htons
). -
sin_addr
is set to the resolved IP address obtained fromgethostbyname
. -
memset
initializes the remaining bytes ofsin_zero
to0
.
Connecting to the Server (connect
):
- The function attempts to establish a connection to the server using
connect(sockfd, (struct sockaddr *)&server_addr, sizeof(struct sockaddr))
. If connection fails (returns-1
), it prints an error message, closes the socket (sockfd
), and returnsfalse
.
void listenForMessages(int sockfd, std::string& apiKey) {
char buffer[512];
int numBytes;
while (true) {
if ((numBytes = recv(sockfd, buffer, sizeof(buffer) - 1, 0)) > 0) {
buffer[numBytes] = '\0';
std::string message(buffer);
std::istringstream iss(message);
std::string command, user, city;
iss >> command >> user >> city;
if (command == "WEATHER") {
std::string weatherJson = fetchWeatherData(city, apiKey);
std::string formattedMessage = formatWeatherResponse(weatherJson);
std::cout << "Client: " << user << " requested weather for " << city << std::endl;
std::istringstream iss(formattedMessage);
std::string line;
while (std::getline(iss, line)) {
std::string response = "PRIVMSG " + user + " :" + line + "\r\n";
sendToServer(sockfd, response);
}
}
} else if (numBytes == 0) {
std::cout << RED << "Server closed the connection" << RESET << std::endl;
break;
} else {
std::cerr << RED << "Failed to receive message ❌" << RESET << std::endl;
break;
}
}
}
std::string fetchWeatherData(const std::string& city, const std::string& apiKey) {
//saving api keys is a vonerable point, so we will use the environment variable instead
// std::string apiKey = "be44eb7afe554d9890b210909240806";
std::string url = "http://api.weatherapi.com/v1/current.json?key=" + apiKey + "&q=" + city;
std::string command = "curl -s \"" + url + "\" -o weather.json";
// Execute the curl command
system(command.c_str());
// Read the content of the file into a string
std::ifstream file("weather.json");
std::string response((std::istreambuf_iterator<char>(file)), std::istreambuf_iterator<char>());
file.close();
// Optionally remove the temporary file
remove("weather.json");
return response;
}
std::string formatWeatherResponse(const std::string& weatherJson) {
std::string location = extractValue(weatherJson, "name");
std::string country = extractValue(weatherJson, "country");
std::string condition = extractValue(weatherJson, "text");
std::string temp_c = extractValue(weatherJson, "temp_c");
std::string wind_kph = extractValue(weatherJson, "wind_kph");
std::string humidity = extractValue(weatherJson, "humidity");
std::string uv_index = extractValue(weatherJson, "uv");
std::ostringstream oss;
oss << "------------------------\n";
oss << "Weather Information:\n";
oss << "Location: " << location << ", " << country << "\n";
oss << "Condition: " << condition << "\n";
oss << "Temperature: " << temp_c << "°C\n";
oss << "Wind Speed: " << wind_kph << " kph\n";
oss << "Humidity: " << humidity << "%\n";
oss << "UV Index: " << uv_index << "\n";
oss << "------------------------\n";
return oss.str();
}
std::string extractValue(const std::string& json, const std::string& key) {
std::string searchKey = "\"" + key + "\":";
std::size_t startPos = json.find(searchKey);
if (startPos == std::string::npos) {
return "";
}
startPos += searchKey.length();
while (json[startPos] == ' ' || json[startPos] == '\"' || json[startPos] == '{') {
startPos++;
}
std::size_t endPos = json.find_first_of(",}", startPos);
std::string value = json.substr(startPos, endPos - startPos);
value.erase(std::remove(value.begin(), value.end(), '\"'), value.end());
return value;
}
NOTE:
When working with APIs that require authentication via API keys, it’s crucial not to hardcode or embed these keys directly into your source code. Here’s why:
- Security Risk: Hardcoding API keys exposes them to potential theft if the code repository is compromised or accessed by unauthorized parties.
- Best Practices: Instead of embedding API keys in code, use environment variables or configuration files that are not included in your version control system (e.g., .gitignore for Git repositories).
- Environmental Variables: Store sensitive information like API keys in environment variables during development and deployment. This approach keeps them secure and separate from your application codebase.
- Avoiding Accidental Exposure: Inadvertently exposing API keys in public repositories can lead to unauthorized usage, potential costs, or compromised data.
- Security Hygiene: Regularly audit your codebase for any hardcoded sensitive information and implement secure practices to handle such credentials.
in my case we used environment variables but you can use other solutions
Top comments (0)