ログインからログアウトまでのケーススタディ

ここでは、実際に一つのクライアントがログイン処理を開始してから最終的にログアウトするまでに、クライアントとサーバーの間で具体的にどのようなやりとりがなされているかを紹介していきます。

ストリームの状態変化

前章で、クライアントはサーバーと、ストリームを通して様々なデータをやり取りすると説明しました。 ただし、ストリームはいくつかの状態でもって管理されており、手続きを進めて有効状態になってからでないとメッセージやプレゼンスの送受信はできません。いわゆるログイン処理のような手続きを進め、それが完了すると、はじめて有効状態になり、サービスの様々な機能を利用できるようになるわけです。

IMサービスにおける一般的なログイン処理の流れは次のような手順になります。

  1. ストリームの開始
  2. TLSの開始(サーバーが要求するならば)
  3. SASL認証
  4. リソースのバインディング
  5. セッションの確立
  6. ロスタの取得(クライアントが必要とするならば)
  7. イニシャルプレゼンスの送信

ここでは、一つ一つのステップについて詳しく説明していくと同時に、実際にクライアントとサーバー間で送信されるXMLの例も紹介します。ただしここでは、全体の流れを把握することを優先し、細かい異常系の制御については省略し、正常系のみのやりとりを紹介します。細かな仕様についてはRFC6120, RFC6121を参照されるとよいでしょう。

ストリームの開始

TCP接続完了後、クライアントは次のようにstreamを開始します。

client to server
<?xml version="1.0"?>
<stream:stream to="xmpp.example.org" version="1.0" xmlns="jabber:client" xmlns:stream="http://etherx.jabber.org/streams">

サーバーはストリームに対しidを発行し、次のように、発行したIDや、サーバーがサポートするfeaturesを返します。

server to client
<?xml version="1.0"?>
<stream:stream from="xmpp.example.org" id="1f6d1a" version="1.0" xmlns="jabber:client" xmlns:stream="http://etherx.jabber.org/streams">
  <stream:features>
    <starttls xmlns='urn:ietf:params:xml:ns:xmpp-tls'>
      <required/>
    </starttls>
  </stream:features>

上の例は、TLSを要求する場合です。TLSを要求する場合はこのような形でstreamを返し、次のTLSの開始に進みます。TLSを要求しない場合は、次のようにfeaturesを返し、SASL認証のステップに飛びます。

server to client
<?xml version="1.0"?>
<stream:stream from="xmpp.example.org" id="7de8a2" version="1.0" xmlns="jabber:client" xmlns:stream="http://etherx.jabber.org/streams">
  <stream:features>
    <mechanisms xmlns='urn:ietf:params:xml:ns:xmpp-sasl'>
      <mechanism>DIGEST-MD5</mechanism>
      <mechanism>CRAM-MD5</mechanism>
      <mechanism>X-OAUTH2</mechanism>
      <mechanism>PLAIN</mechanism>
    </mechanisms>
  </stream:features>

TLSをサポートする場合は、必ずSASL認証の前にTLSの開始を行わなくてはなりません。パスワードなどの機密情報が、暗号化された接続の上でやりとりされるようにするためです。

Oceanでは、設定ファイルのtlsセクションを記述すれば、自動的にTLSを要求し、手続きを進めます。このあたりの詳細に関しては、コンフィギュレーションガイドを参照してください。

TLSの開始

サーバーがセキュアな接続を要求する場合は、まずはTLSのネゴシエーションから始まります。

例えば、HTTPでTLS(SSL)を利用する場合は、はじめにネゴシエーションを完了してからリクエストを送信しますが、XMPPの場合は、上述のようにプレーンな状態でストリーム開始のやり取りを行なってから、TLSネゴシエーションを開始します。

サーバーによっては、HTTPのように最初にTLS化してからstreamのやり取りを開始するものもありますが、正確には、仕様に沿った挙動ではありません。

まず、クライアントがTLS開始の意思をサーバーに伝えます。

client to server
<starttls xmlns='urn:ietf:params:xml:ns:xmpp-tls'/>

サーバーはproceedを返すと、TLSネゴシエーションを待ち受けます。

server to client
<proceed xmlns='urn:ietf:params:xml:ns:xmpp-tls'/>

ここでTLSの交渉を行い、成功すると、クライアントはstreamをリフレッシュします。

XMLをルートエレメントから送信しなおすだけで、TCP接続自体を一度切断して接続し直すわけではありません。

client to server
<?xml version="1.0"?>
<stream:stream to="xmpp.example.org" version="1.0" xmlns="jabber:client" xmlns:stream="http://etherx.jabber.org/streams">

サーバーは前回と同様にIDを発行して、featuresを含めて返します。
今回は、次のSASL認証に必要な情報をfeaturesに含めます。

server to client
<?xml version="1.0"?>
<stream:stream from="xmpp.example.org" id="7de8a2" version="1.0" xmlns="jabber:client" xmlns:stream="http://etherx.jabber.org/streams">
  <stream:features>
    <mechanisms xmlns='urn:ietf:params:xml:ns:xmpp-sasl'>
      <mechanism>DIGEST-MD5</mechanism>
      <mechanism>CRAM-MD5</mechanism>
      <mechanism>X-OAUTH2</mechanism>
      <mechanism>PLAIN</mechanism>
    </mechanisms>
  </stream:features>

SASL認証

XMPPでは、RFC2222で定義されているSASLというチャレンジ/レスポンス型の認証方式を使って、ユーザーの認証を行います。

大まかな手順は次のようになります

  1. サーバーはサポートしている認証メカニズムのリストを提示
  2. クライアントは、提示されたものの中から認証メカニズムを選択
  3. 選択した認証メカニズムの仕様に従って、認証が完了するまで、クライアントとサーバー間でチャレンジとレスポンスの応答を繰り返す

XMPPで利用される、SASLの認証メカニズムはだいたい決まっていて、次の3つをサポートしているサーバーやクライアントが多いようです。

  • PLAIN
  • CRAM-MD5
  • DIGEST-MD5

Oceanでも、この3つをサポートしています。

さらに最近では多くのWebサービスが、OAuthのような三者間の認可のメカニズムの上でAPIを提供していますが、OAuthで発行したトークンを、このSASLで使えるように、独自のメカニズムを定義しているケースも多いようです。

以下のようなものが使われていたりします。

  • X-FACEBOOK-PLATFORM
  • X-OAUTH2
  • X-MESSENGER-OAUTH2

Oceanでは、X-OAUTH2をサポートし、PLAIN認証とほぼ同じような手続で、単純にアクセストークンにをbase64エンコードしたものを渡せばよいようになっています。

ここでは、メカニズムにPLAINを選択した場合の手続きの例を示すことにします。

client to server
<auth xmlns='urn:ietf:params:xml:ns:xmpp-sasl' mechanism='PLAIN'>AGp1bGlldAByMG0zMG15cjBtMzA=</auth>

サーバーは受け取ったデータを検証します。認証が成功した場合は、次のようにsuccessを返します。失敗した場合はfailureを返しますが、ここではその例を省略します。

server to client
<success xmlns='urn:ietf:params:xml:ns:xmpp-sasl'/>

クライアントはsuccessを受け取って認証が完了したことを確認すると、上で説明したTLS完了時の処理と同様にstreamをリフレッシュします。

client to server
<?xml version="1.0"?>
<stream:stream to="xmpp.example.org" version="1.0" xmlns="jabber:client" xmlns:stream="http://etherx.jabber.org/streams">

サーバーは、今度はbindsessionfeaturesに含めて返します。

server to client
<?xml version="1.0"?>
<stream:stream from="xmpp.example.org" id="9f8ee5" version="1.0" xmlns="jabber:client" xmlns:stream="http://etherx.jabber.org/streams">
  <stream:features>
    <bind xmlns='urn:ietf:params:xml:ns:xmpp-bind'/>
    <session xmlns='urn:ietf:params:xml:ns:xmpp-session'/>
  </stream:features>

bindsessionについては、それぞれ、次に続くリソースのバインディングセッションの確立で解説します。

認証メカニズムの安全性

例えばiChatを利用したときに、TLSを利用しない状態で、あるいはTLSを利用したとしても自己署名の証明書を利用したような場合に、SASLのメカニズムにPLAINを選択すると、警告ウィンドウが出ます。

PLAINメカニズムでは、ユーザー名とパスワードをつなげたものをBase64エンコードしただけのものを送信するだけなので、平文、あるいはいい加減な証明書を使ったTLS接続上で流すのは危険だ、ということなのでしょう。

DIGEST-MD5CRAM-MD5のようなメカニズムを選択した場合は、TLSではない接続の上でも警告は出ません。

DIGEST-MD5やCRAM-MD5では、パスワードを含めた文字列を最終的にハッシュ化してから送信するので、送信経路で見られても安全だ、という判断なのでしょう。

ということで、PLAINよりもDIGEST-MD5やCRAM-MD5のほうが安全で推奨されているかというと、それも間違いです。

実際にはいくつかの問題があります。

まず、このようにパスワードをハッシュ化して送信するタイプの認証方式を利用する場合、クライアントが送信してきたデータを検証するために、サーバー側も同じようにパスワードをハッシュ化して比較しなしなければならないのです。つまり、ユーザーの生パスワードをデータベースのどこかに保存しておかなければなりません。これは、現在のサービス運営において、やってはいけないことの一つとなっています。

あるいは、DIGEST-MD5などと全く同じ方式であらかじめハッシュ化したものをデータベースに保存しておく、という方法も考えられますが、ハッシュ化の方式を外部から推測できてしまうのも問題です。またMD5の強度も現在では問題になります。

ハッシュ系のメカニズムが安全だというのは、あくまで通信経路に限った話であって、生でパスワードを保存しているようなサービスにアカウントを預けていることがそもそも安全とは言えません。

ここで話をPLAINに戻します。上でPLAINの危険性に注意しましたが、それはあくまで通信経路が暗号化されてなかった場合の話です。適切にTLSを利用すればその問題はなくなります。

なので、サービスとしてきちんとしたものを提供するならば、サーバーは自己署名ではない証明書を準備して、設定ファイルでTLSを有効にして、メカニズムはPLAINX-OAUTH2のどちらか、あるいは両方を許可するのがよいでしょう。

新しいRFCであるRFC6120では、DIGEST系の認証の代わりにSCRAMという仕組みが推奨されるようになりました。SCRAM-SHA-1SCRAM-SHA-1-PLUSというのがそのためのSASLメカニズムになります。くわしくはRFC5802 Salted Challenge Response Authentication Mechanism ( SCRAM ) SASL and GSS-API Mechanismsを参照するとよいでしょう。

ただし、ほとんどのXMPPサーバー、クライアントでは対応がまだ進んでいない状況です。

リソースのバインディング

認証までが完了すると、ストリームは次に、リソースと紐付けられなくてはなりません。

この段階でリソースを発行し、コネクションとJIDを紐づけることになります。JIDやリソースについては前章での解説を参照して下さい。クライアント/サーバー間のやりとり[ JID - 宛先の特定 ]

基本的には次のような手順になります。

  1. クライアントがサーバーにリソースの紐付けを要求
  2. サーバーはリソースを確保し、データベースに記録後、クライアントにフルJIDを返す

仕様上は、1のステップで、クライアントはリソース紐付け要求を出すと同時に、希望するリソースを指定することができるようになっています。前章の例で出したhomeとかworkみたいなものですね。ただし、これも前章で説明の通り、現状セマンティックリソースにはあまり意味はありません。

ですので、Oceanでは、1のステップでクライアントが候補として提出したリソースは無視し、2のステップで、サーバーが強制的に新たなリソースを生成してしまう方式にしています。

この方式にはいくつかメリットがあるのでそうしています。一つは、無駄なコンフリクトから発生するサーバーとクライアント間のラウンドトリップの削減です。他にもクラスターの構築やHTTPバインディングを行うときに利点があるのですが、ここでは解説を省略しておきます。

それでは、実際のデータのやり取りを見てみましょう

クライアントはサーバーにbindリクエストを送信します。

client to server
<iq id='tn281v37' type='set'>
  <bind xmlns='urn:ietf:params:xml:ns:xmpp-bind'/>
</iq>

次のように使いたいリソースを希望することも出来ます。

client to server
<iq id='tn281v37' type='set'>
  <bind xmlns='urn:ietf:params:xml:ns:xmpp-bind'>
    <resource>balcony</resource>
  </bind>
</iq>

サーバーはbindリクエストを受け取ると、適切にリソースを生成し、このストリームと紐づけてデータベースなどに記録したのち、割り当てられたJIDを返します。クライアントからリソースの指定があった場合に、それを使うか、あるいは希望を無視して、サーバー側で生成したものを強制的に割り当ててしまうかはサーバー側の実装次第です。

上述の通り、Oceanでは後者を採用しています。

server to client
<iq id='tn281v37' type='result'>
  <bind xmlns='urn:ietf:params:xml:ns:xmpp-bind'>
    <jid>juliet@xmpp.example.com/4db06f06-1ea4-11dc-aca3-000bcd821bfb</jid>
  </bind>
</iq>

セッションの確立

次にセッションの確立を行うのですが、実はこれは空のsession IQスタンザをクライアントがサーバーに送り、サーバーも空のスタンザで返すだけで他には何もしません。歴史的経緯で、仕様上の後方互換のためだけにこの処理が残っています。

client to server
<iq to='xmpp.example.com' type='set' id='sess_1'>
  <session xmlns='urn:ietf:params:xml:ns:xmpp-session'/>
</iq>
server to client
<iq from='xmpp.example.com' type='result' id='sess_1'/>

ロスターの要求

ここまで来たら後少しです。クライアントはサーバーに対して、この段階で、ロスターの要求をすることになっています。

client to server
<iq id='hu2bac18' type='get'>
  <query xmlns='jabber:iq:roster'/>
</iq>
server to client
<iq id='hu2bac18' to='juliet@example.com/balcony' type='result'>
  <query xmlns='jabber:iq:roster'>
    <item jid='romeo@example.net' name='Romeo' subscription='both'><group>Friends</group></item>
    <item jid='mercutio@example.com' name='Mercutio' subscription='from'/>
    <item jid='benvolio@example.net' name='Benvolio' subscription='both'/>
  </query>
</iq>

ロスターのバージョニング

さて、毎回ログインの度にロスターを要求することになると、登録された友人が多い場合は、通信の転送量が馬鹿になりません。1000人友人がいるような場合は毎回ログイン時に1000個のロスターアイテムを要求することになってしまいます。

そこでRFC6121より、ロスターのバージョニング仕様が導入されました

ただし、RFC6121が登場したのは2011年3月なので、かなり最近の話です。
ほとんどのアプリケーション、ライブラリではまだ利用されていません。

サーバーがクライアントからのロスター要求に答えて、リストを返すところをもう一度みてみましょう。
バージョニングをサポートする場合は、query要素にver属性を付けてバージョン番号を入れておくように変更します。

server to client
<iq id='hu2bac18' to='juliet@example.com/balcony' type='result'>
  <query xmlns='jabber:iq:roster' ver='ver11'>
    <item jid='romeo@example.net' name='Romeo' subscription='both'><group>Friends</group></item>
    <item jid='mercutio@example.com' name='Mercutio' subscription='from'/>
    <item jid='benvolio@example.net' name='Benvolio' subscription='both'/>
  </query>

クライアントはこのようなバージョン番号付きのリストを受け取ると、ロスターの内容をローカルのデータベースにキャッシュしておき、バージョン番号も記録しておきます。

次回以降、クライアントはロスター要求を行う場合、次のようにバージョン番号を加えます。

client to server
<iq from='juliet@xmpp.example.com/balcony' id='hu2bac18' type='get'>
    <query xmlns='jabber:iq:roster' ver='ver11'/>
</iq>

サーバーは、バージョン番号を確認します。指定されたバージョン番号が最新のものである場合は、次のように空の結果を返すとよいでしょう。

server to client
<iq from='juliet@xmpp.example.net' id='hu2bac18' to='juliet@xmpp.example.net/home' type='result'/>

これで無駄なデータベースアクセスやペイロードが削減できました。

では指定されたバージョン以降、友人が追加、削除されるなどの変更があった場合はどうすればよいでしょうか。
サーバーは二つのパターンのうちのどちらかを採用することになっています。

  1. 最新バージョンの値を含めた上で、全てのロスターアイテムを含めて返す。
  2. まず空の結果を返し、アップデート分はロスタープッシュとして後で順番に渡していく。

vCardリクエスト

名簿にリストアップされたそれぞれのユーザーの詳細な情報を取るために、あわせてvCardのリクエストのリクエストを行うのが定番の処理になっていて、ほとんどのクライアントがそうしています。vCardに関しては拡張の章で別途解説します。

イニシャルプレゼンス

ログイン処理の最後の手続きとして、クライアントはサーバーに対し、プレゼンスを送信することになっています。
中身は空のシンプルなプレゼンスです。

client to server
 <presence/> 

このプレゼンスをイニシャルプレゼンスと呼び、サーバーはこれを受けると次のように処理します。

このユーザーがログインした事を、このユーザーをフォローしている友人の、全てのオンラインのストリームに対して通知する。

このユーザーがフォローしている友人の、全てのオンラインのストリームに関するプレゼンス情報を、このユーザーに対して通知する。

有効状態

ここまでの手続きで、認証も済み、セッションIDにあたりリソースを発行してフルJIDを取得しました。友人の名簿も手元にあり、誰がオンラインかが分かっています。また、友人の側も、自分がオンラインになったことが分かっています。

あとはオンラインの友人に対してメッセージを送信し、コミュニケーションを開始するだけです。

ログアウト

ログアウトするときには、クライアントは、無効プレゼンスをサーバーに送信してから、ストリームを閉じて、TCPコネクションを切断します。

client to server
<presence type="unavailable"/></stream>

サーバーはこれを受け取ると、このユーザーをフォローしている友人の、全てのオンラインストリームに対して、このユーザーがログアウトしたことを通知します。

さらに、場合によっては、サーバーはここで様々な後始末をしなければならないかもしれません。例えば、サーバーがグループチャット機能をサポートしていた場合、このユーザーが参加していた部屋からの退出処理を済ませていなかったとしたら、この段階で自動的に退出処理も行わなければならないわけです。

また、ユーザーがお行儀よく無効プレゼンスを送信せずに切断してしまった場合は、サーバーが自動的にフォロワーに対する通知をを始めとする後始末を行ってあげなければなりません。

タイムアウト

XMPPサービスを利用する場合、基本的にストリームはつなぎっぱなしにしておきます。
ストリームをつないでいる間ずっとチャットし続けるわけではなく、オンライン状態にしたまま放置している時間の方がむしろ長い、というケースも多いでしょう。

ただし、何も送信しない状態がずっと続くことを許可してしまうと、切断検知がしにくくなりコネクションの管理に支障をきたします。ですので、こういったサービスでは定期的なpingの送信が行われるのが一般的です。
サーバー側は受信バッファに対するデータ受信が一定期間ない場合、そのコネクションを切断するようにします。

XMPPではPing拡張仕様がありますのでそれを利用すると良いでしょう。
次のように中身のないスタンザのやりとりを定期的に行うことで、接続を失ってないことを確認しあいます。

client to server
<iq from='juliet@xmpp.example.org' to='xmpp.example.org' id='s2c1' type='get'>
  <ping xmlns='urn:xmpp:ping'/>
</iq>
server to client
<iq from='xmpp.example.org' to='juliet@xmpp.example.org' id='s2c1' type='result'/>

このようなpingスタンザの代わりに、単純に改行コード\r\nなどを飛ばしてくるクライアントもよく存在します。

カーネルチューニング

上記の例はあくまでアプリケーションレイヤーで切断検知を補助する仕組みです。今後は、スマートフォンの普及により、安定しないインターネット接続環境でのサービスの利用が増えることが考えられ、より細かい制御が要求されるシーンが増えるであろうことが予想されます。 sysctlなどを利用して以下のようなLinuxのカーネルパラメータを変更することも検討してみるとよいでしょう。

  • net.ipv4.tcp_syn_retries
  • net.ipv4.tcp_synack_retries
  • net.ipv4.tcp_retries2
  • net.ipv4.tcp_orphan_retries
  • net.ipv4.tcp_tw_recycle
  • net.ipv4.tcp_fin_timeout