人間とウェブの未来

「ウェブの歴史は人類の歴史の繰り返し」という観点から色々勉強しています。

IMAPサーバのdovecotをmrubyでハックする

hb.matsumoto-r.jp

上記のエントリで言及していたメールの受信サーバdovecotをmrubyで制御するpluginが概ね完成しましたので紹介します。というのも、一月前ぐらいにはできていたのですがバタバタしておりブログにできていませんでした。

github.com

dovecot-mruby-pluginはメール受信のIMAPサーバとして動くdovecotをmrubyで色々制御することができます。今日はその制御の例を幾つか紹介します。

mrubyでIMAPコマンドを作る

dovecot-mruby-pluginでは、dovecotのIMAPプロトコルで使うコマンドをmrubyで作ることができます。例えば以下のようにRubyのコードを書き、dovecotに読み込ませます。

# Register new commands
%w(

matsumotory
test

).each do |cmd|
  Dovecot::IMAP.command_register(cmd) do |args|
    Dovecot::IMAP.send_line "Hi, #{Dovecot::IMAP.username}"
    if cmd == Dovecot::IMAP.username
      Dovecot::IMAP.send_line "You are me."
    else
      Dovecot::IMAP.send_line "I am #{cmd}.  Not you with #{args}"
    end
  end
end

その上で、以下のようにdovecot.confにdovecot上で有効な環境変数を設定します。

import_environment = DOVECOT_MRUBY_INIT_PATH

そして、dovecotに上記のRubyコードを環境変数経由で渡して起動させます。

DOVECOT_MRUBY_INIT_PATH=/path/to/command_register.rb ./dovecot/target/sbin/dovecot \
    -c ./dovecot/configuration/dovecot.conf

そして実際にtelnetでIMAPサーバにアクセスしてみましょう。

$ telnet 127.0.0.1 6070
Trying 127.0.0.1...
Connected to 127.0.0.1.
Escape character is '^]'.
* OK [CAPABILITY ...] Dovecot ready.

1 login test testPassword
1 OK [CAPABILITY ...] Logged in

1 matsumotory hoge 1
* matsumotory Hi, test
* matsumotory I am matsumotory.  Not you with ["hoge", "1"]
1 OK matsumotory completed (0.001 + 0.000 secs).

1 test 
* test Hi, test
* test You are me.
1 OK test completed (0.001 + 0.000 secs).

1 matsumotory 
* matsumotory Hi, test
* matsumotory I am matsumotory.  Not you with []
1 OK matsumotory completed (0.001 + 0.000 secs).

なんということでしょう。testコマンドやmatsumotoryコマンドが有効となり、その引数をRubyコード上で受けて何かしら処理するコマンドが動いていることがわかります。これでオレオレコマンドやコマンドの拡張なども、dovecot側のコードを弄らずとも簡単に実装できるようになりますね。なんということでしょう!

既存のIMAPコマンドのaliasコマンドを作る

dovecot-mruby-pluginは既存のIMAPコマンドのaliasを作ることもできます。例えば以下のようにコードを書きます。

# Register CAP command which run CAPABILITY command
Dovecot::IMAP.alias_command_register("CAP") do
  Dovecot::IMAP.send_line "execute CAPABILITY commands"
  Dovecot::IMAP.capability
end

Dovecot::IMAP.alias_command_register("LIST_ALIAS") do
  Dovecot::IMAP.send_line "alias LIST commands"
  Dovecot::IMAP.list
end

そして、上述した読み込み方でdovecotにRubyコードを読み込ませます。その後、telnetでアクセスしてみましょう。

1 cap
* cap execute CAPABILITY commands
* CAPABILITY IMAP4rev1 LITERAL+ SASL-IR ...
1 OK Capability completed (0.001 + 0.000 secs).

1 list "" *
* LIST (\HasNoChildren) "/" INBOX
1 OK List completed (0.001 + 0.000 secs).

1 list_alias "" *
* list_alias alias LIST commands
* LIST (\HasNoChildren) "/" INBOX
1 OK List completed (0.001 + 0.000 secs).

おお、なんということでしょう!capabilityコマンドのaliasであるcapコマンドがちゃんと動いており、listコマンドであるaliasのlist_aliasが有効になっているではありませんか。なんということでしょう。

IMAPコマンドの前後でRubyコードをフックする

さらに、各IMAPコマンドの前後にRubyコードをフックすることもできます。

例えば、listコマンドが非常にCPU負荷を書けてしまう場合に、その処理を拒否したり中断したりするのではなく、CPU使用率の割り当てを30%に抑えたい、ということを簡単に実現することができます。

以下のように、コマンドの前に処理をするpre_list.rbと、コマンド実行後に処理をするpost_list.rbを用意します。

  • pre_list.rb
rate = Cgroup::CPU.new "test"

# limit cpu 30% usage
rate.cfs_quota_us = 30000

rate.create

if Dovecot::IMAP.username == "test" && Dovecot::IMAP.command_name == "LIST"
  rate.attach
end
  • post_list.rb
rate = Cgroup::CPU.new "test"

if Dovecot::IMAP.username == "test" && Dovecot::IMAP.command_name == "LIST"
  rate.detach
end

そして、仮想的にlistコマンドがCPUを使い切る状況を再現するために、上記で解説したように、新たにRubyでlistコマンドを再定義し、ループをする処理にlistコマンドを上書きします。

Dovecot::IMAP.command_register("LIST") do |args|
  100000000.times {}
  Dovecot::IMAP.send_line "done!"
end

そして、以下のようにdovecot.confにpre_list.rbpost_list.rbを登録しておきます。

  • 95-mruby.conf
protocol imap {
  mail_plugins = $mail_plugins imap_mruby
}

plugin {
  mruby_pre_list_path = /path/to/pre_list.rb
  mruby_post_list_path = /path/to/post_list.rb
}

そして、実際にtelnetでアクセスし、listコマンドを実行してみましょう。すると、

29906 ubuntu    20   0   87640   9508   7532 R  29.9  0.1   0:04.46 imap 

なんということでしょう!

まとめ

以上のように、dovecot-mruby-pluginを使えば、dovecotのIMAPサーバを非常に容易に拡張することができます。また、dovecotは1セッションに対して1プロセスを生成破棄するCGI的プロセスモデルを採用しているため、mrubyのインタプリタもプロセス単位で読み込んでおり、各セッション間でmrubyの情報を共有することがなく安全です。

これまでのミドルウェア実装の経験上、セッション単位でプロセスを生成破棄するモデルは、セッション毎にmrubyインタプリタを作ったとしても、性能的にはプロセスの生成破棄がオーバーヘッドになることがわかっている(プロセス生成破棄が1秒間に500セッション処理できるマシンで、mrubyのインタプリタは1秒間に2000回インタプリタを作れる程度のコスト)ため、通常、性能面でも組み込みアーキテクチャが問題にはなることないでしょう。そういうこともあり、実装については非常に簡単でした。

とはいえ、dovecotのコードを読んでdovecotのアーキテクチャや実装を理解した上でpluginの仕様に基いて実装をするということについては、例のごとくドキュメントもなくC言語で書くしか無いので、それなりに敷居が高く手間もかかると思われますので、それをmrubyでwrapしてRubyで比較的誰でも拡張できるような状況になると良いなと思い作りました。いつものモチベーションですね。

ということで、是非まずは色々遊んで頂いて、その後各種用途にご活用頂けると幸いです。また、昨日については最低限ですので、追加機能やメソッドについてはissueで連絡頂けると積極的に実装したいと考えておりますのでお気軽にお問い合わせください。

github.com