IRCの概要から簡易クライアント作成(Python3)まで

みなさんは、**IRC(Internet Relay Chat)**というワードを聞いたことはあるでしょうか。
今**IRC**を使っているという人は少ないかもしれません(私の周りにも一人もいません)が、今回はこの**IRC**について書いていきます！
(すでに**IRCクライアント**のコードが載っているQiitaの記事がありましたが、気になる部分がいくつかあったのと、**Python2**のコードだったので、改めて記事にしようと思いました。)
#IRC(Internet Relay Chat)とは
**IRC**とは、**サーバ**を介して**クライアント**と**クライアント**が会話をする枠組みで、**インスタントメッセンジャー**の**プロトコル**の1つに分類されます。
また、**ユーザ**が**クライアントソフト**を実行し、**サーバ**は情報の伝達を行うだけなので、**サーバ**の負担が軽くチャットが高速に行えます。
チャットに特化した**プロトコル**なんですね！
##なぜIRCの利用は衰退したの？
チャットアプリなんて、今はたくさんありますよね。(**Facebook,Twitter,Discord,Slack,...etc.**)
これら**SNS**を利用するほうが、**IRC**を利用するよりも便利で多機能で使いやすいわけですね。。。
また、**SSL/TLS**を利用せずに**IRC**を利用すると、**暗号化**していないため、第三者に会話の内容を盗み見られる可能性があります。(もちろん**SSL/TLS**を利用すれば安全です！)
**LINE**などのサービスなら、**暗号化**などは自動でやってくれるから、そんなの普段は意識しないですよね。。
##IRCを利用するメリットは？
それだけ聞くと**IRC**を利用するメリットはあまりないように感じるかもしれませんが、そんなことはありません！
まず、**IRC**は、データ通信に関する**プロトコル**が簡素で、かつオープンであるため、**ユーザ**・**クライアント**に用いるソフトウェアの開発が簡単なんです！今回の記事でも、簡易**クライアント**を**たった100行程度**で作っちゃいます！
それだけではなく、**IRCサーバ**を立てるのも、**ngircd**というものを利用すれば超簡単なんです！(今回はこれには触れません。)
##今IRCは何に使われてるの？
では今、**IRC**は何に使われているんでしょうか。
それは、、、**ボットネット**の通信手段です。(誤解を招きそうなので言っておきますが、もちろん正規の普通の使い方でもたくさん利用されています。)
**ボットネット**とは、**ボットウイルス**に感染した端末で構成される**ネットワーク**です。**ボットウイルス**とは、**マルウェア**の一種で、感染すると端末が悪意を持った第三者に**遠隔操作**され、**不正アクセス**や**DDoS攻撃**などの**犯罪行為**に使われてしまいます。
その感染端末と攻撃者の通信手段に、**IRC**はよく使われています。。。
自分のパソコンを**ポートスキャン**したり、通信の**ログ**を見て、「**IRC**」という文字を見つけた場合は、**ボットウイルス**に感染している可能性があるかもしれません。。。
##有名なIRCクライアントソフト
**IRCクライアントソフト**はたくさんありますが、ここでは中でも有名(人気)なもの(すべて**無料**)を紹介します。
###LimeChat
おそらく1番有名(人気)な**IRCクライアントソフト**です。
**Windows**の場合は**LimeChat 2.x**、**Mac**の場合は**LimeChat for OSX**を利用します。
現行**IRCクライアント**の中では**最も安定**しています。ヘルプが充実しているので**初心者にもおすすめ**です。
###HexChat
**XChat**(後述)ベースのソフトウェアです。
**Windows用**、**Mac用**、**Unix系システム用**があり、ほぼすべてのOSで利用できます。
海外のさまざまなネットワークが標準設定で入っています。
###XChat
**Linux**のほとんどの**ディストリビューション**に標準で入っている**IRCクライアント**です。
**Windows用**、**Mac用**、**Unix系システム用**があり、ほぼすべてのOSで利用できます。
海外のさまざまなネットワークが標準設定で入っています。
#Python3で簡易クライアントを作る
では早速**IRCクライアント**を作ってみましょう！
**IRCクライアント**を作るには、まず**IRC**の**コマンド**というものについて知る必要があります。
##IRCのコマンドについて
**IRC**において、**サーバ**と**クライアント**は互いに**メッセージ**を送信し合います。
また、**メッセージ**が正しい**コマンド**を含んでいた場合、**クライアント**は仕様通りのリプライを期待します。
つまり、ある**コマンド**を含む**メッセージ**に対する**サーバ**からの応答**メッセージ**の形式は決まっているわけですね。
この**IRCコマンド**はたくさんありますが、今回は、後で作成する簡易**クライアント**が実装している、最も基本的な**コマンド**8つについてのみ説明します。(**コマンド**は**正規表現**で示します。)
###PASSコマンド
```
PASS <password>
```
**IRCサーバ**に**パスワード**が設定されている場合は、**ユーザ**は接続を開始しようとする前(**NICK**/**USER**の組み合わせを送る前)に**PASSコマンド**を送る必要があります。
###NICKコマンド
```
NICK :<nickname>
```
**ユーザ**に**ニックネーム**を設定したり、今の**ニックネーム**を変更したりするのに使います。
###USERコマンド
```
USER <username> <hostname> <servername> <realname>
```
新しい**ユーザ**の**ユーザ名**や**ホスト名**、**本名**を指定するために、接続のはじめに使われます。
###QUITコマンド
```
QUIT (:<Quit Message>)??
```
**クライアントセッション**を終了します。**サーバ**は**クライアント**に**ERRORメッセージ**を送ることでこれを承認します。
###JOINコマンド
```
JOIN :<channel> ("," <channel>)* (<key> ("," <key>)*)??
```
**ユーザ**によって、特定の**チャンネル**に接続する**リクエスト**を行うために使用されます。
###PRIVMSGコマンド
```
PRIVMASG <msgtarget> :<text to be sent>
```
**ユーザ**間の**プライベートメッセージ**を送るのに使います。また、**メッセージ**を**チャンネル**に送るのにも使われます。*msgtarget*は通常、**メッセージ**の受け取り手の**ニックネーム**か、**チャンネル名**です。
誰かが**メッセージ**を送った場合は、**サーバ**から次の形式で**メッセージ**が送られてきます。

```:送られてくるとき
:<A's nickname>!(~<A's username>)??@<A's gateway-name> PRIVMSG <msgtarget> :<text to be sent>
```
###PINGコマンド
```
PING :<server1> (<server2>)??
```
**ネットワーク**上のアクティブな**クライアント**や**サーバ**が、実際につながって動いているかどうかを確認するのに使われます。
**サーバ**は、**ネットワーク**からほかのアクションが届かない場合、一定間隔で**PINGメッセージ**を送出します。
指定時間内に**PINGメッセージ**に返答がない場合、その接続は閉じられます。
**PINGメッセージ**を受け取ったら、*server1*(**PINGメッセージ**を送ったサーバ)への**リプライ**として、可能な限り早く正しい**PONGメッセージ**を送らなくてはなりません。*server2*パラメータが指定されていれば、それがpingのターゲットとなり、**メッセージ**はそこに転送されます。
###PONGコマンド
```
PONG <server1> (<server2>)??
```
**PINGメッセージ**への**リプライ**です。*server2*パラメータが指定されれば、**メッセージ**はそこに転送されなくてはなりません。
##IRCクライアント(Python3コード)

```python
import sys, socket, os, signal

HOST = "X.X.X.X"
PORT = 6667 #IRCサーバでは一般的に6667番ポートが使われることが多い

BUF_SIZE = 1024

class IRC(object):
    def __init__(self):
        self.server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) #socketオブジェクトの生成(TCP)

    def connect(self, host, port):
        self.server.connect((host, port)) #接続

    def login(self, password, nickname, username, realname, hostname = "hostname", servername = "*"):
        if password is not None: #中にはパスワードがいらないサーバもある
            pass_message = "PASS " + password + "\n" #PASSメッセージ
            self.server.send(pass_message.encode('utf-8')) #送信
        nick_message = "NICK " + nickname + "\n" #NICKメッセージ
        user_message = "USER %s %s %s :%s\n" % (username, hostname, servername, realname) #USERメッセージ
        self.server.send(nick_message.encode('utf-8')) #送信
        self.server.send(user_message.encode('utf-8')) #送信

    def join(self, channel):
        join_message = "JOIN " + channel + "\n" #JOINメッセージ
        self.server.send(join_message.encode('utf-8')) #送信

    def pong(self, server1, server2 = None):
        pong_message = "PONG %s %s" % (server1, server2) #PONGメッセージ
        pong_message += "\n"
        self.server.send(pong_message.encode('utf-8')) #送信

    def privmsg(self, channel, text):
        privmsg_message = "PRIVMSG %s :%s\n" % (channel, text) #PRIVMSGメッセージ
        self.server.send(privmsg_message.encode('utf-8')) #送信

    def quit(self):
        self.server.send(b"QUIT :bye!") #QUITメッセージ送信

    def handle_privmsg(self, prefix, text):
        print("\r" + prefix + ">" + text + "\n>", end="") #受信したPRIVMSGメッセージを処理、表示

    def wait_message(self):
        while(True):
            msg_buf = self.server.recv(BUF_SIZE) #受信
            msg_buf = msg_buf.decode('utf-8').strip()
            ## ここからメッセージ処理 ##
            prefix = None
            if msg_buf[0] == ":":
                p = msg_buf.find(" ")
                prefix = msg_buf[1:p]
                msg_buf = msg_buf[(p + 1):]

            p = msg_buf.find(":")
            if p != -1: #":"から始まるパラメータがまだあった場合
                last_param = msg_buf[(p + 1):]
                msg_buf = msg_buf[:p]
                msg_buf = msg_buf.strip()

            messages = msg_buf.split()
            ## ここまで ##

            command = messages[0] #コマンド名
            params = messages[1:] #今回は無視

            if command == "PING":
                self.pong(last_param) #PINGが来たらすぐPONGを返す
            elif command == "PRIVMSG":
                text = last_param #PRIVMSGコマンドで送られてきたメッセージ
                self.handle_privmsg(prefix, text)


    def client_interface(self, channel, prompt = ">"):
        while(True):
            line = input(prompt)

            if line == "quit":
                self.quit()
                break

            self.privmsg(channel, line)


def main():
    password = "password"
    nickname = "nickhoge"
    username = "usr"
    realname = "realname"
    channel = "#test_channel"

    irc = IRC()
    irc.connect(HOST, PORT)
    irc.login(password, nickname, username, realname)
    irc.join(channel)

    pid = os.fork() #子プロセス生成

    if(pid == 0): #os.fork()は、子プロセスでは0を返す
        irc.wait_message()
    else:
        irc.client_interface(channel)
        os.kill(pid, signal.SIGTERM) #子プロセスをkill(これをしないと子プロセスがゾンビプロセスになる)

if __name__ == "__main__":
    main()
```
##macで実行してみた
実際に**LimeChat**ユーザと会話してみました。
![1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/264330/5c6c18b6-c936-a6f8-9beb-96c5fecd92f0.png)
**LimeChat**側からみるとこんな感じです。
![2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/264330/b08f471c-d227-ab55-13bd-b50c3d72b9ba.png)
いい感じですね！
#参考
https://qiita.com/mmttt202/items/d182c6f27f466c923fea
http://jbpe.tripod.com/rfcj/rfc2812.j.sjis.txt
https://www.friend-chat.jp/irc.html
https://wikiwiki.jp/2chIRC/IRC%E3%82%AF%E3%83%A9%E3%82%A4%E3%82%A2%E3%83%B3%E3%83%88%E7%B4%B9%E4%BB%8B
https://ja.wikipedia.org/wiki/Internet_Relay_Chat
https://murashun.jp/blog/20190215-01.html