第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の基本実装は完成です。実際のプロジェクトでは、エッジケースの処理やセキュリティ対策をさらに強化してください。