第6章:ボーナス機能

はじめに

ft_ircのボーナスパートでは、ファイル転送(DCC)とボットの実装が求められます。本章では、これらの追加機能について学びます。

---

1. ファイル転送(DCC)

1.1 DCCの概要

DCC(Direct Client-to-Client):
- クライアント間の直接接続
- サーバーを経由しない
- ファイル転送やプライベートチャットに使用

+--------+                        +--------+
| Client |<===== Direct TCP ====>| Client |
|   A    |                        |   B    |
+--------+                        +--------+
     |                                 |
     |    CTCP DCC request             |
     | ------------------------------> |
     |          via IRC Server         |

1.2 DCCメッセージ形式

CTCP(Client-To-Client Protocol):
- PRIVMSG内に特殊なメッセージを埋め込む
- ^A(ASCII 1)で囲まれる

DCC SEND リクエスト:
PRIVMSG target :^ADCC SEND filename ip port filesize^A

例:
PRIVMSG Bob :^ADCC SEND test.txt 3232235777 5000 1024^A

ip: 32ビット整数形式(192.168.1.1 → 3232235777)
port: リスニングポート
filesize: ファイルサイズ(バイト)

1.3 IPアドレス変換

#include <arpa/inet.h>

// IPアドレスを32ビット整数に変換
uint32_t ipToLong(const std::string& ip) {
    struct in_addr addr;
    inet_pton(AF_INET, ip.c_str(), &addr);
    return ntohl(addr.s_addr);
}

// 32ビット整数をIPアドレスに変換
std::string longToIp(uint32_t ip) {
    struct in_addr addr;
    addr.s_addr = htonl(ip);
    char buffer[INET_ADDRSTRLEN];
    inet_ntop(AF_INET, &addr, buffer, sizeof(buffer));
    return std::string(buffer);
}

1.4 DCC SENDの処理

void Server::handleDCCSend(Client* sender, Client* receiver,
                           const std::string& ctcpMsg) {
    // CTCPメッセージをパース
    // "DCC SEND filename ip port filesize"
    std::vector<std::string> parts = split(ctcpMsg, ' ');

    if (parts.size() < 5) {
        return;
    }

    std::string filename = parts[2];
    std::string ip = parts[3];
    std::string port = parts[4];
    std::string filesize = (parts.size() > 5) ? parts[5] : "0";

    // 受信者にDCCリクエストを転送
    std::string dccMsg = ":" + sender->getPrefix() +
                         " PRIVMSG " + receiver->getNickname() +
                         " :\\001DCC SEND " + filename + " " +
                         ip + " " + port + " " + filesize + "\\001\\r\\n";

    sendToClient(receiver->getFd(), dccMsg);
}

1.5 ファイル転送の流れ

1. 送信者がファイルを選択
2. 送信者がリスニングソケットを開く
3. 送信者がDCC SENDリクエストを送信
4. 受信者がリクエストを受信
5. 受信者が送信者に接続
6. ファイルデータを転送
7. 接続を閉じる

Client A (Sender)                 Client B (Receiver)
      |                                 |
      | 1. Listen on port 5000          |
      | 2. DCC SEND test.txt ...        |
      | -------------------------------->|
      |                                 |
      | <------ 3. TCP Connect ---------|
      |                                 |
      | -------- 4. File data --------->|
      |                                 |
      | <------ 5. ACK ----------------|
      |                                 |
      | 6. Close connection             |

---

2. ボット実装

2.1 ボットの基本構造

class Bot {
private:
    int _fd;
    std::string _nickname;
    std::string _server;
    int _port;
    std::string _password;
    std::string _channel;

    std::string _recvBuffer;

public:
    Bot(const std::string& server, int port,
        const std::string& password, const std::string& nick);
    ~Bot();

    void connect();
    void run();
    void sendMessage(const std::string& msg);

private:
    void handleMessage(const std::string& line);
    void handlePrivmsg(const std::string& prefix,
                       const std::string& target,
                       const std::string& message);
};

Bot::Bot(const std::string& server, int port,
         const std::string& password, const std::string& nick)
    : _fd(-1),
      _nickname(nick),
      _server(server),
      _port(port),
      _password(password),
      _channel("#bottest") {
}

void Bot::connect() {
    // ソケット作成
    _fd = socket(AF_INET, SOCK_STREAM, 0);
    if (_fd < 0) {
        throw std::runtime_error("Failed to create socket");
    }

    // サーバーに接続
    struct sockaddr_in addr;
    addr.sin_family = AF_INET;
    addr.sin_port = htons(_port);
    inet_pton(AF_INET, _server.c_str(), &addr.sin_addr);

    if (::connect(_fd, (struct sockaddr*)&addr, sizeof(addr)) < 0) {
        throw std::runtime_error("Failed to connect");
    }

    // 登録
    sendMessage("PASS " + _password);
    sendMessage("NICK " + _nickname);
    sendMessage("USER bot 0 * :IRC Bot");
}

void Bot::run() {
    char buffer[512];

    while (true) {
        ssize_t n = recv(_fd, buffer, sizeof(buffer) - 1, 0);
        if (n <= 0) break;

        buffer[n] = '\\0';
        _recvBuffer += buffer;

        // 行ごとに処理
        size_t pos;
        while ((pos = _recvBuffer.find("\\r\\n")) != std::string::npos) {
            std::string line = _recvBuffer.substr(0, pos);
            _recvBuffer.erase(0, pos + 2);
            handleMessage(line);
        }
    }
}

void Bot::sendMessage(const std::string& msg) {
    std::string data = msg + "\\r\\n";
    send(_fd, data.c_str(), data.size(), 0);
}

2.2 メッセージ処理

void Bot::handleMessage(const std::string& line) {
    std::cout << "[RECV] " << line << std::endl;

    // PINGに応答
    if (line.substr(0, 4) == "PING") {
        sendMessage("PONG" + line.substr(4));
        return;
    }

    // ウェルカムメッセージ → チャンネル参加
    if (line.find(" 001 ") != std::string::npos) {
        sendMessage("JOIN " + _channel);
        return;
    }

    // PRIVMSGを処理
    if (line.find(" PRIVMSG ") != std::string::npos) {
        // :nick!user@host PRIVMSG target :message
        size_t prefixEnd = line.find(' ');
        std::string prefix = line.substr(1, prefixEnd - 1);

        size_t targetStart = line.find("PRIVMSG ") + 8;
        size_t targetEnd = line.find(' ', targetStart);
        std::string target = line.substr(targetStart, targetEnd - targetStart);

        size_t msgStart = line.find(':', targetEnd);
        std::string message = line.substr(msgStart + 1);

        handlePrivmsg(prefix, target, message);
    }
}

void Bot::handlePrivmsg(const std::string& prefix,
                        const std::string& target,
                        const std::string& message) {
    // ニックネームを抽出
    std::string sender = prefix.substr(0, prefix.find('!'));

    // コマンドを処理
    if (message[0] == '!') {
        std::string cmd = message.substr(1);

        if (cmd == "hello") {
            sendMessage("PRIVMSG " + target + " :Hello, " + sender + "!");
        }
        else if (cmd == "time") {
            time_t now = time(NULL);
            std::string timeStr = ctime(&now);
            timeStr.pop_back();  // 改行を削除
            sendMessage("PRIVMSG " + target + " :Current time: " + timeStr);
        }
        else if (cmd == "help") {
            sendMessage("PRIVMSG " + target + " :Commands: !hello, !time, !help");
        }
    }
}

2.3 ボットの起動

int main(int argc, char* argv[]) {
    if (argc != 4) {
        std::cerr << "Usage: " << argv[0]
                  << " <server> <port> <password>" << std::endl;
        return 1;
    }

    try {
        Bot bot(argv[1], std::atoi(argv[2]), argv[3], "MyBot");
        bot.connect();
        bot.run();
    } catch (const std::exception& e) {
        std::cerr << "Error: " << e.what() << std::endl;
        return 1;
    }

    return 0;
}

---

3. 追加コマンド

3.1 NOTICE

// NOTICEはPRIVMSGと同様だが、自動応答を生成しない
void Server::cmdNotice(Client* client, const Message& msg) {
    if (msg.params.size() < 2) {
        return;  // NOTICEはエラーを返さない
    }

    std::string target = msg.params[0];
    std::string text = msg.params[1];

    if (target[0] == '#' || target[0] == '&') {
        Channel* channel = getChannel(target);
        if (channel && channel->hasMember(client)) {
            std::string noticeMsg = ":" + client->getPrefix() +
                                    " NOTICE " + target + " :" + text + "\\r\\n";
            sendToChannel(channel, noticeMsg, client);
        }
    } else {
        Client* targetClient = getClientByNick(target);
        if (targetClient) {
            std::string noticeMsg = ":" + client->getPrefix() +
                                    " NOTICE " + target + " :" + text + "\\r\\n";
            sendToClient(targetClient->getFd(), noticeMsg);
        }
    }
}

3.2 WHO

void Server::cmdWho(Client* client, const Message& msg) {
    if (msg.params.empty()) {
        return;
    }

    std::string target = msg.params[0];

    if (target[0] == '#' || target[0] == '&') {
        Channel* channel = getChannel(target);
        if (channel) {
            const std::set<Client*>& members = channel->getMembers();
            for (std::set<Client*>::iterator it = members.begin();
                 it != members.end(); ++it) {
                Client* member = *it;
                std::string flags = "H";  // Here
                if (channel->isOperator(member)) {
                    flags += "@";
                }

                // 352 RPL_WHOREPLY
                sendToClient(client->getFd(),
                    ":" + _serverName + " 352 " + client->getNickname() + " " +
                    target + " " + member->getUsername() + " " +
                    member->getHostname() + " " + _serverName + " " +
                    member->getNickname() + " " + flags + " :0 " +
                    member->getRealname() + "\\r\\n");
            }
        }
    }

    // 315 RPL_ENDOFWHO
    sendToClient(client->getFd(),
        ":" + _serverName + " 315 " + client->getNickname() + " " +
        target + " :End of WHO list\\r\\n");
}

---

4. エラーハンドリング

4.1 堅牢性の向上

// バッファオーバーフロー対策
void Client::appendToRecvBuffer(const std::string& data) {
    _recvBuffer += data;

    // バッファサイズ制限
    if (_recvBuffer.size() > MAX_BUFFER_SIZE) {
        _recvBuffer.clear();
        // エラーログ
    }
}

// 不正なメッセージ対策
bool Server::validateMessage(const Message& msg) {
    // コマンド長の制限
    if (msg.command.length() > 20) {
        return false;
    }

    // パラメータ数の制限
    if (msg.params.size() > 15) {
        return false;
    }

    // 各パラメータの長さ制限
    for (size_t i = 0; i < msg.params.size(); i++) {
        if (msg.params[i].length() > 510) {
            return false;
        }
    }

    return true;
}

4.2 タイムアウト処理

void Server::checkTimeouts() {
    time_t now = time(NULL);

    for (std::map<int, Client*>::iterator it = _clients.begin();
         it != _clients.end(); ) {
        Client* client = it->second;
        int fd = it->first;
        ++it;  // イテレータを先に進める

        // 未登録クライアントのタイムアウト(30秒)
        if (!client->isRegistered()) {
            if (now - client->getConnectTime() > 30) {
                sendToClient(fd, "ERROR :Registration timeout\\r\\n");
                handleClientDisconnect(fd);
                continue;
            }
        }

        // PINGタイムアウト(180秒)
        if (now - client->getLastActivity() > 180) {
            sendToClient(fd, "ERROR :Ping timeout\\r\\n");
            handleClientDisconnect(fd);
        }
        // PING送信(120秒)
        else if (now - client->getLastActivity() > 120) {
            if (!client->isPingSent()) {
                sendToClient(fd, "PING :" + _serverName + "\\r\\n");
                client->setPingSent(true);
            }
        }
    }
}

---

5. テストスクリプト

5.1 自動テスト

#!/usr/bin/env python3
# test_irc_full.py

import socket
import time

class IRCTester:
    def __init__(self, host, port, password):
        self.host = host
        self.port = port
        self.password = password
        self.sock = None

    def connect(self):
        self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        self.sock.connect((self.host, self.port))
        self.sock.settimeout(5)

    def send(self, msg):
        self.sock.send((msg + "\\r\\n").encode())
        time.sleep(0.1)

    def recv(self):
        try:
            return self.sock.recv(4096).decode()
        except socket.timeout:
            return ""

    def close(self):
        self.sock.close()

    def test_registration(self, nick):
        self.send(f"PASS {self.password}")
        self.send(f"NICK {nick}")
        self.send(f"USER {nick} 0 * :{nick}")
        response = self.recv()
        assert "001" in response, f"Registration failed: {response}"
        print(f"✓ Registration for {nick}")

    def test_join(self, channel):
        self.send(f"JOIN {channel}")
        response = self.recv()
        assert "JOIN" in response, f"JOIN failed: {response}"
        print(f"✓ JOIN {channel}")

    def test_privmsg(self, target, message):
        self.send(f"PRIVMSG {target} :{message}")
        print(f"✓ PRIVMSG to {target}")

def main():
    # クライアント1
    client1 = IRCTester("localhost", 6667, "test")
    client1.connect()
    client1.test_registration("Alice")
    client1.test_join("#test")

    # クライアント2
    client2 = IRCTester("localhost", 6667, "test")
    client2.connect()
    client2.test_registration("Bob")
    client2.test_join("#test")

    # メッセージテスト
    client1.test_privmsg("#test", "Hello from Alice!")
    response = client2.recv()
    assert "Hello from Alice!" in response

    # オペレータテスト
    client1.send("MODE #test +o Bob")
    time.sleep(0.1)

    # クリーンアップ
    client1.send("QUIT")
    client2.send("QUIT")
    client1.close()
    client2.close()

    print("\\n✓ All tests passed!")

if __name__ == "__main__":
    main()

---

まとめ

本章で学んだこと:

  • DCC: ファイル転送プロトコル
  • ボット: 自動応答システム
  • 追加コマンド: NOTICE, WHO
  • エラーハンドリング: バッファ制限、タイムアウト
  • テスト: 自動テストスクリプト

これでft_ircの基本実装は完成です。実際のプロジェクトでは、エッジケースの処理やセキュリティ対策をさらに強化してください。