オリジナルのドキュメントは Dan Milstein により、
これは Apache JServ Protocol バージョン1.3 (以下ajp13) について記述します。プロトコルがどのように機能するかに関する現在のドキュメントは、どうやら存在しないようです。本ドキュメントは、JKのメンテナーや、プロトコルをどこかに移植したい人 (例えば jakarta 4.x へ) の作業を容易にするために、その状況を改善しようとする試みです。
オリジナルのドキュメントは Dan Milstein により、
これは Apache JServ Protocol バージョン1.3 (以下ajp13) について記述します。プロトコルがどのように機能するかに関する現在のドキュメントは、どうやら存在しないようです。本ドキュメントは、JKのメンテナーや、プロトコルをどこかに移植したい人 (例えば jakarta 4.x へ) の作業を容易にするために、その状況を改善しようとする試みです。
私はこのプロトコルの設計者の一人ではありません。オリジナルの設計者は Gal Shachor だと私は信じています。このドキュメントのすべては、Tomcat 3.x コードで見つけた実際の実装から派生しています。役立つことを願っていますが、完璧な正確さを保証することはできません。また、特定の設計上の決定がなぜなされたのかも知りません。私ができた範囲で、特定の選択についていくつかの可能な正当化を提示しましたが、それらは単なる私の推測にすぎません。一般的に、Shachorが書いたCコードは非常にクリーンで理解しやすいものですが(ほとんど全く文書化されていません)。私はJavaコードを整理し、合理的に読みやすいものになったと思います。
Gal Shachor から jakarta-dev メーリングリストへのメールによると、JK (ひいてはajp13) の当初の目標は、(Webサーバーとサーブレットコンテナ間の通信に関連する目標のみを含みます)
isSecure()
および getScheme()
が正しく機能するようにすること。クライアント証明書と暗号スイートは、リクエスト属性としてサーブレットで利用可能になります。ajp13 プロトコルはパケット指向です。より読みやすいプレーンテキストではなくバイナリ形式が選択されたのは、おそらくパフォーマンス上の理由によるものです。Webサーバーは TCP 接続を介してサーブレットコンテナと通信します。ソケット作成という高価なプロセスを削減するため、Webサーバーはサーブレットコンテナへの永続的な TCP 接続を維持し、複数のリクエスト/レスポンスサイクルで接続を再利用しようとします。
一度特定の要求に割り当てられた接続は、要求処理サイクルが終了するまで他の要求には使用されません。言い換えれば、要求は接続上で多重化されません。これにより、接続の両端でのコードがはるかに単純になりますが、同時に開かれる接続の数が増えることになります。
Webサーバーがサーブレットコンテナへの接続を開いた後、接続は次のいずれかの状態になります。
一度接続が特定の要求を処理するために割り当てられると、基本的な要求情報(HTTPヘッダーなど)は、極めて凝縮された形式(例:共通の文字列は整数としてエンコードされる)で接続を介して送信されます。その形式の詳細は、後述の「要求パケット構造」にあります。要求に本文がある場合(content-length > 0)、それは直後に別のパケットで送信されます。
この時点で、サーブレットコンテナはリクエストの処理を開始する準備ができています。処理中に、以下のメッセージをWebサーバーに送り返すことができます。
各メッセージには、異なる形式のデータパケットが付属しています。詳細は以下のレスポンスパケット構造を参照してください。
このプロトコルにはXDRの伝統が少しありますが、多くの点で異なります(例えば、4バイトアライメントはありません)。
AJP13 は、すべてのデータ型にネットワークバイトオーダーを使用します。
プロトコルには、バイト、ブール値、整数、文字列の4つのデータ型があります。
strlen
のようです。これはJava側では少し混乱を招き、これらの終端文字をスキップするために奇妙な自動インクリメント文が散見されます。これがなされた理由は、サーブレットコンテナが送り返す文字列を読み取る際に、Cコードが非常に効率的になるようにするためだと私は考えています。終端の\0文字があれば、Cコードはコピーせずに単一のバッファへの参照を渡すことができます。\0が欠落している場合、Cコードは文字列の概念を得るために内容をコピーする必要があるでしょう。この場合、サイズが -1 (65535) の場合はヌル文字列を示し、長さの後にデータは続きません。多くのコードによると、最大パケットサイズは 8 * 1024 バイト (8K) です。パケットの実際の長さはヘッダーにエンコードされています。
サーバーからコンテナに送信されるパケットは 0x1234
で始まります。コンテナからサーバーに送信されるパケットは AB
(これはAのASCIIコードの後にBのASCIIコードが続くもの) で始まります。最初の2バイトの後には、ペイロードの長さを示す整数(上記のようにエンコードされる)が続きます。これにより、最大ペイロードが2^16にもなる可能性が示唆されますが、実際にはコードが最大値を8Kに設定しています。
パケット形式(サーバー → コンテナ) | |||||
---|---|---|---|---|---|
バイト | 0 | 1 | 2 | 3 | 4...(n+3) |
内容 | 0x12 | 0x34 | データ長 (n) | データ |
パケット形式(コンテナ → サーバー) | |||||
---|---|---|---|---|---|
バイト | 0 | 1 | 2 | 3 | 4...(n+3) |
内容 | A | B | データ長 (n) | データ |
ほとんどのパケットでは、ペイロードの最初のバイトがメッセージのタイプをエンコードします。例外は、サーバーからコンテナに送信されるリクエストボディパケットです。これらは標準のパケットヘッダー (0x1234 とその後のパケット長) で送信されますが、その後のプレフィックスコードは含まれません (これは私には間違いのように見えます)。
Webサーバーはサーブレットコンテナに次のメッセージを送信できます。
コード | パケットのタイプ | 意味 |
---|---|---|
2 | リクエスト転送 | 以下のデータでリクエスト処理サイクルを開始します。 |
7 | シャットダウン | Webサーバーはコンテナにシャットダウンを要求します。 |
8 | Ping | Webサーバーはコンテナに制御を引き継ぐよう要求します (セキュアログインフェーズ)。 |
10 | CPing | WebサーバーはコンテナにCPongですばやく応答するよう要求します。 |
なし | データ | サイズ(2バイト)と対応するボディデータ。 |
基本的なセキュリティを確保するため、コンテナはリクエストがホストされているマシンと同じマシンから来た場合にのみ Shutdown
を実行します。
最初のData
パケットは、WebサーバーによってForward Request
の直後に送信されます。
サーブレットコンテナはWebサーバーに次の種類のメッセージを送信できます。
コード | パケットのタイプ | 意味 |
---|---|---|
3 | ボディチャンク送信 | サーブレットコンテナからWebサーバーへ(そしておそらくブラウザへ)ボディのチャンクを送信します。 |
4 | ヘッダー送信 | サーブレットコンテナからWebサーバーへ(そしておそらくブラウザへ)レスポンスヘッダーを送信します。 |
5 | レスポンス終了 | レスポンスの終了(したがってリクエスト処理サイクルの終了)を示します。 |
6 | ボディチャンク取得 | まだすべて転送されていない場合、リクエストからさらにデータを取得します。 |
9 | CPong応答 | CPingリクエストへの返答 |
上記の各メッセージには、以下に詳述する異なる内部構造があります。
サーバーからコンテナへの「Forward Request」タイプのメッセージの場合
AJP13_FORWARD_REQUEST :=
prefix_code (byte) 0x02 = JK_AJP13_FORWARD_REQUEST
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)
attributes *(attribut_name attribute_value)
request_terminator (byte) OxFF
request_headers
は以下の構造を持ちます。
req_header_name :=
sc_req_header_name | (string) [see below for how this is parsed]
sc_req_header_name := 0xA0xx (integer)
req_header_value := (string)
attributes
はオプションで、以下の構造を持ちます。
attribute_name := sc_a_name | (sc_a_req_attribute string)
attribute_value := (string)
コンテナが直ちに別のパケットを探すかどうかを決定するため、最も重要なヘッダーは「content-length」です。
Forward Requestの要素の詳細説明。
すべてのリクエストについて、これは2になります。他のプレフィックスコードの詳細については上記を参照してください。
HTTPメソッド、単一バイトとしてエンコード
コマンド名 | コード |
---|---|
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 |
REPORT | 16 |
VERSION-CONTROL | 17 |
CHECKIN | 18 |
CHECKOUT | 19 |
UNCHECKOUT | 20 |
SEARCH | 21 |
MKWORKSPACE | 22 |
UPDATE | 23 |
LABEL | 24 |
MERGE | 25 |
BASELINE_CONTROL | 26 |
MKACTIVITY | 27 |
これらはすべてかなり自明です。これらのそれぞれが必須であり、すべてのリクエストで送信されます。
request_headers
の構造は次のとおりです。まず、ヘッダーの数num_headers
がエンコードされます。次に、ヘッダー名req_header_name
/値req_header_value
のペアが続きます。一般的なヘッダー名は、スペースを節約するために整数としてエンコードされます。ヘッダー名が基本ヘッダーのリストにない場合、それは通常通り(プレフィックス長付きの文字列として)エンコードされます。共通ヘッダーsc_req_header_name
とそのコードのリストは次のとおりです(すべて大文字小文字を区別します)。
名前 | コード値 | コード名 |
---|---|---|
accept | 0xA001 | SC_REQ_ACCEPT |
accept-charset | 0xA002 | SC_REQ_ACCEPT_CHARSET |
accept-encoding | 0xA003 | SC_REQ_ACCEPT_ENCODING |
accept-language | 0xA004 | SC_REQ_ACCEPT_LANGUAGE |
authorization | 0xA005 | SC_REQ_AUTHORIZATION |
connection | 0xA006 | SC_REQ_CONNECTION |
content-type | 0xA007 | SC_REQ_CONTENT_TYPE |
content-length | 0xA008 | SC_REQ_CONTENT_LENGTH |
cookie | 0xA009 | SC_REQ_COOKIE |
cookie2 | 0xA00A | SC_REQ_COOKIE2 |
host | 0xA00B | SC_REQ_HOST |
pragma | 0xA00C | SC_REQ_PRAGMA |
referer | 0xA00D | SC_REQ_REFERER |
user-agent | 0xA00E | SC_REQ_USER_AGENT |
これを読み取るJavaコードは、最初の2バイト整数を取得し、最上位バイトに'0xA0'
が見つかった場合、2番目のバイトの整数をヘッダー名配列へのインデックスとして使用します。最初のバイトが'0xA0'でない場合、2バイト整数が文字列の長さであると仮定し、その文字列を読み込みます。
これは、ヘッダー名の長さが 0x9FFF (==0xA000 - 1) を超えないという仮定に基づいています。これは全く合理的ですが、やや恣意的です。(もし私のようにここでCookieの仕様とヘッダーの長さについて考え始めたとしても、心配はいりません。この制限はヘッダー名に対するものであり、ヘッダー値に対するものではありません。扱いにくいほど巨大なヘッダー名がHTTP仕様にすぐ現れることは考えにくいです)。
注意: content-length
ヘッダーは極めて重要です。このヘッダーが存在し、ゼロ以外の場合、コンテナはリクエストにボディがあるとみなし(例:POSTリクエスト)、すぐに別のパケットを入力ストリームから読み込んでそのボディを取得します。
?
(例: ?context
) でプレフィックスされた属性はすべてオプションです。それぞれについて、属性のタイプを示す1バイトコードがあり、その値を示す文字列が続きます。これらは任意の順序で送信できます(ただし、Cコードは常に以下のリストの順序で送信します)。オプション属性のリストの終わりを示す特別な終了コードが送信されます。バイトコードのリストは次のとおりです。
情報 | コード値 | 注記 |
---|---|---|
?context | 0x01 | 現在未実装 |
?servlet_path | 0x02 | 現在未実装 |
?remote_user | 0x03 | |
?auth_type | 0x04 | |
?query_string | 0x05 | |
?route | 0x06 | |
?ssl_cert | 0x07 | |
?ssl_cipher | 0x08 | |
?ssl_session | 0x09 | |
?req_attribute | 0x0A | 名前 (属性の名前が続きます) |
?ssl_key_size | 0x0B | |
?secret | 0x0C | |
?stored_method | 0x0D | |
are_done | 0xFF | request_terminator |
context
とservlet_path
は現在Cコードで設定されておらず、Javaコードのほとんどはこれらのフィールドに送信されるものを完全に無視します(そして一部のコードは、これらのコードの後に文字列が送信されると実際に壊れます)。これがバグなのか未実装の機能なのか、あるいは単に残存コードなのかはわかりませんが、接続の両側で欠落しています。
remote_user
および auth_type
はおそらく HTTP レベルの認証を指し、リモートユーザーのユーザー名と、その身元を確立するために使用された認証タイプ (例: Basic, Digest) を伝達します。パスワードも送信されない理由については明確ではありませんが、HTTP 認証の内外を知っているわけではありません。
query_string
, ssl_cert
, ssl_cipher
, ssl_session
は、HTTP および HTTPS の対応する部分を参照しています。
route
は、私の理解では、複数のロードバランシングサーバーが存在する場合に、ユーザーのセッションを特定のTomcatインスタンスに関連付ける、スティッキーセッションをサポートするために使用されます。詳細は不明です。
この基本属性リスト以外にも、req_attribute
コード (0x0A) を介して任意の数の他の属性を送信できます。このコードの各インスタンスの直後に、属性名と値を表す文字列のペアが送信されます。環境変数はこの方法で渡されます。
最後に、すべての属性が送信された後、属性ターミネータである0xFFが送信されます。これは属性リストの終わりと、リクエストパケットの終わりを両方示します。
コンテナがサーバーに送り返すことができるメッセージの場合。
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) [see below for how this is parsed]
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)
詳細
このチャンクは基本的にバイナリデータであり、直接ブラウザに送り返されます。
ステータスコードとメッセージは通常のHTTPのものです(例:「200」と「OK」)。レスポンスヘッダー名は、リクエストヘッダー名と同じ方法でエンコードされます。コードが文字列とどのように区別されるかについては、上記を参照してください。共通ヘッダーのコードは次のとおりです。
名前 | コード値 |
---|---|
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 (実際のCコードでは0以外の値) の場合、このTCP接続は新しい受信リクエストの処理に再利用できます。reuse
が false (==0) の場合、接続は閉じられるべきです。
コンテナはリクエストから追加のデータを要求します(ボディが最初のパケットに収まらないほど大きい場合や、リクエストがチャンク化されている場合)。サーバーは、request_length
、最大送信ボディサイズ(8186 (8 Kbytes - 6))、およびリクエストボディから実際に送信される残りのバイト数のうち、最小のデータ量を含むボディパケットを返送します。
ボディにデータがもうない場合(つまり、サーブレットコンテナがボディの終端を超えて読み取ろうとしている場合)、サーバーはペイロード長が0の「空の」パケット(0x12, 0x34, 0x00, 0x00)を返送します。
リクエストヘッダーが最大パケットサイズを超えた場合、どうなりますか?8Kを超えるリクエストヘッダーを2番目のパケットとして送信する規定がありません(レスポンスヘッダーについては正しく処理されていると思いますが、確信はありません)。初期のリクエストヘッダーのセットに8K以上のデータを含める方法があるかどうかはわかりませんが、おそらくあるでしょう(長いCookieと長いSSL情報、多くの環境変数を組み合わせれば、簡単に8Kに達するはずです)。この場合、コネクタはヘッダーを送信する前に失敗するだけだと思いますが、確信はありません。
認証についてはどうでしょうか?Webサーバーとコンテナ間の接続には認証がないようです。これは潜在的に危険だと感じます。