このドキュメントでは、Apache JServプロトコル バージョン1.3 (以後はajp13)について説明します。 現在、プロトコルの動作方法についてのドキュメントはまったく存在しません。 このドキュメントは、mod_jkの保守をおこなう人たちや、プロトコルを他に(たとえば、Jakarta 4.xに)移植したい人たちがより楽に作業できるようにするために、この問題を改善しようとする試みです。
私は誰?
私は、このプロトコルの設計者の一人ではありません - 私はGal Shachorがオリジナルの設計者だったと信じています。
このドキュメントのすべての情報は、私がTomcat 3.xのコードで見つけた実際の実装から得ています。
私は、このドキュメントは非常に便利だと思いますが、完全に正確かどうかについては保証できません。
また、私は設計方針の決定の理由についても知りません。
私ができることは、ある選択枝に対して可能性のある事実を提供することだけですが、それらは単なる私の推測にすぎません。
一般的に、Shachorが書いたC言語のコードは非常にきれいで、(ほとんどドキュメント化されていませんが)理解しやすいものです。
私がすでにJava言語のコードを書き直したので、比較的読みやすくなったと思います。
設計目標
Gal Shachorがjakarta-devメーリングリストに投稿した電子メールによると、mod_jk(とajp13)の最初の目標は、mod_jservとajp12を以下のように拡張することでした(私は、WebサーバとServletコンテナ間の通信に関連した目標を取り込んだだけです)。
isSecure()
とgetScheme()
が機能的に正しく動作するようにします。
Servletで、リクエスト属性として、クライアントの認証と暗号アルゴリズムが利用できるようになります。
概要
ajp13プロトコルは、パケット指向です。
より読みやすいプレーンテキスト形式ではなく、たぶん性能向上のためにバイナリ形式を選択しています。
Webサーバは、ServletコンテナとTCPコネクション上で通信します。
ソケット作成の無駄な過程を削減するために、WebサーバはServletコンテナに対して永続的なTCPコネクションを保持して、複数のリクエスト/レスポンスサイクルの間コネクションを再利用しようとします。
いったんある特定のリクエストにコネクションを割り当てたら、リクエスト処理サイクルが終了するまでは他に使用することはありません。 つまり、リクエストは、コネクション上で多重化されることはありません。 これによって、一度に複数のコネクションをオープンできるにもかかわらず、コネクションのどちらかをより単純なコードにすることができます。
いったんWebサーバがServletコンテナに対するコネクションをオープンしたら、コネクションは以下の状態のどれかの状態になります。
いったんコネクションがある特定のリクエストを処理するために割り当てられたら、基本的なリクエスト情報 (たとえば、HTTPヘッダなど)は、コネクション上を高度に圧縮された形式(たとえば、共通の文字列は整数でエンコードされます)で送信されます。 この詳しいフォーマットを、この後のリクエストパケット構造で説明します。 リクエストに対してボディ部が存在する場合(content-length > 0)には、すぐ後に別のパケットで送信されます。
この時点では、たぶんServletコンテナはリクエスト処理を開始する用意ができています。 その場合には、以下のメッセージをWebサーバに返信することができます。
このプロトコルは、XDRの特徴を少し受け継いでいますが、多くの点で異なっています(たとえば、4バイトのアライメントがないことです)。
バイトオーダー : 私はバイトのエンディアンについてはよく知りません。 私は、リトルエンディアンだと思いますが、それはXDRがそうだからです。 さらに、(C言語側のコードがそうであるように)私はsys/socketライブラリが自動的に処理してくれると予想しています。 socketコールについて誰かもっと詳しく知っている人が参加してくれればすばらしいと思います。
このプロトコルには、byte, boolean, integer, stringという4つのデータ型があります。
strlen
のように末尾の'\0'を含まないことに注意してください。
これはJava言語側では少々混乱の元になっていて、これらのターミネータを読み飛ばすために、一見奇妙に見えるインクリメント演算子がコード中にちらばっています。
Servletコンテナが返信する文字列を読む時には、'\0'文字で終了しておけば、C言語のコードで一つのバッファに書き込んで、その参照を渡すことができるので、コピーが不要になりますから、私はこれはC言語コードをより効率的にするためにおこなったと信じています。
'\0'がない場合には、C言語のコードでは、C言語の文字列の仕様に従うために、一度コピーしなければいけません。
パケットサイズ
多くのコードによると、最大パケットサイズは8 * 1024 バイト (8K) です。
パケットの実際の長さは、ヘッダ部にエンコードされます。
パケットのヘッダ
Servletコンテナは、Webサーバに以下のようなメッセージのタイプを送信することができます。
上記のメッセージは、それぞれ別の内部構造を持っていますので、この後に説明します。
サーバからコンテナに送信されるタイプ"Forward Request"のメッセージは、以下の通りです。
上記の詳細は以下の通りです。
これは、0x9999 (==0xA000 - 1)より長いヘッダ名が存在しないことを仮定すれば動作しますが、これはやや独断的ですが、きわめて妥当でしょう。(もし、あなたが私のようにcookieの仕様と、どのくらいの長さのヘッダを得ることができるかについて考え始めたとしても、恐れることはありません
- というのは、これはヘッダの名前の制限であって、ヘッダの値の制限ではないからです。
まともに管理されていない巨大なヘッダ名についてHTTP仕様が定義することは、とてもありえません。)
注意:
この基本属性のリスト以外にも、他の多くの属性を
最後に、すべての属性を送信した後に、属性のターミネータとして、0xFFを送信します。
これは、属性のリストの終了とともに、リクエストパケット全体の終了を知らせます。
コンテナがサーバに返信することができるメッセージです。
ボディ部中にそれ以上データがない場合(すなわち、Servletコンテナがボディ部の最後を越えて読み込もうとした場合)には、サーバは送信データ長が0の"空の"パケットを返信します。
リクエストヘッダの合計サイズが最大パケットサイズを越えた時に、何が起るでしょうか?
8K以上の場合には、リクエストヘッダの二番目のパケットを送信するための対策が何もされていません(確かめていませんが、私はレスポンスヘッダはうまく処理できると思います)。
私は、リクエストヘッダの初期集合に入っている8K以上のデータを取得する方法が存在するかどうかは知りませんが、たぶん存在するでしょう(長いSSL情報を持った長いCookieと多量の環境変数を組み合わせれば、簡単に8Kを越えるでしょう)。
私は、このような場合にヘッダが送信できるかを試す前に、コネクタが落ちるのではないかと思いますが、確かめたわけではありません。
認証についてはどうなのでしょうか?
Webサーバとコンテナの間の認証があるようには見えません。
これについては、私は潜在的な危険を感じてます。
サーバからコンテナに送信されるパケットは、AB
で開始します (つまり、ASCIIコードのAの直後にASCIIコードのBがきます)。
この最初の2バイトの直後に、送信データの長さの整数がきます(この前に説明したようにエンコードされています)。
つまり、論理的な最大データサイズは2^16ですが、実際の最大値は8Kに制限されています。
パケットフォーマット (サーバ->コンテナ)
バイト
0
1
2
3
4...(n+3)
内容
0x12
0x34
データ長 (n)
データ
大部分のパケットでは、送信データの最初の1バイトにメッセージのタイプがエンコードされています。
この例外は、サーバからコンテナに送信されるリクエスト内容のパケットです -
これらは標準パケットヘッダ (0x1234の後にパケット長)を付加して送信されますが、その直後にプレフィクスコードはありません(これは私は間違いのように思います)。
Webサーバは、以下のようなメッセージをServletコンテナに送信することができます。
パケットフォーマット (コンテナ->サーバ)
バイト
0
1
2
3
4...(n+3)
内容
A
B
データ長 (n)
データ
コード
パケットのタイプ
意味
2
Forward Request
その直後のデータを用いてリクエスト処理サイクルを開始します。
7
Shutdown
Webサーバが、コンテナに自分自身を停止させるように依頼します。
コード
パケットのタイプ
意味
3
Send Body Chunk
ServletコンテナからWebサーバにメッセージボディのチャンクを送信します(そして、たぶんブラウザに対して送信されます)。
4
Send Headers
ServletコンテナからWebサーバに、レスポンスヘッダを送信します(そして、たぶんブラウザに対して送信されます)。
5
End Response
レスポンス(さらに、リクエスト処理サイクル)の最後をマークします。
6
Get Body Chunk
まだリクエストがすべて転送されていない場合には、さらにデータを要求します。
リクエストパケット構造
AJP13_FORWARD_REQUEST :=
prefix_code 2
method (byte)
protocol (string)
req_uri (string)
remote_addr (string)
remote_host (string)
server_name (string)
server_port (integer)
is_ssl (boolean)
num_headers (integer)
request_headers *(req_header_name req_header_value)
?context (byte string)
?servlet_path (byte string)
?remote_user (byte string)
?auth_type (byte string)
?query_string (byte string)
?jvm_route (byte string)
?ssl_cert (byte string)
?ssl_cipher (byte string)
?ssl_session (byte string)
?attributes *(attribute_name attribute_value)
request_terminator (byte)
req_header_name :=
sc_req_header_name | (string) [この解析方法については、この後を参照してください]
sc_req_header_name := 0xA0 (byte)
req_header_value := (string)
attribute_name := (string)
attribute_value := (string)
request_terminator := 0xFF
だからといって、重要なヘッダがすべてあるわけではありませんが、"content-olength"はコンテナが別のパケットをすぐに要求するかどうかを決定します。
サーバは、
OPTIONS 1
GET 2
HEAD 3
POST 4
PUT 5
DELETE 6
TRACE 7
PROPFIND 8
PROPPATCH 9
MKCOL 10
COPY 11
MOVE 12
LOCK 13
UNLOCK 14
ACL 15
accept 0xA001
accept-charset 0xA002
accept-encoding 0xA003
accept-language 0xA004
authorization 0xA005
connection 0xA006
content-type 0xA007
content-length 0xA008
cookie 0xA009
cookie2 0xA00A
host 0xA00B
pragma 0xA00C
referer 0xA00D
user-agent 0xA00E
これを読み込むJava言語のコードでは、まず最初の2バイトの整数を読み込んで、
MSB (Most Significant Byte)が'0xA0'の場合には、2番目のバイトをヘッダ名の配列に対するインデックスである整数と見なします。
最初の1バイトが'0xA0'でない場合には、2バイトの整数が文字列の長さを表していると見なして、それを読み込みます。content-length
ヘッダは非常に重要です。
このヘッダが存在して、0以外の値をとる場合には、コンテナはリクエストが(たとえば、POSTリクエストのように)ボディ部を持っていて、ボディ部を得るために個別のパケットをインプットストリームから読み込むことを仮定しなければいけません。
?
が先頭についた属性(例, ?context
)のリストのすべては必須ではありません。
それぞれについて、属性のタイプを示す1バイトコードが定義されていて、文字列をこの値に変換します。
これらのヘッダは、(C言語のコードは常に以下の順序で送信するのですが)任意の順序で送信できます。
必須ではない属性のリストの最後を知らせるために、特別な終了コードを送信します。
バイトコードのリストは以下の通りです。
context 1 [現在はまだ実装されていません]
servlet_path 2 [現在はまだ実装されていません]
remote_user 3
auth_type 4
query_string 5
jvm_route 6
ssl_cert 7
ssl_cipher 8
ssl_session 9
req_attribute 10
terminator 0xFF
context
とservlet_path
は、現在はC言語のコードでは設定されませんが、Java言語のコードの大部分では、これらのフィールドが送信されても完全に無視されます
(そして、これらのコードの後に文字列が送信された場合には、実際に失敗します)。
私はこれがバグなのか、未実装の仕様なのか、それとも単に昔のコードの痕跡なのかは知りませんが、コネクションの両側で実装されていません。remote_user
とauth_type
は、どうもHTTPレベルの認証を対応していて、リモートのユーザのユーザ名とその本人確認をおこなうために使用した認証のタイプ(例、Basicダイジェスト)のようです。
私はパスワードも一緒に送信されるのかはよく知りませんし、HTTP認証についてはまったく知りません。query_string
とssl_cert
、
ssl_cipher
、ssl_session
は、HTTPとHTTPSの相当する部分に対応しているようです。jvm_route
は、私が理解している限りでは、sticky sessionをサポートするために -
つまり複数の付加分散サーバが存在する場合に、ユーザのセッションとある特定のTomcatインスタンスを関連付けるために使用しているようです。
私は詳しいことについては知りません。req_attribute
コード (10)を用いて送信します。
属性名と値を表す文字列のペアは、このコードの後に直接送信されます。
環境変数は、このメソッドを用いて渡します。
shutdown
パケットも送信することができます。
基本的なセキュリティを保持するために、実際にはコンテナはリクエストが運用しているマシンと同じマシンから送信されてくる場合にのみ、終了します。
レスポンスパケット構造
AJP13_SEND_BODY_CHUNK :=
prefix_code 3
chunk_length (integer)
chunk *(byte)
AJP13_SEND_HEADERS :=
prefix_code 4
http_status_code (integer)
http_status_msg (string)
num_headers (integer)
response_headers *(res_header_name header_value)
res_header_name :=
sc_res_header_name | (string) [この解析方法については、この後を参照してください]
sc_res_header_name := 0xA0 (byte)
header_value := (string)
AJP13_END_RESPONSE :=
prefix_code 5
reuse (boolean)
AJP13_GET_BODY_CHUNK :=
prefix_code 6
requested_length (integer)
詳細は以下の通りです。
Content-Type 0xA001
Content-Language 0xA002
Content-Length 0xA003
Date 0xA004
Last-Modified 0xA005
Location 0xA006
Set-Cookie 0xA007
Set-Cookie2 0xA008
Servlet-Engine 0xA009
Status 0xA00A
WWW-Authenticate 0xA00B
コードや文字列のヘッダ名の後に、ヘッダの値が直接エンコードされます。reuse
フラグがtrue (==1)の場合には、このTCPコネクションを新しく到着するリクエストを処理するために使用できます。
reuse
がfalse (実際のC言語のコードでは1以外の値です)の場合には、このコネクションをクローズしなければいけません。request_length
と、最大送信ボディサイズ (XXX)、そしてリクエストボディから送信されて実際に残っている値の中の最小値に相当する両のデータを返信します。
私の疑問点