DEV Community

Cover image for Internet Relay Chat
B.R.O.L.Y
B.R.O.L.Y

Posted on

Internet Relay Chat

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.

server-to-server-image

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.

project requests

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                                          |
+-------------------------+-------------------------------------------------------------+
Enter fullscreen mode Exit fullscreen mode

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.

server structures

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;
}
Enter fullscreen mode Exit fullscreen mode

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
                }
            }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Explanation:

Adding Server Socket to Pollfds:

  • pollfd serverfd = {sock_fd, POLLIN, 0}; initializes a pollfd 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
}
Enter fullscreen mode Exit fullscreen mode

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) with POLLIN flag indicating readiness for reading.
  • Retrieving Client Hostname: getnameinfo() retrieves the hostname of the connecting client from clientAddress. 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");
    }
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

Explanation:

  • Receive Message: recv(fd, buffer, 1024, 0) reads up to 1024 bytes from the client socket (fd) into buffer.
  • 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));
        }
    }
}
Enter fullscreen mode Exit fullscreen mode
  • 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

algorithm flow

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.

bot handling flow in server

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;
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode
Hostname Resolution (gethostbyname):
  • The function first attempts to resolve the hostname (server) to an IP address using gethostbyname. If unsuccessful (returns NULL), it prints an error message and returns false, indicating failure to resolve the host.
Socket Creation (socket):
  • If hostname resolution succeeds, the function creates a TCP socket (SOCK_STREAM) using socket(AF_INET, SOCK_STREAM, 0). If socket creation fails (returns -1), it prints an error message and returns false.
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 to AF_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 from gethostbyname.
  • memset initializes the remaining bytes of sin_zero to 0.
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 returns false.
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;
        }
    }
}
Enter fullscreen mode Exit fullscreen mode
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;
}
Enter fullscreen mode Exit fullscreen mode
  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();
}
Enter fullscreen mode Exit fullscreen mode
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;
}
Enter fullscreen mode Exit fullscreen mode

bot algorithm flow

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:

  1. Security Risk: Hardcoding API keys exposes them to potential theft if the code repository is compromised or accessed by unauthorized parties.
  2. 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).
  3. 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.
  4. Avoiding Accidental Exposure: Inadvertently exposing API keys in public repositories can lead to unauthorized usage, potential costs, or compromised data.
  5. 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)