第2章:IRCプロトコル

はじめに

IRCプロトコルはRFC 1459で定義されたテキストベースのプロトコルです。本章では、メッセージ形式、コマンド、リプライについて詳しく学びます。

---

1. メッセージ形式

1.1 BNF記法

RFC 1459によるメッセージ形式:

message    =  [ ":" prefix SPACE ] command [ params ] crlf
prefix     =  servername / ( nickname [ [ "!" user ] "@" host ] )
command    =  1*letter / 3digit
params     =  *14( SPACE middle ) [ SPACE ":" trailing ]
           /  14( SPACE middle ) [ SPACE [ ":" ] trailing ]

middle     =  nospcrlfcl *( ":" / nospcrlfcl )
trailing   =  *( ":" / " " / nospcrlfcl )

nospcrlfcl =  %x01-09 / %x0B-0C / %x0E-1F / %x21-39 / %x3B-FF
           ; any octet except NUL, CR, LF, " " and ":"

crlf       =  %x0D %x0A  ; "\\r\\n"

1.2 メッセージ構造

IRCメッセージの構成:

[:<prefix>] <command> [<params>] \\r\\n

┌─────────────────────────────────────────────────┐
│ :Nick!user@host PRIVMSG #channel :Hello World\\r\\n │
└─────────────────────────────────────────────────┘
  ↑                ↑       ↑         ↑
  prefix           command param     trailing

prefix: オプション、送信元を示す
command: コマンド名または3桁の数字
params: スペース区切りのパラメータ
trailing: ":"以降の残り全て(スペース含む)

1.3 パラメータの最大数

パラメータ:
- 最大15個(command含む)
- trailing は1つのパラメータとしてカウント

例:
MODE #channel +o Alice Bob
     ↑        ↑   ↑     ↑
     param1   2   3     4

PRIVMSG #channel :This is a long message with spaces
        ↑        ↑
        param1   param2(trailing)

---

2. メッセージパーサー

2.1 パーサー実装

struct Message {
    std::string prefix;
    std::string command;
    std::vector<std::string> params;
};

class MessageParser {
public:
    static Message parse(const std::string& line) {
        Message msg;
        std::string::size_type pos = 0;
        std::string::size_type end = line.length();

        // 末尾の\\r\\n を除去
        while (end > 0 && (line[end-1] == '\\r' || line[end-1] == '\\n')) {
            end--;
        }

        // プレフィックス(オプション)
        if (pos < end && line[pos] == ':') {
            pos++;  // ':' をスキップ
            std::string::size_type spacePos = line.find(' ', pos);
            if (spacePos != std::string::npos && spacePos < end) {
                msg.prefix = line.substr(pos, spacePos - pos);
                pos = spacePos + 1;
            }
        }

        // 先頭のスペースをスキップ
        while (pos < end && line[pos] == ' ') {
            pos++;
        }

        // コマンド
        std::string::size_type spacePos = line.find(' ', pos);
        if (spacePos == std::string::npos || spacePos >= end) {
            msg.command = line.substr(pos, end - pos);
            toUpperCase(msg.command);
            return msg;
        }
        msg.command = line.substr(pos, spacePos - pos);
        toUpperCase(msg.command);
        pos = spacePos + 1;

        // パラメータ
        while (pos < end) {
            // スペースをスキップ
            while (pos < end && line[pos] == ' ') {
                pos++;
            }
            if (pos >= end) break;

            // trailing パラメータ
            if (line[pos] == ':') {
                msg.params.push_back(line.substr(pos + 1, end - pos - 1));
                break;
            }

            // 通常のパラメータ
            spacePos = line.find(' ', pos);
            if (spacePos == std::string::npos || spacePos >= end) {
                msg.params.push_back(line.substr(pos, end - pos));
                break;
            }
            msg.params.push_back(line.substr(pos, spacePos - pos));
            pos = spacePos + 1;
        }

        return msg;
    }

private:
    static void toUpperCase(std::string& str) {
        for (size_t i = 0; i < str.length(); i++) {
            str[i] = std::toupper(str[i]);
        }
    }
};

2.2 テストケース

void testParser() {
    // 基本的なコマンド
    Message m1 = MessageParser::parse("NICK Alice\\r\\n");
    assert(m1.command == "NICK");
    assert(m1.params[0] == "Alice");

    // プレフィックス付き
    Message m2 = MessageParser::parse(":Alice!alice@host PRIVMSG #test :Hello\\r\\n");
    assert(m2.prefix == "Alice!alice@host");
    assert(m2.command == "PRIVMSG");
    assert(m2.params[0] == "#test");
    assert(m2.params[1] == "Hello");

    // trailing にスペース
    Message m3 = MessageParser::parse("PRIVMSG #test :Hello World!\\r\\n");
    assert(m3.params[1] == "Hello World!");

    // 複数パラメータ
    Message m4 = MessageParser::parse("MODE #test +o Alice\\r\\n");
    assert(m4.params[0] == "#test");
    assert(m4.params[1] == "+o");
    assert(m4.params[2] == "Alice");
}

---

3. 数値リプライ

3.1 リプライのカテゴリ

数値リプライ:

000-099: 接続関連(ウェルカムメッセージ等)
200-399: コマンド成功
400-599: エラー

リプライ形式:
:<server> <code> <target> [<params>] :<message>

例:
:irc.server.net 001 Alice :Welcome to the IRC Network
:irc.server.net 433 * Alice :Nickname is already in use

3.2 よく使うリプライ

// 接続成功
#define RPL_WELCOME        "001"  // :Welcome to the IRC Network
#define RPL_YOURHOST       "002"  // :Your host is <server>
#define RPL_CREATED        "003"  // :This server was created <date>
#define RPL_MYINFO         "004"  // <server> <version> <usermodes> <chanmodes>

// チャンネル情報
#define RPL_NOTOPIC        "331"  // :No topic is set
#define RPL_TOPIC          "332"  // :<topic>
#define RPL_INVITING       "341"  // <nick> <channel>
#define RPL_NAMREPLY       "353"  // = <channel> :<nick list>
#define RPL_ENDOFNAMES     "366"  // :End of /NAMES list

// エラー
#define ERR_NOSUCHNICK     "401"  // :No such nick/channel
#define ERR_NOSUCHCHANNEL  "403"  // :No such channel
#define ERR_CANNOTSENDTOCHAN "404" // :Cannot send to channel
#define ERR_TOOMANYCHANNELS "405" // :You have joined too many channels
#define ERR_UNKNOWNCOMMAND "421"  // :Unknown command
#define ERR_NONICKNAMEGIVEN "431" // :No nickname given
#define ERR_ERRONEUSNICKNAME "432" // :Erroneous nickname
#define ERR_NICKNAMEINUSE  "433"  // :Nickname is already in use
#define ERR_USERNOTINCHANNEL "441" // :They aren't on that channel
#define ERR_NOTONCHANNEL   "442"  // :You're not on that channel
#define ERR_USERONCHANNEL  "443"  // :is already on channel
#define ERR_NOTREGISTERED  "451"  // :You have not registered
#define ERR_NEEDMOREPARAMS "461"  // :Not enough parameters
#define ERR_ALREADYREGISTRED "462" // :You may not reregister
#define ERR_PASSWDMISMATCH "464"  // :Password incorrect
#define ERR_CHANNELISFULL  "471"  // :Cannot join channel (+l)
#define ERR_INVITEONLYCHAN "473"  // :Cannot join channel (+i)
#define ERR_BADCHANNELKEY  "475"  // :Cannot join channel (+k)
#define ERR_CHANOPRIVSNEEDED "482" // :You're not channel operator

3.3 リプライ生成

class Reply {
public:
    static std::string welcome(const std::string& nick,
                               const std::string& serverName) {
        return ":" + serverName + " 001 " + nick +
               " :Welcome to the IRC Network, " + nick + "\\r\\n";
    }

    static std::string nickInUse(const std::string& nick,
                                 const std::string& serverName) {
        return ":" + serverName + " 433 * " + nick +
               " :Nickname is already in use\\r\\n";
    }

    static std::string noSuchNick(const std::string& target,
                                  const std::string& nick,
                                  const std::string& serverName) {
        return ":" + serverName + " 401 " + target + " " + nick +
               " :No such nick/channel\\r\\n";
    }

    static std::string needMoreParams(const std::string& nick,
                                      const std::string& command,
                                      const std::string& serverName) {
        return ":" + serverName + " 461 " + nick + " " + command +
               " :Not enough parameters\\r\\n";
    }

    static std::string namReply(const std::string& nick,
                                const std::string& channel,
                                const std::string& names,
                                const std::string& serverName) {
        return ":" + serverName + " 353 " + nick + " = " + channel +
               " :" + names + "\\r\\n";
    }

    static std::string endOfNames(const std::string& nick,
                                  const std::string& channel,
                                  const std::string& serverName) {
        return ":" + serverName + " 366 " + nick + " " + channel +
               " :End of /NAMES list\\r\\n";
    }
};

---

4. 接続シーケンス

4.1 クライアント登録

クライアント → サーバー:
PASS <password>
NICK <nickname>
USER <username> <mode> <unused> :<realname>

サーバー → クライアント:
:server 001 <nick> :Welcome to the IRC Network
:server 002 <nick> :Your host is <server>, running version <ver>
:server 003 <nick> :This server was created <date>
:server 004 <nick> <server> <version> <usermodes> <chanmodes>

4.2 シーケンス図

Client                                Server
   |                                     |
   |  PASS secretpass                    |
   | ----------------------------------> |
   |                                     |
   |  NICK Alice                         |
   | ----------------------------------> |
   |                                     |
   |  USER alice 0 * :Alice Smith        |
   | ----------------------------------> |
   |                                     |
   |      :server 001 Alice :Welcome...  |
   | <---------------------------------- |
   |      :server 002 Alice :Your host...|
   | <---------------------------------- |
   |      :server 003 Alice :Created...  |
   | <---------------------------------- |
   |      :server 004 Alice ...          |
   | <---------------------------------- |
   |                                     |
   |  (登録完了)                          |

4.3 登録状態の管理

class Client {
private:
    enum RegState {
        REG_NONE = 0,
        REG_PASS = 1,   // パスワード受信済み
        REG_NICK = 2,   // ニックネーム設定済み
        REG_USER = 4,   // ユーザー情報設定済み
        REG_COMPLETE = REG_PASS | REG_NICK | REG_USER
    };

    int _fd;
    std::string _nickname;
    std::string _username;
    std::string _realname;
    std::string _hostname;
    int _regState;
    bool _registered;

public:
    void setPassReceived() {
        _regState |= REG_PASS;
        checkRegistration();
    }

    void setNickname(const std::string& nick) {
        _nickname = nick;
        _regState |= REG_NICK;
        checkRegistration();
    }

    void setUser(const std::string& user, const std::string& realname) {
        _username = user;
        _realname = realname;
        _regState |= REG_USER;
        checkRegistration();
    }

    bool isRegistered() const {
        return _registered;
    }

private:
    void checkRegistration() {
        if (!_registered && _regState == REG_COMPLETE) {
            _registered = true;
            // ウェルカムメッセージを送信
        }
    }
};

---

5. チャンネル操作

5.1 JOINシーケンス

クライアント → サーバー:
JOIN #channel

サーバー → クライアント(参加者全員):
:Alice!alice@host JOIN #channel

サーバー → クライアント:
:server 332 Alice #channel :Channel topic
:server 353 Alice = #channel :@Alice Bob Carol
:server 366 Alice #channel :End of /NAMES list

5.2 PRIVMSGシーケンス

チャンネルメッセージ:
クライアント:
PRIVMSG #channel :Hello everyone!

サーバー(送信者以外の全員に):
:Alice!alice@host PRIVMSG #channel :Hello everyone!

プライベートメッセージ:
クライアント:
PRIVMSG Bob :Hi there!

サーバー(Bobに):
:Alice!alice@host PRIVMSG Bob :Hi there!

5.3 PART/QUITシーケンス

PART:
クライアント:
PART #channel :Goodbye!

サーバー(チャンネルメンバー全員に):
:Alice!alice@host PART #channel :Goodbye!

QUIT:
クライアント:
QUIT :Leaving

サーバー(共有チャンネルのメンバー全員に):
:Alice!alice@host QUIT :Leaving

---

6. モード

6.1 チャンネルモード

ft_ircで必要なモード:
i - invite-only(招待制)
t - topic restriction(オペレータのみトピック変更可)
k - channel key(パスワード)
o - operator(オペレータ権限)
l - user limit(人数制限)

MODE #channel +i        → 招待制にする
MODE #channel -i        → 招待制を解除
MODE #channel +k secret → パスワード設定
MODE #channel -k        → パスワード解除
MODE #channel +o Alice  → Aliceをオペレータに
MODE #channel -o Alice  → オペレータ権限剥奪
MODE #channel +l 50     → 最大50人
MODE #channel -l        → 人数制限解除

複合:
MODE #channel +itk secret  → 複数モード同時設定
MODE #channel +ol Alice 50 → オペレータ+人数制限

6.2 MODEパース

struct ModeChange {
    bool add;          // true: +, false: -
    char mode;         // モード文字
    std::string param; // パラメータ(必要な場合)
};

class ModeParser {
public:
    static std::vector<ModeChange> parse(const std::vector<std::string>& params) {
        std::vector<ModeChange> changes;

        if (params.size() < 2) return changes;

        const std::string& modeStr = params[1];
        size_t paramIndex = 2;
        bool adding = true;

        for (size_t i = 0; i < modeStr.length(); i++) {
            char c = modeStr[i];

            if (c == '+') {
                adding = true;
            } else if (c == '-') {
                adding = false;
            } else {
                ModeChange change;
                change.add = adding;
                change.mode = c;

                // パラメータが必要なモード
                if (needsParam(c, adding)) {
                    if (paramIndex < params.size()) {
                        change.param = params[paramIndex++];
                    }
                }

                changes.push_back(change);
            }
        }

        return changes;
    }

private:
    static bool needsParam(char mode, bool adding) {
        // +o, -o: 常にパラメータ必要
        if (mode == 'o') return true;
        // +k: パラメータ必要、-k: 不要
        if (mode == 'k') return adding;
        // +l: パラメータ必要、-l: 不要
        if (mode == 'l') return adding;
        return false;
    }
};

---

7. ホストマスク

7.1 形式

ホストマスク:
nickname!username@hostname

例:
Alice!alice@192.168.1.100
Bob!~bob@example.com
Guest123!guest@10.0.0.1

生成:
std::string Client::getPrefix() const {
    return _nickname + "!" + _username + "@" + _hostname;
}

7.2 メッセージ生成

std::string Client::formatMessage(const std::string& command,
                                  const std::vector<std::string>& params) const {
    std::string msg = ":" + getPrefix() + " " + command;

    for (size_t i = 0; i < params.size(); i++) {
        msg += " ";
        // 最後のパラメータでスペースを含む場合
        if (i == params.size() - 1 && params[i].find(' ') != std::string::npos) {
            msg += ":";
        }
        msg += params[i];
    }

    msg += "\\r\\n";
    return msg;
}

---

まとめ

本章で学んだこと:

  • メッセージ形式: prefix, command, params
  • パーサー実装: BNF記法に基づく解析
  • 数値リプライ: 成功とエラーのコード
  • 接続シーケンス: PASS/NICK/USER
  • チャンネル操作: JOIN/PART/PRIVMSG
  • モード: チャンネルモードとパース
  • ホストマスク: nick!user@host

次章では、サーバーアーキテクチャを学びます。